@qilitt-mickey/vue3-temp-skill 1.1.49 → 1.1.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qilitt-mickey/vue3-temp-skill",
3
- "version": "1.1.49",
3
+ "version": "1.1.51",
4
4
  "type": "module",
5
5
  "description": "Vue 3 企业级中后台项目开发规范技能包 — core-kernel 架构、按需功能模块",
6
6
  "bin": {
@@ -1,9 +1,11 @@
1
1
  {
2
- "$comment": "端到端示例 · 整页场景还原(对照效果图逐模块提取的完整交付样例,模拟设计技能真实产出)。覆盖:global 主题 + 壳层全模块 + 列表页全字段 + 表格细粒度点位(操作列形态/状态Tag/金额对齐/缩略图)+ pending 新点位。值口径演示:主题色用语义值(钴蓝-N/错误色)、尺寸用数值字面量(28px/40px 等,设计侧 v1.0 协议第 3 条合法通道)、灰阶用临时主题 hex + notes 申报。业务功能差异(新增运单按钮功能、demo 演示按钮去留)不在本变更集范围,走项目侧业务闭环开发(双层交付)。运行:node parser/index.js match <本文件>(cwd 为 skill 根)。",
2
+ "$comment": "端到端示例 · 整页场景还原(对照效果图逐模块提取的完整交付样例,模拟设计技能真实产出)。G8 模式闸正例:mode=restore + scheme + oracle(含 mockup) + coverage(含 registry) 齐备。覆盖:global 主题 + 壳层全模块 + 列表页全字段 + 表格细粒度点位(操作列形态/状态Tag/金额对齐/缩略图)+ pending 新点位。值口径演示:主题色用语义值(钴蓝-N/错误色)、尺寸用数值字面量(28px/40px 等,设计侧 v1.0 协议第 3 条合法通道)、灰阶用临时主题 hex + notes 申报。业务功能差异(新增运单按钮功能、demo 演示按钮去留)不在本变更集范围,走项目侧业务闭环开发(双层交付)。运行:node parser/index.js match <本文件>(cwd 为 skill 根)。",
3
3
  "schema": "design.changeset/1.0",
4
4
  "changeset_version": "1.0.0",
5
5
  "mapping_version": "1.0.0",
6
6
  "scene": "admin",
7
+ "scheme": "default",
8
+ "mode": "restore",
7
9
  "layout_type": "vertical",
8
10
  "global": {
9
11
  "brand_primary": "钴蓝-5",
@@ -26,12 +28,35 @@
26
28
  }
27
29
  },
28
30
  { "id": "admin.layout.header", "style": { "bg": "#FFFFFF" } },
29
- { "id": "admin.layout.header.lang_switch", "form": "inline_switch", "suppress": ["icon_only"] },
31
+ {
32
+ "id": "admin.layout.header.lang_switch",
33
+ "form": "inline_switch",
34
+ "suppress": ["icon_only"],
35
+ "style": { "icon_size": "16px", "gap": "8px", "text_font_size": "14px" }
36
+ },
30
37
  {
31
38
  "id": "admin.layout.header.user_profile",
32
39
  "style": { "avatar_width": "28px", "avatar_height": "28px", "avatar_radius": "全圆角", "name_font_size": "14px", "name_font_weight": "中等" }
33
40
  },
34
41
  { "id": "admin.layout.tabs", "form": "chrome", "style": { "bg": "#FFFFFF" } },
42
+ {
43
+ "id": "admin.content.table.header_cell",
44
+ "style": { "height": "48px", "padding": "16px", "bg": "#FAFAFA", "font_weight": "中等", "text_align": "左对齐", "nowrap": "不换行" },
45
+ "layer": { "height": "host", "padding": "host", "bg": "host", "font_weight": "host", "text_align": "inner", "nowrap": "inner" },
46
+ "suppress_inner": true
47
+ },
48
+ {
49
+ "id": "admin.content.table.cell",
50
+ "style": { "padding_y": "8px", "vertical_align": "middle" },
51
+ "layer": { "padding_y": "inner", "vertical_align": "inner" },
52
+ "suppress_inner": true
53
+ },
54
+ {
55
+ "id": "admin.content.table.cell_status",
56
+ "form": "filled",
57
+ "style": { "bg": "#E8F3FF", "text_color": "钴蓝-5", "radius": "小圆角" }
58
+ },
59
+ { "id": "component.tag.status", "form": "filled", "style": { "height": "24px", "radius": "小圆角", "bg_color": "#F6FFED" } },
35
60
  {
36
61
  "id": "admin.content.page_header",
37
62
  "style": { "title_size": "20px", "title_weight": "加粗", "desc_color": "钴蓝-3", "actions_gap": "8px" }
@@ -52,10 +77,41 @@
52
77
  { "id": "admin.content.table.area", "style": { "header_bg": "#FAFAFA", "row_hover_bg": "#FAFAFA", "radius": "小圆角" } },
53
78
  { "id": "admin.content.table.cell_amount", "style": { "align": "right", "font_variant_numeric": "等宽数字" } },
54
79
  { "id": "admin.content.table.cell_thumb", "style": { "thumb_width": "40px", "thumb_height": "40px", "thumb_radius": "小圆角" } },
55
- { "id": "component.tag.status", "form": "filled", "style": { "height": "24px", "radius": "小圆角" } },
56
80
  { "id": "component.button.danger", "style": { "bg_color": "错误色", "border_color": "错误色", "text_color": "#FFFFFF" } },
57
81
  { "id": "admin.content.pagination", "style": { "active_bg": "钴蓝-5", "active_color": "#FFFFFF" } }
58
82
  ],
83
+ "oracle": {
84
+ "mockup": "verify/运单管理-钴蓝.html",
85
+ "sids": [
86
+ "admin.layout.sidebar",
87
+ "admin.layout.sidebar.logo_area",
88
+ "admin.layout.sidebar.menu",
89
+ "admin.layout.header",
90
+ "admin.layout.header.lang_switch",
91
+ "admin.layout.header.user_profile",
92
+ "admin.layout.tabs",
93
+ "admin.content.page_header",
94
+ "admin.content.filter_area",
95
+ "admin.content.table.operation_bar",
96
+ "admin.content.table.operation_column",
97
+ "admin.content.table.area",
98
+ "admin.content.table.header_cell",
99
+ "admin.content.table.cell",
100
+ "admin.content.table.cell_status",
101
+ "admin.content.table.cell_amount",
102
+ "admin.content.table.cell_thumb",
103
+ "component.tag.status",
104
+ "component.button.danger",
105
+ "admin.content.pagination"
106
+ ]
107
+ },
108
+ "coverage": {
109
+ "registry": "references/page-specs/styles/default/scenes/admin/restore-registry.json",
110
+ "registrable": 186,
111
+ "applicable": 186,
112
+ "covered": 186,
113
+ "gaps": []
114
+ },
59
115
  "pending": [
60
116
  {
61
117
  "proposed_id": "admin.content.page_header.greeting",
@@ -0,0 +1,15 @@
1
+ {
2
+ "$comment": "G8 模式闸负例:mode=restore 但缺 oracle 与 coverage(伪还原变更集,2026-09-20 事故的机检复现件)。预期:match 打印「G8 模式闸」并 exit 1。与 changeset-pos.json 的区别仅此——mode=restore 却没带基线三件套。",
3
+ "schema": "design.changeset/1.0",
4
+ "changeset_version": "1.0.0",
5
+ "mapping_version": "1.0.0",
6
+ "scene": "admin",
7
+ "mode": "restore",
8
+ "layout_type": "vertical",
9
+ "global": { "brand_primary": "钴蓝-5" },
10
+ "changes": [
11
+ { "id": "admin.layout.sidebar.menu", "style": { "menu_bg": "钴蓝-9", "menu_active_bg": "钴蓝-5" } },
12
+ { "id": "component.button.primary", "style": { "bg_color": "钴蓝-5" } }
13
+ ],
14
+ "notes": "伪还原变更集:只换主题色就宣称场景还原——没有 oracle 验收基线(还原度不可证明)、没有 coverage 登记表记账(组内子项静默丢失不可数)。G8 之前这类变更集只收软提示、一路绿灯,落地后用户看到「只变了主题、布局细节零变化」。"
15
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$comment": "G8 强化负例(二轮):mode=restore,scheme/oracle/coverage 三个对象都在,但 oracle.mockup 与 coverage.registry 为空串 —— 「有壳无芯」的伪基线:验收抽不出效果图、覆盖率无法以登记表重算。预期:match 打印「G8 模式闸」并 exit 1。与 changeset-fullpage-restore.json 的区别仅在此三处。",
3
+ "schema": "design.changeset/1.0",
4
+ "changeset_version": "1.0.0",
5
+ "mapping_version": "1.0.0",
6
+ "scene": "admin",
7
+ "scheme": "default",
8
+ "mode": "restore",
9
+ "layout_type": "vertical",
10
+ "global": { "brand_primary": "钴蓝-5" },
11
+ "changes": [
12
+ { "id": "admin.layout.sidebar.menu", "style": { "menu_bg": "钴蓝-9", "menu_active_bg": "钴蓝-5" } }
13
+ ],
14
+ "oracle": {
15
+ "mockup": "",
16
+ "sids": ["admin.layout.sidebar.menu"]
17
+ },
18
+ "coverage": {
19
+ "registry": "",
20
+ "registrable": 186,
21
+ "applicable": 4,
22
+ "covered": 4,
23
+ "gaps": []
24
+ },
25
+ "notes": "空壳伪基线:oracle/coverage 对象齐全但 mockup 与 registry 为空。过去只校验『对象是否存在』,这类写法能一路绿灯——效果图无从抽取、A/C 无从重算,『完备性可数』形同虚设。G8 强化后予拦截。"
26
+ }
@@ -0,0 +1,44 @@
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>
package/parser/index.js CHANGED
@@ -104,7 +104,7 @@ function printHelp() {
104
104
 
105
105
  用法:
106
106
  node parser/index.js build [--dry] 校验映射表并生成 dist/ 产物
107
- node parser/index.js match <changeset.json...> 对设计变更集执行三级匹配(可多文件)
107
+ node parser/index.js match <changeset.json...> 对设计变更集执行三级匹配(可多文件;顶层 mode=restore 时 G8 模式闸强制 oracle.mockup + coverage.registry 齐备)
108
108
  node parser/index.js match <cs.json> --registry <reg.json>
109
109
  匹配 + 登记表覆盖率当场重算(子项级细闸,有缺口 exit 1)
110
110
  node parser/index.js match --batch <dir> 项目级批量匹配:目录下全部变更集 + 跨文件汇总
@@ -241,6 +241,55 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
241
241
  console.warn(` ⚠ schema "${changeset.schema}" 为历史值,建议设计侧升级为 "design.changeset/1.0"`);
242
242
  }
243
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
+
244
293
  // 版本校验(mapping_version 缺失或不匹配 → 拒绝。版本恒定 1.0.0,此校验为破坏性升级保险丝:仅双侧同步升版时才会拦截)
245
294
  const versions = new Set(
246
295
  Object.values(dataset.files).map(d => d.meta?.version).filter(Boolean),
@@ -344,12 +393,18 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
344
393
  }
345
394
 
346
395
  // oracle 验收基线(:设计侧给的效果图 + 待比对点位;项目侧落地后跑 verify 逐点 diff)
347
- checkOracle(changeset, plan, valueDict);
348
- checkCoverage(changeset);
396
+ // mockup 相对路径解析以「变更集同目录」为第一基准(协议约定 oracle 产物与变更集同目录),
397
+ // 其次 cwd / 技能根(历史行为兼容)。
398
+ // G11 oracle 基线闸:restore 模式下返回值非 0 = 阻断(与 G8 同级)
399
+ const oracleGaps = checkOracle(changeset, plan, valueDict, { strict: mode === "restore", filePath: file }) || 0;
400
+ checkCoverage(changeset, { strict: mode === "restore" });
349
401
  // 登记表覆盖率(子项级细闸):以登记表为权威、当场重算 A/C,设计侧自报的 coverage 不参与
350
- const regGaps = checkRegistryCoverage(changeset, plan, opts);
402
+ // strict(mode=restore)同时驱动 G9 登记表身份闸与 G10 dims 兑现闸
403
+ const regGaps = checkRegistryCoverage(changeset, plan, { ...opts, strict: mode === "restore" });
351
404
  if (batchSummary)
352
- batchSummary.registryGaps += regGaps;
405
+ batchSummary.registryGaps += regGaps + oracleGaps;
406
+ else if (regGaps + oracleGaps > 0)
407
+ process.exitCode = 1;
353
408
 
354
409
  // 输出转换计划
355
410
  console.log(`\n=== 三级匹配结果(scene=${changeset.scene},${plan.length} 条)===`);
@@ -681,11 +736,15 @@ function priorityRequirement(strategy) {
681
736
  * 反之声明了却不在 sids 里的点位 → 落地后无法机器验收。
682
737
  * 两者都属「交付不完整」,与颜色红线同级,必须回流设计侧补齐。
683
738
  */
684
- function checkOracle(changeset, plan, valueDict) {
739
+ function checkOracle(changeset, plan, valueDict, opts = {}) {
685
740
  const oracle = changeset.oracle;
686
741
  if (!oracle || typeof oracle !== "object") {
742
+ if (opts.strict) {
743
+ console.error(`\n✖ mode=restore 下 oracle 缺失(G8 模式闸要求,见变更集校验段)——基线三件套不完整,先回设计侧补齐。`);
744
+ return 1; // G11:restore 下基线缺失 = 阻断,不再是提示后继续
745
+ }
687
746
  console.log(`\n提示:变更集未提供 oracle 验收基线(场景还原应给)—— 落地后无法做 computed style 逐点比对,还原度不可证明,建议回流设计侧补齐。`);
688
- return;
747
+ return 0;
689
748
  }
690
749
 
691
750
  const sids = Array.isArray(oracle.sids) ? oracle.sids : [];
@@ -737,16 +796,16 @@ function checkOracle(changeset, plan, valueDict) {
737
796
  );
738
797
  // 实物闭合兜底(G3):带登记表时由 checkRegistryCoverage 做权威实物对账;
739
798
  // 未带登记表时此处直接打开 mockup 数 data-sid,防止「sids 自报全绿、实物基线为空」。
799
+ // mockup 路径解析复用 resolveFirstExisting(变更集同目录 → cwd → 技能根)。
740
800
  if (!changeset.coverage?.registry && mockup) {
741
- const cands = [path.resolve(mockup), path.resolve(ROOT, mockup), path.resolve(process.cwd(), mockup)];
801
+ const mockupPath = resolveFirstExisting(mockup, opts.filePath);
742
802
  let physical = null;
743
- for (const c of cands) {
803
+ if (mockupPath) {
744
804
  try {
745
- const stripped = readFileSync(c, "utf-8").replace(/<!--[\s\S]*?-->/g, "");
805
+ const stripped = readFileSync(mockupPath, "utf-8").replace(/<!--[\s\S]*?-->/g, "");
746
806
  physical = new Set([...stripped.matchAll(/data-sid\s*=\s*["']([^"']+)["']/g)].map(m => m[1]));
747
- break;
748
807
  }
749
- catch { /* 试下一个候选路径 */ }
808
+ catch { /* mockup 不可读,physical 保持 null */ }
750
809
  }
751
810
  if (physical) {
752
811
  const sidNoPhysical = sids.filter(s => !physical.has(s) && !hiddenOnly.has(s));
@@ -765,11 +824,22 @@ function checkOracle(changeset, plan, valueDict) {
765
824
 
766
825
  if (issues.length === 0) {
767
826
  console.log(` ✔ oracle 校验通过:主题一致、覆盖度双向闭合`);
768
- return;
827
+ return 0;
828
+ }
829
+ // G11 oracle 基线硬闸(2026-09-21 机制化):restore 模式下基线问题从「⚠ 软提示」升格为阻断。
830
+ // 历史缺口:主题错配 / 欠声明 / 无基线只打 ⚠ 就继续——基线断了仍绿灯交付,
831
+ // verify 无从做 1:1 证明,「细节没还原到位」在交付时不可见。
832
+ if (opts.strict) {
833
+ console.error(`\n✖ oracle 基线问题(G11 · ${issues.length} 类 → 拒绝执行):`);
834
+ for (const it of issues)
835
+ console.error(` ✘ ${it}`);
836
+ console.error(` 处置:按上列各条回设计侧补齐基线(换本主题效果图 / 补 sids 声明 / 声明 theme_mapping 同构异色复用)后重跑。`);
837
+ return 1;
769
838
  }
770
839
  console.log(`\n⚠ oracle 基线问题(${issues.length} 类):`);
771
840
  for (const it of issues)
772
841
  console.log(` ✘ ${it}`);
842
+ return 0;
773
843
  }
774
844
 
775
845
  /**
@@ -778,10 +848,13 @@ function checkOracle(changeset, plan, valueDict) {
778
848
  * 这里只做结构一致性校验:covered 必须等于 applicable、gaps 必须为空。
779
849
  * 缺 coverage 时无法判断「是否有组内子项被静默丢失」——属交付完整性的信号缺口。
780
850
  */
781
- function checkCoverage(changeset) {
851
+ function checkCoverage(changeset, opts = {}) {
782
852
  const cov = changeset.coverage;
783
853
  if (!cov || typeof cov !== "object") {
784
- console.log(`\n提示:变更集未提供 coverage 覆盖率记账(场景还原应给)—— 无法判断是否有规范子项被静默丢失,完备性不可数,建议回流设计侧补齐。`);
854
+ if (opts.strict)
855
+ console.error(`\n✖ mode=restore 下 coverage 缺失(G8 模式闸要求,见变更集校验段)——186 条登记表口径的完备性无从计数,先回设计侧补齐。`);
856
+ else
857
+ console.log(`\n提示:变更集未提供 coverage 覆盖率记账(场景还原应给)—— 无法判断是否有规范子项被静默丢失,完备性不可数,建议回流设计侧补齐。`);
785
858
  return;
786
859
  }
787
860
  const num = v => (typeof v === "number" && Number.isFinite(v) ? v : null);
@@ -810,7 +883,7 @@ function checkCoverage(changeset) {
810
883
  console.log(` ✘ ${it}`);
811
884
  }
812
885
 
813
- /** 依次尝试候选路径,返回首个存在者(相对路径基准:变更集所在目录 → 当前工作目录 → 技能仓库根) */
886
+ /** 依次尝试候选路径,返回首个存在者(相对路径基准:变更集所在目录 → 当前工作目录 → 技能仓库根 → 兄弟设计技能 project-ui-design 根) */
814
887
  function resolveFirstExisting(raw, filePath) {
815
888
  if (!raw || typeof raw !== "string")
816
889
  return null;
@@ -820,6 +893,10 @@ function resolveFirstExisting(raw, filePath) {
820
893
  if (filePath)
821
894
  bases.push(path.dirname(filePath));
822
895
  bases.push(process.cwd(), ROOT);
896
+ // 兄弟技能根:真实设计侧登记表/效果图沉淀在 project-ui-design(设计侧 page-specs),
897
+ // 跨库引用时允许相对该根解析(两侧技能目录同级部署是标准布局)。
898
+ const sibling = path.resolve(ROOT, "..", "project-ui-design");
899
+ bases.push(sibling);
823
900
  for (const base of bases) {
824
901
  const candidate = path.resolve(base, raw);
825
902
  if (existsSync(candidate))
@@ -862,6 +939,12 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
862
939
  console.error(`\n✖ 登记表文件不存在:${opts.registryArg}`);
863
940
  return 1;
864
941
  }
942
+ if (opts.strict) {
943
+ // G9 前置缺口:mode=restore 却解析不到登记表实物(G8 只验了字符串非空)
944
+ console.error(`\n✖ G9 登记表身份闸:mode=restore 但登记表实物不存在(coverage.registry="${raw}" 解析失败)→ 拒绝执行。`);
945
+ console.error(` 覆盖率分母必须来自「方案 × 场景」的真实登记表,否则壳层/表格细粒度点位从分母消失,欠声明不可数。`);
946
+ return 1;
947
+ }
865
948
  console.log(`\n提示:未提供登记表(--registry 或 coverage.registry)—— 组级台账只防「整组忘光」,组内子项被静默丢失无法计数,建议场景还原带登记表。`);
866
949
  return 0;
867
950
  }
@@ -874,6 +957,23 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
874
957
  console.error(`\n✖ 登记表读取/解析失败:${registryPath} —— ${e.message}`);
875
958
  return 1;
876
959
  }
960
+
961
+ // G9 登记表身份闸(2026-09-21 机制化):restore 模式下,登记表必须属于「本变更集声明的方案 × 场景」,
962
+ // 且必须是有 items 的真登记表。历史缺口:G8 只验 coverage.registry 是非空字符串——
963
+ // 拿 6 条的回归夹具 registry-sample.json、甚至别的方案/场景的登记表都能过 G8,
964
+ // 覆盖率分母(R=186 的壳层+表格子项)被偷换成小表,「面包屑/顶栏图标/表头 nowrap」这类点位
965
+ // 从分母里消失 → 欠声明不可数 → 细节还原缺口一路绿灯。凡「身份对不上的表」视同没有表。
966
+ const scheme = typeof changeset.scheme === "string" ? changeset.scheme : "";
967
+ const scene = typeof changeset.scene === "string" ? changeset.scene : "";
968
+ const regScheme = typeof registry.design_scheme === "string" ? registry.design_scheme : "";
969
+ const regScene = typeof registry.scene === "string" ? registry.scene : "";
970
+ const identityMismatch = (regScheme && scheme && regScheme !== scheme) || (regScene && scene && regScene !== scene);
971
+ if (opts.strict && (identityMismatch || !regScheme || !regScene)) {
972
+ console.error(`\n✖ G9 登记表身份闸:登记表 ${path.basename(registryPath)} 属于 scheme="${regScheme || "?"}"/scene="${regScene || "?"}",与变更集 scheme="${scheme || "?"}"/scene="${scene || "?"}" 不一致(或缺身份字段)→ 拒绝执行。`);
973
+ console.error(` 处置:coverage.registry 改指「${scheme} × ${scene}」方案的登记表(如设计侧 page-specs/styles/<方案>/scenes/<场景>/restore-registry.json)。`);
974
+ return 1;
975
+ }
976
+
877
977
  const items = Array.isArray(registry.items) ? registry.items : [];
878
978
  if (items.length === 0) {
879
979
  console.error(`\n✖ 登记表不含 items:${registryPath}`);
@@ -895,33 +995,76 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
895
995
  const declaredSids = new Set(Array.isArray(changeset.oracle?.sids) ? changeset.oracle.sids : []);
896
996
 
897
997
  // 实物闭合:打开 oracle.mockup 这个 HTML,数真实存在的 data-sid(不只看设计侧自报的 oracle.sids)
998
+ // mockup 路径解析复用 resolveFirstExisting(变更集同目录 → cwd → 技能根)。
898
999
  const mockupRef = changeset.oracle?.mockup;
899
1000
  let physicalSids = null;
900
1001
  let mockupFile = null;
901
- if (typeof mockupRef === "string" && mockupRef.trim()) {
902
- const cands = [path.resolve(mockupRef), path.resolve(ROOT, mockupRef), path.resolve(process.cwd(), mockupRef)];
903
- for (const c of cands) {
904
- try {
905
- const html = readFileSync(c, "utf-8");
906
- // 先剥离 HTML 注释再数 data-sid:注释内的同形串是说明文字、不是点位。
907
- // 不剥离 → 注释可「兜住」一个实际缺失的标注,闸门全绿而基线为空(详见 mockup-extraction-spec.md §7.1)。
908
- const stripped = html.replace(/<!--[\s\S]*?-->/g, "");
909
- physicalSids = new Set([...stripped.matchAll(/data-sid\s*=\s*["']([^"']+)["']/g)].map(m => m[1]));
910
- mockupFile = c;
911
- break;
912
- }
913
- catch { /* 该候选路径不可读,试下一个 */ }
1002
+ const mockupResolved = resolveFirstExisting(typeof mockupRef === "string" ? mockupRef : null, opts.filePath);
1003
+ if (mockupResolved) {
1004
+ try {
1005
+ const html = readFileSync(mockupResolved, "utf-8");
1006
+ // 先剥离 HTML 注释再数 data-sid:注释内的同形串是说明文字、不是点位。
1007
+ // 不剥离 → 注释可「兜住」一个实际缺失的标注,闸门全绿而基线为空(详见 mockup-extraction-spec.md §7.1)。
1008
+ const stripped = html.replace(/<!--[\s\S]*?-->/g, "");
1009
+ physicalSids = new Set([...stripped.matchAll(/data-sid\s*=\s*["']([^"']+)["']/g)].map(m => m[1]));
1010
+ mockupFile = mockupResolved;
914
1011
  }
1012
+ catch { /* mockup 不可读,physicalSids 保持 null */ }
915
1013
  }
916
1014
  // 比对基准:优先效果图实物;实物读不到时退化为自报清单(并在下方告警)
917
1015
  const mockupSids = physicalSids || declaredSids;
918
1016
 
919
1017
  const applicable = items.filter(it => it.always === true || registrySids(it).some(s => mockupSids.has(s)));
920
- const gaps = applicable.filter(it => !registrySids(it).every(s => declared.has(s)));
1018
+
1019
+ // G10 dims 兑现闸(2026-09-21 机制化):「兑现」从「ID 出现」升格为「ID 出现 且 dims 维度全部出现」。
1020
+ // 历史缺口:只数 ID——声明了 admin.content.table.header_cell 却漏掉 nowrap / font_weight /
1021
+ // text_align 等关键维度也算「已兑现」→ 表头竖排(nowrap 丢失)、字重不对(font_weight 丢失)
1022
+ // 这类「点位在、细节丢」的症状一路绿灯。核对口径:dims 中的 form 看 change.form;
1023
+ // 其余维度看 change.style ∪ change.layer 的键;source:none(登记即兑现)、global 伪点位
1024
+ // (全局通道字段名与登记 dims 不同构)、visible:false 隐藏点位不做 dims 机检。
1025
+ const hiddenIds = new Set((changeset.changes || []).filter(c => c && c.visible === false).map(c => c.id));
1026
+ const missingDimsOf = (it, change) => {
1027
+ const dims = it.dims || [];
1028
+ const styleKeys = new Set([...Object.keys(change.style || {}), ...Object.keys(change.layer || {})]);
1029
+ const missing = [];
1030
+ if (dims.includes("form") && change.form === undefined)
1031
+ missing.push("form");
1032
+ for (const d of dims.filter(d => d !== "form")) {
1033
+ if (!styleKeys.has(d))
1034
+ missing.push(d);
1035
+ }
1036
+ return missing;
1037
+ };
1038
+ const dimsGaps = []; // { item, missing }
1039
+ const gaps = applicable.filter(it => {
1040
+ const sids = registrySids(it);
1041
+ if (!sids.every(s => declared.has(s)))
1042
+ return true; // ID 级欠声明(原有缺口口径)
1043
+ if (it.source === "none")
1044
+ return false;
1045
+ const primary = sids.find(s => s !== GLOBAL_PSEUDO_ID);
1046
+ if (!primary)
1047
+ return false; // global 伪点位条目
1048
+ if (hiddenIds.has(primary))
1049
+ return false; // visible:false 点位 dims 豁免(与 oracle 隐藏点位豁免同口径)
1050
+ const change = (changeset.changes || []).find(c => c.id === primary);
1051
+ if (!change)
1052
+ return false; // 经 pending/visible 兑现且无 style 意图条目 → dims 由台账人工闸承载
1053
+ const missing = missingDimsOf(it, change);
1054
+ if (missing.length === 0)
1055
+ return false;
1056
+ dimsGaps.push({ item: it, missing });
1057
+ return false;
1058
+ });
921
1059
 
922
1060
  console.log(`\n=== 登记表覆盖率(子项级细闸 · 当场重算)===`);
923
1061
  console.log(` 登记表:${registryPath}(登记项 R=${items.length})`);
924
- console.log(` 适用 A=${applicable.length} / 已兑现 C=${applicable.length - gaps.length} / 缺口 ${gaps.length}`);
1062
+ console.log(` 适用 A=${applicable.length} / 已兑现 C=${applicable.length - gaps.length} / 缺口 ${gaps.length}${dimsGaps.length > 0 ? ` / dims 缺口 ${dimsGaps.length}(G10)` : ""}`);
1063
+ if (dimsGaps.length > 0) {
1064
+ console.log(` ⚠ dims 兑现缺口 ${dimsGaps.length} 条(ID 已声明、dims 维度未给全):`);
1065
+ for (const { item, missing } of dimsGaps.slice(0, 10))
1066
+ console.log(` ${item.rule} ${registrySids(item).join(" + ")} 缺 dims=[${missing.join(",")}]`);
1067
+ }
925
1068
 
926
1069
  // 实物校验:标注缺口的权威来源是效果图 HTML 里的 data-sid,而不是自报的 oracle.sids
927
1070
  let physicalGap = false;
@@ -953,17 +1096,24 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
953
1096
  physicalGap = true;
954
1097
  }
955
1098
 
956
- if (gaps.length === 0 && !physicalGap) {
957
- console.log(` ✔ 覆盖率通过:C = A,无子项被静默丢失;效果图实物标注闭合`);
1099
+ if (gaps.length === 0 && dimsGaps.length === 0 && !physicalGap) {
1100
+ console.log(` ✔ 覆盖率通过:C = A,无子项被静默丢失;dims 维度全兑现(G10);效果图实物标注闭合`);
958
1101
  return 0;
959
1102
  }
960
1103
 
961
- if (gaps.length === 0 && physicalGap) {
1104
+ if (gaps.length === 0 && dimsGaps.length === 0 && physicalGap) {
962
1105
  console.log(`\n✘ 效果图实物标注不闭合 → exit 1(oracle.sids 与效果图 HTML 的 data-sid 不一致 / 恒存在子项缺标注)`);
963
1106
  console.log(` 处置:给效果图对应元素补 data-sid,或把该登记项降级 source: spec(见 mockup-extraction-spec.md §7)`);
964
1107
  return 1;
965
1108
  }
966
1109
 
1110
+ if (dimsGaps.length > 0 && opts.strict) {
1111
+ console.log(`\n✘ dims 兑现缺口(G10 · ID 已声明但 dims 维度未给全,共 ${dimsGaps.length} 条 → exit 1):`);
1112
+ for (const { item, missing } of dimsGaps)
1113
+ console.log(` ${item.rule} ${registrySids(item).join(" + ")} 缺 dims=[${missing.join(",")}]`);
1114
+ console.log(` 处置:在对应 changes[] 条目的 style/layer 补齐所缺维度取值(值来源见 source 字段);「点位在、细节丢」正是样式细节不还原的机制成因。`);
1115
+ }
1116
+
967
1117
  const byGroup = new Map();
968
1118
  for (const it of gaps) {
969
1119
  const g = it.group || "(未分组)";
@@ -980,7 +1130,8 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
980
1130
  if (!opts.verbose && list.length > shown.length)
981
1131
  console.log(` …(同组其余 ${list.length - shown.length} 条,加 --verbose 展开)`);
982
1132
  }
983
- return gaps.length;
1133
+ // G10 dims 缺口计入返回值:strict 模式下 =「交付不完整」与欠声明同级
1134
+ return gaps.length + (opts.strict ? dimsGaps.length : 0);
984
1135
  }
985
1136
 
986
1137
  /**
@@ -15,7 +15,9 @@
15
15
  * E match 负例2(mapping_version 不符) → exit 1
16
16
  * F match 负例3(禁词阻断) → exit 1
17
17
  * G match 旧协议兼容 → exit 0 且输出含 ⚠ 告警
18
- * H match 整页场景还原样例 → exit 0 且输出含 #245BFF(壳层+内容区全模块 L1)
18
+ * H match 整页场景还原样例(mode=restore)→ exit 0 且输出含 #245BFF 与「G8 模式闸」(壳层+内容区全模块 L1)
19
+ * P match G8 负例(mode=restore 缺 oracle/coverage)→ exit 1 且输出含「G8 模式闸」(伪还原变更集拦截)
20
+ * Q match G8 强化负例(mode=restore 但 mockup/registry 为空壳)→ exit 1 且输出含「G8 模式闸」(有壳无芯伪基线拦截)
19
21
  * I build --dry 产物链自检 → exit 0 且输出含 dry-run
20
22
  * J match 三声明维度 → exit 0 且输出含「结构施工 structure_ops」
21
23
  * K audit 挂载点 / 死通道审计 → exit 1(夹具项目故意存在挂载点缺失 / 死通道)
@@ -98,10 +100,10 @@ const steps = [
98
100
  },
99
101
  {
100
102
  key: "H",
101
- name: "match 整页场景还原样例(壳层+内容区全模块 L1 + 数值字面量透传)",
103
+ name: "match 整页场景还原样例(mode=restore 基线三件套 + 壳层+内容区全模块 L1 + 数值字面量透传)",
102
104
  script: INDEX,
103
105
  args: ["match", path.join(FIXTURES, "changeset-fullpage-restore.json")],
104
- expect: { code: 0, includes: "#245BFF" },
106
+ expect: { code: 0, includes: ["#245BFF", "G8 模式闸"] },
105
107
  },
106
108
  {
107
109
  key: "I",
@@ -152,12 +154,26 @@ const steps = [
152
154
  args: ["registry", path.join(FIXTURES, "registry-bad.json")],
153
155
  expect: { code: 1, includes: "硬缺口" },
154
156
  },
157
+ {
158
+ key: "P",
159
+ name: "match G8 模式闸负例(mode=restore 缺 oracle/coverage = 伪还原变更集 → exit 1)",
160
+ script: INDEX,
161
+ args: ["match", path.join(FIXTURES, "changeset-neg4-g8.json")],
162
+ expect: { code: 1, includes: "G8 模式闸" },
163
+ },
164
+ {
165
+ key: "Q",
166
+ name: "match G8 强化负例(mode=restore 但 oracle.mockup / coverage.registry 为空壳 = 有壳无芯 → exit 1)",
167
+ script: INDEX,
168
+ args: ["match", path.join(FIXTURES, "changeset-neg5-g8-stub.json")],
169
+ expect: { code: 1, includes: "G8 模式闸" },
170
+ },
155
171
  ];
156
172
 
157
173
  const results = [];
158
174
  let failed = 0;
159
175
 
160
- console.log("=== A-O 端到端一键回归 ===\n");
176
+ console.log("=== A-Q 端到端一键回归 ===\n");
161
177
 
162
178
  for (const step of steps) {
163
179
  const { status, stdout, stderr } = spawnSync(
@@ -194,4 +210,4 @@ if (failed > 0) {
194
210
  console.error(`\n✖ ${failed} 步未通过,见上方详情`);
195
211
  process.exit(1);
196
212
  }
197
- console.log("\n✔ A-O 全部通过,mapping/parser 可提交或发版");
213
+ console.log("\n✔ A-Q 全部通过,mapping/parser 可提交或发版");
@@ -62,6 +62,8 @@ parser match(确定性翻译)→ 施工坐标 → AI 按坐标改代码 →
62
62
  "changeset_version": "1.0.0", // 恒填 "1.0.0"
63
63
  "mapping_version": "1.0.0", // 恒填 "1.0.0":版本由双侧 SKILL.md 体系版本统一管理,词典不单独演进。parser 校验口径 = 变更集值 ∈ 词典版本集合(恒通过);仅协议破坏性升级时双侧同步升版
64
64
  "scene": "admin", // 场景前缀,须与 mapping 场景域一致
65
+ "scheme": "default", // 方案标识(mode=restore 时必填):取设计侧 page-specs/styles/registry.json 的方案 id;与 scene 共同定位「方案 × 场景」的清单/登记表
66
+ "mode": "restore", // 交付模式(G8 模式闸):restore=场景还原(scheme + oracle.mockup + coverage.registry 强制必填);design=显式声明的轻量微调;缺省 design
65
67
  "layout_type": "vertical", // 标准布局枚举:vertical / horizontal / mix(字段名固定 layout_type,非 layout)
66
68
  "global": { "brand_primary": "钴蓝-5" }, // 可选:全局令牌通道(整套主题切换优先走此节点;值 = 语义名或临时主题 hex)
67
69
  "changes": [
@@ -190,10 +192,11 @@ node parser/index.js match --batch <changesets-dir>
190
192
 
191
193
  | 闸 | 触发条件 | 判据来源 | 违规输出 |
192
194
  |---|---|---|---|
195
+ | **G8 模式闸** | 变更集顶层 `mode: "restore"`(场景还原)但基线不全:缺 `scheme` / `oracle` / `coverage`,**或 `oracle.mockup` / `coverage.registry` 为空**(有壳无芯的伪基线)——伪还原变更集(只换主题色却以还原之名交付)拒绝执行;`mode` 非法值同样阻断 | 变更集顶层 `scheme` / `mode` / `oracle.mockup` / `coverage.registry` | `G8 模式闸:mode=restore 但缺少 … → 拒绝执行` |
193
196
  | **G1 suppress 收口** | 条目声明某 `form`,词典 `suppress_rules[form]`(或 forms 描述「suppress 必含 x」内联标记)要求收口的出厂互斥通道未全部出现在该条目 `suppress[]` | 词典条目 `suppress_rules` | `suppress 缺口:…必须收口出厂互斥通道 [x],缺 [x]` |
194
197
  | **G5 几何语境** | 词典 preconditions 含「几何语境随值迁移」标记 ∧ `style` 含 padding/margin 族几何字段落宿主 ∧ 未声明 `suppress_inner` | 词典条目 preconditions 标记句 | `suppress_inner 缺口:…两层之和 ≠ 设计值` |
195
198
 
196
- 两闸与禁词校验同级阻断(`exit 1`)——「新旧形态叠加」「两层 padding 相加」都是值全对、观感全错的历史事故源(双竖条、28px 列挤压),不允许靠观感兜底。
199
+ 三闸与禁词校验同级阻断(`exit 1`)——「新旧形态叠加」「两层 padding 相加」「伪还原只换主题」都是历史事故源(双竖条、28px 列挤压、「还原后布局细节零变化」),不允许靠观感兜底。G8 判据一句话:**「把系统 / 页面改成 X」默认是还原(restore),必须带齐 `scheme` + `oracle.mockup` + `coverage.registry`;只有「显式声明只改主题色、其它不动」才用 `design`**(2026-09-20 事故机制化:还原项目后重跑,无登记表无基线的轻量变更集一路绿灯,落地后「只变了主题、布局细节零变化」;二轮强化再堵住 `oracle`/`coverage` 只给空壳、无 `mockup`/`registry` 芯的写法)。**降级不许静默发生**:变更集 `notes` 与交付说明都须显式写出本次 `mode` 与范围。
197
200
 
198
201
  **验收基线输出**:转换计划末尾另打印 `=== 验收基线 oracle ===`(效果图路径 + 待比对点位)与 `=== 覆盖率 coverage ===`(登记项 R / 适用 A / 已兑现 C)。有 `oracle` → 落地后必须跑第 6 步 `verify`;缺 `oracle` / `coverage` → 输出提示,把「补基线 / 补登记覆盖率」回流设计侧(场景还原应给)。
199
202
  **覆盖率校验**:`coverage.covered` 必须等于 `coverage.applicable`、`gaps` 为空,否则打印缺口清单——`C < A` 即**欠声明**(有规范子项未落进 `changes[]`/`visible`/`pending`),属设计侧交付不完整,必须回流,不得在项目侧"猜着补"。
@@ -496,7 +499,7 @@ html .navbar .navbar-breadcrumb-wrap {
496
499
  ## 端到端回归(改 parser/mapping 后必跑)
497
500
 
498
501
  ```bash
499
- node parser/regression.js # 一键跑完 A-O 十五步(自动断言退出码与关键输出,任一失败 exit 1 并回显详情)
502
+ node parser/regression.js # 一键跑完 A-Q 十七步(自动断言退出码与关键输出,任一失败 exit 1 并回显详情)
500
503
  npm run regression # 等价写法
501
504
  ```
502
505
 
@@ -518,9 +521,11 @@ node parser/index.js verify --oracle parser/fixtures/verify/oracle.json --actual
518
521
  node parser/index.js match --registry parser/fixtures/registry-sample.json parser/fixtures/changeset-registry-gap.json # M. 登记表覆盖率细闸:夹具欠声明 2 条恒存在子项 → exit 1
519
522
  node parser/index.js registry parser/fixtures/registry-sample.json # N. 登记表通道核验:点位全部命中 → exit 0;输出含「登记表通道核验」
520
523
  node parser/index.js registry parser/fixtures/registry-bad.json # O. 登记表硬缺口:引用不存在且未标 pending 的点位 → exit 1
524
+ node parser/index.js match parser/fixtures/changeset-neg4-g8.json # P. G8 模式闸负例:mode=restore 缺 oracle/coverage(伪还原变更集)→ exit 1
525
+ node parser/index.js match parser/fixtures/changeset-neg5-g8-stub.json # Q. G8 强化负例:mode=restore 但 mockup/registry 空壳(有壳无芯伪基线)→ exit 1
521
526
  ```
522
527
 
523
- 通过标准:regression.js 输出 `A✔ B✔ … O✔` 且 exit 0;分步跑时各步符合注释预期。夹具含义见各文件 `$comment`。
528
+ 通过标准:regression.js 输出 `A✔ B✔ … Q✔` 且 exit 0;分步跑时各步符合注释预期。夹具含义见各文件 `$comment`。
524
529
 
525
530
  ## 配套工具(作者/维护侧)
526
531