@qilitt-mickey/vue3-temp-skill 1.1.35 → 1.1.36

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.
Files changed (56) hide show
  1. package/README.md +6 -36
  2. package/SKILL.md +11 -19
  3. package/bin/cli.js +19 -65
  4. package/dist/mapping-type.d.ts +33 -0
  5. package/dist/theme-vars.js +61 -0
  6. package/dist/theme-vars.ts +111 -0
  7. package/dist/theme.css +72 -0
  8. package/mapping/basic/color.json +34 -0
  9. package/mapping/basic/radius.json +17 -0
  10. package/mapping/basic/semantic-values.json +72 -0
  11. package/mapping/basic/spacing.json +21 -0
  12. package/mapping/basic/typography.json +21 -0
  13. package/mapping/component/button.json +59 -0
  14. package/mapping/component/input.json +39 -0
  15. package/mapping/component/overlay.json +63 -0
  16. package/mapping/component/table.json +42 -0
  17. package/mapping/layout/breakpoint.json +20 -0
  18. package/mapping/layout/shell-geometry.json +26 -0
  19. package/mapping/scenes/admin/content.json +112 -0
  20. package/mapping/scenes/admin/layout.json +331 -0
  21. package/package.json +8 -4
  22. package/parser/fixtures/changeset-legacy.json +11 -0
  23. package/parser/fixtures/changeset-neg1.json +17 -0
  24. package/parser/fixtures/changeset-neg2.json +10 -0
  25. package/parser/fixtures/changeset-neg3.json +22 -0
  26. package/parser/fixtures/changeset-pos.json +43 -0
  27. package/parser/generator.js +178 -0
  28. package/parser/index.js +553 -0
  29. package/parser/loader.js +66 -0
  30. package/parser/override.js +119 -0
  31. package/parser/regression.js +137 -0
  32. package/parser/validator.js +137 -0
  33. package/parser/watch.js +103 -0
  34. package/references/code-quality.md +24 -23
  35. package/references/core-kernel.md +2 -4
  36. package/references/crud-pages.md +18 -290
  37. package/references/data-mapping.md +229 -229
  38. package/references/data-screen.md +102 -102
  39. package/references/design-apply.md +177 -0
  40. package/references/flowchart-g6.md +260 -260
  41. package/references/form-advanced.md +1 -1
  42. package/references/graph-relation.md +1 -1
  43. package/references/layout-theme.md +513 -520
  44. package/references/qrcode-barcode.md +117 -117
  45. package/references/workflow-bpmn.md +214 -214
  46. package/scripts/check.mjs +84 -90
  47. package/scripts/cli.mjs +115 -120
  48. package/scripts/match.py +134 -134
  49. package/scripts/modules.json +109 -103
  50. package/scripts/validate.mjs +106 -106
  51. package/references/design-handoff.md +0 -767
  52. package/references/design-system.md +0 -142
  53. package/references/design-verify.md +0 -469
  54. package/scripts/check.cmd +0 -9
  55. package/scripts/field-schema-audit.mjs +0 -246
  56. package/scripts/handoff.mjs +0 -474
@@ -0,0 +1,553 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * index.js — 映射体系解析执行入口
5
+ *
6
+ * 职责(V3.0 §3.4.2 / §4.1):
7
+ * 1. 读取:递归合并 mapping/ 全目录 → 完整映射数据集
8
+ * 2. 校验:调用 validator 全量校验,失败 exit 1
9
+ * 3. 编译:调用 generator 生成 dist/ 产物
10
+ * 4. 匹配:对设计变更集(design-changeset/v1)执行三级匹配,输出逐 ID 转换计划
11
+ * - 一级匹配:ID 完整命中 mappings
12
+ * - 二级匹配:父模块(去掉最后一段)命中
13
+ * - 三级匹配:全新模块 → 待补充映射清单
14
+ *
15
+ * CLI:
16
+ * node parser/index.js build [--dry] # 校验 + 生成产物
17
+ * node parser/index.js match <changeset.json...> # 三级匹配(可多文件)
18
+ * node parser/index.js match --batch <dir> # 项目级批量匹配目录下全部变更集
19
+ * node parser/index.js list [--scene admin] [--verbose] # 列出全部语义 ID(--verbose 含字段与形态)
20
+ * node parser/index.js query <semantic.id> # 查询单个 ID 映射详情
21
+ * node parser/watch.js # 开发态监听 mapping/ 自动重编译(§3.4.3)
22
+ */
23
+
24
+ import { readFileSync, statSync, readdirSync } from "node:fs";
25
+ import path from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+ import { buildMappingDataset } from "./loader.js";
28
+ import { validateMappingDataset, buildMergedIndex } from "./validator.js";
29
+ import { generateAll } from "./generator.js";
30
+
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+ const ROOT = path.resolve(__dirname, "..");
33
+
34
+ /** 定性值(非色彩类语义描述,如 中等/全圆角/宽松):不参与译表,AI 按字段语义施工,不告警。
35
+ * 注意:模块级常量必须在 main() 调用前声明(TDZ),勿下移到文件底部。 */
36
+ const QUALITATIVE_RE = /^(中等|加粗|常规|细|粗|小圆角|全圆角|直角|宽松|紧凑|标准|大|中|小)$/;
37
+
38
+ main();
39
+
40
+ function main() {
41
+ const [cmd, ...args] = process.argv.slice(2);
42
+
43
+ // 每个命令先构建 + 校验(保证在任何操作前映射数据可信)
44
+ const dataset = buildMappingDataset();
45
+ const { errors, warnings } = validateMappingDataset(dataset);
46
+ if (errors.length > 0) {
47
+ console.error(`✖ 映射表校验失败(${errors.length} 项):`);
48
+ for (const e of errors)
49
+ console.error(` ✘ ${e}`);
50
+ process.exit(1);
51
+ }
52
+
53
+ switch (cmd) {
54
+ case "build":
55
+ return cmdBuild(args, warnings);
56
+ case "match":
57
+ return cmdMatch(args, dataset);
58
+ case "list":
59
+ return cmdList(args, dataset);
60
+ case "query":
61
+ return cmdQuery(args, dataset);
62
+ default:
63
+ printHelp();
64
+ }
65
+ }
66
+
67
+ function printHelp() {
68
+ console.log(`vue3-temp-skill mapping parser
69
+
70
+ 用法:
71
+ node parser/index.js build [--dry] 校验映射表并生成 dist/ 产物
72
+ node parser/index.js match <changeset.json...> 对设计变更集执行三级匹配(可多文件)
73
+ node parser/index.js match --batch <dir> 项目级批量匹配:目录下全部变更集 + 跨文件汇总
74
+ node parser/index.js list [--scene admin] [--verbose] 列出全部语义 ID(--verbose 含字段与形态,供下发词汇约束)
75
+ node parser/index.js query <semantic.id> 查询单个 ID 的映射详情
76
+ node parser/watch.js 开发态监听 mapping/ 变更自动重编译
77
+ node parser/override.js <overrides.json> 业务级映射覆写调试(API 见 parser/override.js)`);
78
+ }
79
+
80
+ // ---- build ----
81
+ function cmdBuild(args, warnings) {
82
+ const dry = args.includes("--dry");
83
+ for (const w of warnings)
84
+ console.warn(` ⚠ ${w}`);
85
+ const { written, tokenCount } = generateAll(path.join(ROOT, "dist"), dry);
86
+ console.log(`✔ 映射表校验通过(token ${tokenCount} 项)`);
87
+ if (dry)
88
+ console.log(`[dry-run] 将生成:${written.join("、")}`);
89
+ else
90
+ console.log(`✔ 产物已生成 → dist/:${written.join("、")}`);
91
+ }
92
+
93
+ // ---- match:三级匹配 ----
94
+ function cmdMatch(args, dataset) {
95
+ const files = args.filter(a => !a.startsWith("--"));
96
+ if (files.length === 0) {
97
+ console.error("✖ 缺少变更集文件参数:node parser/index.js match <changeset.json...|--batch <dir>>");
98
+ process.exit(1);
99
+ }
100
+
101
+ // --batch 目录模式:批量读取目录下全部变更集(V3.1 设计侧项目级模式产出
102
+ // 「一份全局变更集 + 各页面变更集」,项目侧需一次性匹配)
103
+ let targets = [];
104
+ if (args.includes("--batch")) {
105
+ const dir = files[0];
106
+ const stat = statSync(path.resolve(dir));
107
+ if (!stat.isDirectory()) {
108
+ console.error(`✖ --batch 参数应为目录:${dir}`);
109
+ process.exit(1);
110
+ }
111
+ targets = readdirSync(path.resolve(dir))
112
+ .filter(f => f.endsWith(".json") && !f.startsWith("_"))
113
+ .sort()
114
+ .map(f => path.join(path.resolve(dir), f));
115
+ if (targets.length === 0) {
116
+ console.error(`✖ 目录内无变更集 JSON:${dir}`);
117
+ process.exit(1);
118
+ }
119
+ console.log(`── 批量模式:${targets.length} 份变更集(${dir})──`);
120
+ }
121
+ else {
122
+ targets = files.map(f => path.resolve(f));
123
+ }
124
+
125
+ // 跨文件汇总(项目级模式对账用)
126
+ const batchSummary = { files: 0, l1: 0, l2: 0, l3: 0, pendingIds: [], fieldPending: [], invalid: [] };
127
+
128
+ for (const target of targets) {
129
+ batchSummary.files++;
130
+ if (targets.length > 1)
131
+ console.log(`\n════ 变更集 ${batchSummary.files}/${targets.length}:${path.basename(target)} ════`);
132
+ matchOne(target, dataset, batchSummary);
133
+ }
134
+
135
+ // 任一文件被拒(schema/版本/禁词校验失败)→ 进程退出码 1(v1.0 修正:单文件模式此前误返回 0)
136
+ if (batchSummary.invalid.length > 0)
137
+ process.exitCode = 1;
138
+
139
+ // 项目级汇总输出
140
+ if (targets.length > 1) {
141
+ console.log(`\n════════ 项目级汇总(${targets.length} 份变更集)════════`);
142
+ console.log(`L1=${batchSummary.l1}(100%) L2=${batchSummary.l2}(85-90%) L3=${batchSummary.l3}(70-80%,补映射后 100%)`);
143
+ if (batchSummary.invalid.length > 0) {
144
+ console.log(`✖ 校验失败文件:${batchSummary.invalid.join("、")}`);
145
+ }
146
+ if (batchSummary.pendingIds.length > 0) {
147
+ const uniq = [...new Set(batchSummary.pendingIds)];
148
+ console.log(`待补充映射 ID(去重 ${uniq.length}):`);
149
+ for (const id of uniq)
150
+ console.log(` - ${id}`);
151
+ }
152
+ if (batchSummary.fieldPending.length > 0) {
153
+ console.log(`字段级待补充:`);
154
+ for (const f of batchSummary.fieldPending)
155
+ console.log(` - ${f.id}: ${f.fields.join("、")}`);
156
+ }
157
+ }
158
+ }
159
+
160
+ /** 单份变更集匹配(cmdMatch 内部复用) */
161
+ function matchOne(file, dataset, batchSummary) {
162
+ let changeset;
163
+ try {
164
+ changeset = JSON.parse(readFileSync(file, "utf-8"));
165
+ }
166
+ catch (e) {
167
+ console.error(`✖ 变更集读取/解析失败:${e.message}`);
168
+ batchSummary?.invalid.push(path.basename(file));
169
+ if (!batchSummary)
170
+ process.exit(1);
171
+ return;
172
+ }
173
+
174
+ // 协议校验(canonical:design.changeset/1.0,见 project-ui-design references/changeset-schema.md v1.0;
175
+ // 兼容历史值 design-changeset/v1,仅告警不阻断)
176
+ const SUPPORTED_SCHEMAS = ["design.changeset/1.0", "design-changeset/v1"];
177
+ if (!SUPPORTED_SCHEMAS.includes(changeset.schema)) {
178
+ console.error(`✖ schema 应为 "design.changeset/1.0",实际 "${changeset.schema ?? "缺失"}"`);
179
+ batchSummary?.invalid.push(path.basename(file));
180
+ if (!batchSummary)
181
+ process.exit(1);
182
+ return;
183
+ }
184
+ if (changeset.schema === "design-changeset/v1") {
185
+ console.warn(` ⚠ schema "${changeset.schema}" 为历史值,建议设计侧升级为 "design.changeset/1.0"`);
186
+ }
187
+
188
+ // 版本校验(v1.0 硬校验:mapping_version 缺失或不匹配 → 拒绝,与双侧文档承诺一致)
189
+ const versions = new Set(
190
+ Object.values(dataset.files).map(d => d.meta?.version).filter(Boolean),
191
+ );
192
+ const required = changeset.mapping_version;
193
+ if (!required) {
194
+ console.error(`✖ 缺少 mapping_version(v1.0 协议必填;获取方式:parser list 输出头部的映射集版本)`);
195
+ batchSummary?.invalid.push(path.basename(file));
196
+ if (!batchSummary)
197
+ process.exit(1);
198
+ return;
199
+ }
200
+ if (!versions.has(required)) {
201
+ console.error(`✖ 变更集依赖映射表版本 ${required},当前映射集版本为 ${[...versions].join("/")},不兼容 → 拒绝执行`);
202
+ batchSummary?.invalid.push(path.basename(file));
203
+ if (!batchSummary)
204
+ process.exit(1);
205
+ return;
206
+ }
207
+
208
+ // 禁词校验(v1.0:双侧文档承诺「协议违规 = parser 阻断」,此处实现)
209
+ const banned = checkBannedWords(changeset);
210
+ if (banned.length > 0) {
211
+ console.error(`✖ 协议违规(禁词出现即阻断,共 ${banned.length} 处):`);
212
+ for (const b of banned)
213
+ console.error(` ✘ ${b}`);
214
+ batchSummary?.invalid.push(path.basename(file));
215
+ if (!batchSummary)
216
+ process.exit(1);
217
+ return;
218
+ }
219
+
220
+ const index = buildMergedIndex(dataset);
221
+ const valueDict = buildSemanticValueIndex(dataset);
222
+ const plan = [];
223
+ const pending = [];
224
+ const fieldPending = []; // 字段级待补充:ID 命中但 style 字段两层映射均未命中
225
+
226
+ // global 令牌翻译(design-apply 第 3 步:走 basic/ 词典,主色另需 setEpThemeColor 链路)
227
+ if (changeset.global && Object.keys(changeset.global).length > 0) {
228
+ const tokenDict = buildTokenIndex(dataset);
229
+ console.log(`\n=== global 令牌翻译(basic/ 词典 + 语义值译表)===`);
230
+ const gPending = [];
231
+ for (const [field, value] of Object.entries(changeset.global)) {
232
+ const def = tokenDict[field];
233
+ const resolved = resolveSemanticValue(value, valueDict);
234
+ if (def)
235
+ console.log(` . ${field}: ${value} → 令牌 ${def.token}${resolved ? `(精确值 ${resolved})` : ""}${def.hook ? `(钩子 ${def.hook})` : ""} @ ${def.file}`);
236
+ else
237
+ gPending.push(field);
238
+ if (!resolved && def && !QUALITATIVE_RE.test(String(value).trim()) && !/^(#|rgb|hsl)/.test(String(value).trim()))
239
+ console.log(` ⚠ 值 "${value}" 未命中语义值译表且非 hex,施工前需确认精确值`);
240
+ }
241
+ if (gPending.length > 0)
242
+ console.log(` ⚠ 未匹配令牌(跳过):${gPending.join("、")}`);
243
+ }
244
+
245
+ // layout_type 枚举校验(shell-geometry layout_modes)
246
+ // 兼容读取:设计侧 v0.2 曾误用顶层 `layout` 键——读出来并告警,防止布局意图静默丢失
247
+ let layoutValue = changeset.layout_type;
248
+ if (!layoutValue && changeset.layout !== undefined) {
249
+ layoutValue = changeset.layout;
250
+ console.warn(` ⚠ 顶层字段名 "layout" 已废弃(v1.0 协议为 "layout_type"),本次按值 "${changeset.layout}" 兼容处理;请上游更正字段名`);
251
+ }
252
+ if (layoutValue) {
253
+ const layoutModes = collectLayoutModes(dataset);
254
+ if (layoutModes.size > 0 && !layoutModes.has(layoutValue)) {
255
+ console.warn(` ⚠ layout_type "${layoutValue}" 不在标准枚举 [${[...layoutModes].join("/")}] 内 → 按变体/定制布局流程处理(design-apply §4.3)`);
256
+ }
257
+ }
258
+
259
+ for (const change of changeset.changes || []) {
260
+ const id = change.id;
261
+ // 一级:完整命中
262
+ if (index[id]) {
263
+ plan.push({ level: 1, id, entry: index[id], change });
264
+ continue;
265
+ }
266
+ // 二级:父模块命中
267
+ const parentId = id.split(".").slice(0, -1).join(".");
268
+ if (parentId && index[parentId]) {
269
+ plan.push({ level: 2, id, parent: parentId, entry: index[parentId], change });
270
+ continue;
271
+ }
272
+ // 三级:全新模块
273
+ plan.push({ level: 3, id, change });
274
+ pending.push(id);
275
+ }
276
+
277
+ // pending[] 解析(v1.0:设计侧新 ID 通道,function/parent 供 L3 施工与临时映射生成)
278
+ if (Array.isArray(changeset.pending) && changeset.pending.length > 0) {
279
+ console.log(`\n=== pending 新增 ID(${changeset.pending.length} 条,归入 L3 施工,携带功能描述)===`);
280
+ for (const p of changeset.pending) {
281
+ if (!p || !p.proposed_id)
282
+ continue;
283
+ const hit = pending.includes(p.proposed_id);
284
+ console.log(` - ${p.proposed_id}${hit ? "" : "(注意:未出现在 changes[] 中,本次不落地)"}`);
285
+ console.log(` 功能:${p.function || "(未提供)"};父模块:${p.parent || "(未提供)"}`);
286
+ }
287
+ }
288
+
289
+ // 输出转换计划
290
+ console.log(`\n=== 三级匹配结果(scene=${changeset.scene},${plan.length} 条)===`);
291
+ for (const p of plan) {
292
+ if (p.level === 1) {
293
+ console.log(`\n[L1 完整命中] ${p.id}`);
294
+ checkFormEnum(p.entry, p.change);
295
+ printStyleTranslation(p.entry, p.change, p.id, fieldPending, valueDict);
296
+ }
297
+ else if (p.level === 2) {
298
+ console.log(`\n[L2 父模块命中] ${p.id} → 继承 ${p.parent}(${p.entry.selector ?? p.entry.component ?? "?"})`);
299
+ console.log(` 结构:定位父容器按相对位置增量插入;样式:复用父模块映射 + 全局组件映射`);
300
+ printStyleTranslation(p.entry, p.change, `${p.id}(继承 ${p.parent})`, fieldPending, valueDict);
301
+ }
302
+ else {
303
+ console.log(`\n[L3 全新模块] ${p.id} → 按场景规范生成默认结构,复用全局原子层映射`);
304
+ }
305
+ }
306
+
307
+ if (pending.length > 0) {
308
+ console.log(`\n=== 待补充映射清单(${pending.length})===`);
309
+ for (const id of pending)
310
+ console.log(` - ${id}`);
311
+ }
312
+
313
+ if (fieldPending.length > 0) {
314
+ console.log(`\n=== 字段级待补充(ID 命中但 style 字段两层均未命中,跳过不猜)===`);
315
+ for (const f of fieldPending)
316
+ console.log(` - ${f.id}: ${f.fields.join("、")}`);
317
+ }
318
+
319
+ const l1 = plan.filter(p => p.level === 1).length;
320
+ const l2 = plan.filter(p => p.level === 2).length;
321
+ const l3 = plan.filter(p => p.level === 3).length;
322
+ console.log(`\n汇总:L1=${l1}(还原度 100%) L2=${l2}(85-90%) L3=${l3}(70-80%,补映射后可达 100%)`);
323
+
324
+ // 汇入跨文件统计(项目级批量模式)
325
+ if (batchSummary) {
326
+ batchSummary.l1 += l1;
327
+ batchSummary.l2 += l2;
328
+ batchSummary.l3 += l3;
329
+ batchSummary.pendingIds.push(...pending);
330
+ batchSummary.fieldPending.push(...fieldPending);
331
+ }
332
+ }
333
+
334
+ /** 禁词校验(v1.0 守门员):DOM/CSS/技术栈词与 hex 位置违规 → 违规描述数组(空 = 通过)
335
+ * 口径与 project-ui-design changeset-schema.md §禁词校验 一致:
336
+ * - hex 仅允许出现在 changes[].style 值与 notes 中
337
+ * - id/position/form/visible 等字段出现 hex、CSS 变量名(--el-* / --vts-* / --*)、
338
+ * 选择器写法(.xxx / #xxx 开头)、伪类(::v-deep / :deep())即阻断
339
+ * - 语义 ID 命名合法性(小写下划线 2~4 段) */
340
+ function checkBannedWords(changeset) {
341
+ const violations = [];
342
+ const HEX_RE = /#[0-9a-fA-F]{3,8}\b/;
343
+ const CSSVAR_RE = /--[a-z][a-z0-9-]*/i;
344
+ const DEEP_RE = /::?(v-)?deep\(/;
345
+ const SEL_RE = /^[.#]|[\s]+[.][a-z]|[\s]+#[a-z0-9]/i;
346
+ const TECH_WORDS = [":deep", "::v-deep", "!important", "el-button", "el-menu", "el-input", "element-plus", "querySelector"];
347
+ const ID_RE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$/;
348
+
349
+ const scan = (label, value) => {
350
+ const s = String(value);
351
+ if (HEX_RE.test(s))
352
+ violations.push(`${label}:出现 hex 色值 "${s}"(hex 仅允许在 style 值与 notes)`);
353
+ if (CSSVAR_RE.test(s))
354
+ violations.push(`${label}:出现 CSS 变量名 "${s}"`);
355
+ if (DEEP_RE.test(s))
356
+ violations.push(`${label}:出现穿透写法 "${s}"`);
357
+ if (SEL_RE.test(s))
358
+ violations.push(`${label}:疑似选择器写法 "${s}"`);
359
+ for (const w of TECH_WORDS) {
360
+ if (s.toLowerCase().includes(w.toLowerCase()))
361
+ violations.push(`${label}:出现技术栈词 "${w}"`);
362
+ }
363
+ };
364
+
365
+ for (const [i, c] of (changeset.changes || []).entries()) {
366
+ const label = `changes[${i}]`;
367
+ if (!c.id || !ID_RE.test(c.id))
368
+ violations.push(`${label}.id "${c.id}" 不符合语义 ID 命名规范(小写下划线、2~4 段点分层级)`);
369
+ if (c.position !== undefined && c.position !== null)
370
+ scan(`${label}.position`, c.position);
371
+ if (c.form !== undefined && c.form !== null)
372
+ scan(`${label}.form`, c.form);
373
+ if (c.id)
374
+ scan(`${label}.id`, c.id);
375
+ }
376
+ return violations;
377
+ }
378
+
379
+ function extractForms(e) {
380
+ if (!e.forms)
381
+ return [];
382
+ if (Array.isArray(e.forms))
383
+ return e.forms.map(f => (typeof f === "string" ? f : f[0])).filter(Boolean);
384
+ // 对象形态:{ chrome: "说明", card: "说明" } 取键;{ mode: [a,b,c] } 取数组值
385
+ const out = [];
386
+ for (const v of Object.values(e.forms)) {
387
+ if (Array.isArray(v))
388
+ out.push(...v.filter(x => typeof x === "string"));
389
+ else if (typeof v === "string" && /^[a-z][a-z0-9_]*$/.test(v))
390
+ out.push(v);
391
+ }
392
+ return out.length > 0 ? out : Object.keys(e.forms);
393
+ }
394
+
395
+ /** 全局令牌索引:basic/ + layout/ 词典摊平为 { abstractField: {token, hook, file} } */
396
+ function buildTokenIndex(dataset) {
397
+ const dict = {};
398
+ for (const [file, data] of Object.entries(dataset.files)) {
399
+ if (!data.tokens)
400
+ continue;
401
+ for (const [field, def] of Object.entries(data.tokens)) {
402
+ if (field.startsWith("$") || typeof def !== "object")
403
+ continue;
404
+ dict[field] = {
405
+ token: Array.isArray(def.token) ? def.token.join(" → ") : (def.token || "(value token)"),
406
+ hook: def.hook || null,
407
+ file: `mapping/${file}`,
408
+ };
409
+ }
410
+ }
411
+ return dict;
412
+ }
413
+
414
+ /** 标准布局枚举集合:取 layout 域文件的 layout_modes 键 */
415
+ function collectLayoutModes(dataset) {
416
+ const modes = new Set();
417
+ for (const data of Object.values(dataset.files)) {
418
+ if (data.layout_modes) {
419
+ for (const k of Object.keys(data.layout_modes)) {
420
+ if (!k.startsWith("$"))
421
+ modes.add(k);
422
+ }
423
+ }
424
+ }
425
+ return modes;
426
+ }
427
+
428
+ /** form 枚举校验:条目定义了 forms 时,form 值必须在其枚举内(守门员职责,§4.5.2) */
429
+ function checkFormEnum(entry, change) {
430
+ if (!change.form || !entry.forms)
431
+ return;
432
+ const formKeys = extractForms(entry);
433
+ if (formKeys.length > 0 && !formKeys.includes(change.form)) {
434
+ console.log(` ✘ form "${change.form}" 不在条目枚举 [${formKeys.join("/")}] 内 → 报告不猜(design-apply §第2步)`);
435
+ }
436
+ }
437
+
438
+ /** 两层样式转换:css_vars 优先 → custom_style_map 兜底 → 未匹配记待补充 */
439
+ function printStyleTranslation(entry, change, idLabel, fieldPending, valueDict) {
440
+ const styles = change.style || {};
441
+ if (Object.keys(styles).length === 0)
442
+ return;
443
+ const lines = [];
444
+ const unmatched = [];
445
+ for (const [field, value] of Object.entries(styles)) {
446
+ const resolved = resolveSemanticValue(value, valueDict);
447
+ const valueLabel = resolved ? `${value} → ${resolved}` : value;
448
+ if (entry.css_vars && entry.css_vars[field]) {
449
+ lines.push(` . ${field}: ${valueLabel} → var 覆盖 ${entry.css_vars[field]}(@ ${entry.style_scope})`);
450
+ }
451
+ else if (entry.custom_style_map && entry.custom_style_map[field]) {
452
+ lines.push(` . ${field}: ${valueLabel} → 自定义 ${entry.custom_style_map[field]}(@ ${entry.style_scope} + :deep() 隔离)`);
453
+ }
454
+ else {
455
+ unmatched.push(field);
456
+ continue;
457
+ }
458
+ // 字段命中但值未消歧:非 hex、非定性值、不在译表 → 提示(不阻断,AI 施工前须确认)
459
+ if (!resolved && !QUALITATIVE_RE.test(String(value).trim()) && !/^(#|rgb|hsl|var\(|calc\()/.test(String(value).trim()))
460
+ lines.push(` ⚠ 值 "${value}" 未命中语义值译表且非 hex,施工前需确认精确值`);
461
+ }
462
+ for (const l of lines)
463
+ console.log(l);
464
+ if (unmatched.length > 0) {
465
+ console.log(` ⚠ 未匹配字段(跳过):${unmatched.join("、")}`);
466
+ if (fieldPending)
467
+ fieldPending.push({ id: idLabel || change.id, fields: unmatched });
468
+ }
469
+ }
470
+
471
+ /** 语义值译表索引:mapping 中 semantic-values.json 摊平为 { 语义名(中/英): 精确值 } */
472
+ function buildSemanticValueIndex(dataset) {
473
+ const dict = {};
474
+ for (const data of Object.values(dataset.files)) {
475
+ if (!data.themes)
476
+ continue;
477
+ for (const theme of Object.values(data.themes)) {
478
+ if (theme && theme.values && typeof theme.values === "object" && !theme.values.$comment) {
479
+ const label = theme.label || "";
480
+ for (const [name, hex] of Object.entries(theme.values)) {
481
+ dict[name] = hex;
482
+ // 英文别名:钴蓝-5 ↔ cobalt-5(主题 label 的拼音对应 themes 键名)
483
+ }
484
+ }
485
+ }
486
+ // 英文拼写别名:theme 键名(cobalt/emerald/...)+ 级别号
487
+ for (const [key, theme] of Object.entries(data.themes)) {
488
+ if (key.startsWith("$") || !theme || !theme.values)
489
+ continue;
490
+ for (let i = 1; i <= 10; i++)
491
+ dict[`${key}-${i}`] = theme.values[`${theme.label}-${i}`] || "";
492
+ dict[`${key}-success`] = theme.values["成功色"] || "";
493
+ dict[`${key}-error`] = theme.values["错误色"] || "";
494
+ dict[`${key}-warning`] = theme.values["警告色"] || "";
495
+ dict[`${key}-info`] = theme.values["信息色"] || "";
496
+ dict[`${key}-link`] = theme.values["链接色"] || "";
497
+ }
498
+ }
499
+ // 清空串
500
+ for (const k of Object.keys(dict))
501
+ if (!dict[k])
502
+ delete dict[k];
503
+ return dict;
504
+ }
505
+
506
+ /** 语义值解析:hex 透传 → 中文名查表 → 英文名查表 → null(未命中) */
507
+ function resolveSemanticValue(value, valueDict) {
508
+ const v = String(value).trim();
509
+ if (/^#([0-9a-fA-F]{3,8})$/.test(v) || /^(rgb|hsl)a?\(/i.test(v))
510
+ return v;
511
+ if (valueDict && valueDict[v])
512
+ return valueDict[v];
513
+ return null;
514
+ }
515
+
516
+ // ---- list ----
517
+ function cmdList(args, dataset) {
518
+ const sceneIdx = args.indexOf("--scene");
519
+ const scene = sceneIdx > -1 ? args[sceneIdx + 1] : null;
520
+ const verbose = args.includes("--verbose") || args.includes("-v");
521
+ const index = buildMergedIndex(dataset);
522
+ const ids = Object.keys(index).sort();
523
+ const filtered = scene ? ids.filter(id => id.startsWith(`${scene}.`) || !/^(admin|portal|dashboard)\./.test(id)) : ids;
524
+ // v1.0:头部输出映射集版本(供设计侧填 mapping_version,词汇约束下发时随清单同步)
525
+ const versions = [...new Set(Object.values(dataset.files).map(d => d.meta?.version).filter(Boolean))];
526
+ console.log(`映射集版本:${versions.join(" / ")}(变更集 mapping_version 应填其中之一)`);
527
+ console.log(`共 ${filtered.length} 条语义 ID${scene ? `(scene=${scene} + 全局)` : ""}:`);
528
+ for (const id of filtered) {
529
+ const e = index[id];
530
+ console.log(` ${id.padEnd(46)} [${e.type}] ${e.selector ?? e.component ?? ""}`);
531
+ if (verbose) {
532
+ // 词汇约束模式:输出设计侧可用的 style 字段与 form 形态枚举(供下发词汇约束)
533
+ const fields = [...Object.keys(e.css_vars || {}), ...Object.keys(e.custom_style_map || {})];
534
+ const forms = extractForms(e);
535
+ if (fields.length > 0)
536
+ console.log(` style 字段:${fields.join(" ")}`);
537
+ if (forms.length > 0)
538
+ console.log(` form 形态:${forms.join(" / ")}`);
539
+ }
540
+ }
541
+ }
542
+
543
+ // ---- query ----
544
+ function cmdQuery(args, dataset) {
545
+ const id = args.find(a => !a.startsWith("--"));
546
+ const index = buildMergedIndex(dataset);
547
+ const entry = index[id];
548
+ if (!entry) {
549
+ console.error(`✖ 语义 ID「${id}」未在映射表中定义`);
550
+ process.exit(1);
551
+ }
552
+ console.log(JSON.stringify({ id, ...entry }, null, 2));
553
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * loader.js — mapping/ 目录读取与合并
3
+ *
4
+ * 递归遍历 mapping/ 下所有 .json 文件,按目录层级合并为完整映射数据集。
5
+ * 合并顺序(后加载覆盖先加载,同 ID 时场景级扩展全局定义):
6
+ * basic/ → layout/ → component/ → scenes/<scene>/
7
+ */
8
+
9
+ import { readdirSync, readFileSync, statSync } from "node:fs";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ export const MAPPING_ROOT = path.resolve(__dirname, "..", "mapping");
15
+
16
+ /** 递归收集 JSON 文件(按目录名字典序,保证合并顺序稳定) */
17
+ function collectJsonFiles(dir) {
18
+ const out = [];
19
+ for (const name of readdirSync(dir).sort()) {
20
+ if (name.startsWith("_") || name.startsWith("."))
21
+ continue;
22
+ const full = path.join(dir, name);
23
+ const st = statSync(full);
24
+ if (st.isDirectory())
25
+ out.push(...collectJsonFiles(full));
26
+ else if (name.endsWith(".json"))
27
+ out.push(full);
28
+ }
29
+ return out;
30
+ }
31
+
32
+ /**
33
+ * 构建映射数据集。
34
+ * @returns {{ files: Record<string, object>, fileCount: number, ids: Record<string, {file, meta}> }}
35
+ */
36
+ export function buildMappingDataset() {
37
+ const files = {};
38
+ const idSource = {};
39
+
40
+ for (const filePath of collectJsonFiles(MAPPING_ROOT)) {
41
+ const rel = path.relative(MAPPING_ROOT, filePath).replaceAll("\\", "/");
42
+ let data;
43
+ try {
44
+ data = JSON.parse(readFileSync(filePath, "utf-8"));
45
+ }
46
+ catch (e) {
47
+ throw new Error(`[mapping/${rel}] JSON 解析失败:${e.message}`);
48
+ }
49
+ files[rel] = data;
50
+
51
+ if (data.mappings) {
52
+ for (const id of Object.keys(data.mappings)) {
53
+ if (id.startsWith("$"))
54
+ continue;
55
+ if (idSource[id])
56
+ console.warn(` ⚠ 语义 ID「${id}」在 ${idSource[id].file} 与 ${rel} 中重复,后者覆盖`);
57
+ idSource[id] = { file: rel, meta: data.meta || {} };
58
+ }
59
+ }
60
+ }
61
+
62
+ return { files, fileCount: Object.keys(files).length, idSource };
63
+ }
64
+
65
+ /** 供 validator 使用的别名(保持独立可测) */
66
+ export { buildMappingDataset as buildMappingDatasetForValidator };