figma-cache-toolchain 2.0.3 → 2.0.5
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/README.md +197 -170
- package/cursor-bootstrap/AGENT-SETUP-PROMPT.md +75 -43
- package/cursor-bootstrap/examples/README.md +26 -15
- package/cursor-bootstrap/examples/ui-adapter.contract.template.json +90 -0
- package/cursor-bootstrap/examples/ui-execution-template.fast.md +11 -0
- package/cursor-bootstrap/examples/ui-execution-template.strict.md +13 -0
- package/cursor-bootstrap/examples/ui-override.template.json +26 -0
- package/cursor-bootstrap/figma-cache.config.example.js +51 -9
- package/cursor-bootstrap/managed-files.json +40 -40
- package/cursor-bootstrap/skills/figma-ui-dual-mode-execution/SKILL.md +55 -37
- package/figma-cache/adapters/recipes/button.recipe.json +24 -0
- package/figma-cache/adapters/recipes/card.recipe.json +24 -0
- package/figma-cache/adapters/recipes/checkbox.recipe.json +24 -0
- package/figma-cache/adapters/recipes/input.recipe.json +24 -0
- package/figma-cache/adapters/recipes/modal.recipe.json +25 -0
- package/figma-cache/adapters/recipes/radio.recipe.json +23 -0
- package/figma-cache/adapters/recipes/select.recipe.json +24 -0
- package/figma-cache/adapters/recipes/table.recipe.json +25 -0
- package/figma-cache/adapters/recipes/tabs.recipe.json +24 -0
- package/figma-cache/adapters/recipes/tooltip.recipe.json +24 -0
- package/figma-cache/docs/README.md +323 -237
- package/figma-cache/docs/p0-ui-preflight-handoff.md +207 -0
- package/figma-cache/docs/ui-1to1-optimization-roadmap.md +182 -0
- package/figma-cache/docs/ui-1to1-report.schema.json +104 -0
- package/figma-cache/figma-cache.js +639 -562
- package/figma-cache/js/contract-check-cli.js +466 -0
- package/figma-cache/js/cursor-bootstrap-cli.js +22 -0
- package/figma-cache/js/ui-facts-normalizer.js +233 -0
- package/package.json +93 -73
- package/scripts/cross-project-e2e.js +594 -0
- package/scripts/ui-1to1-audit.js +431 -0
- package/scripts/ui-auto-acceptance.js +248 -0
- package/scripts/ui-preflight.js +289 -0
- package/scripts/ui-profile.js +46 -0
- package/scripts/ui-report-aggregate.js +124 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
function normalizeHexColor(value) {
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
const raw = value.trim();
|
|
9
|
+
const match = raw.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/);
|
|
10
|
+
if (!match) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
const body = match[1].toUpperCase();
|
|
14
|
+
if (body.length === 3) {
|
|
15
|
+
return `#${body
|
|
16
|
+
.split("")
|
|
17
|
+
.map((ch) => `${ch}${ch}`)
|
|
18
|
+
.join("")}`;
|
|
19
|
+
}
|
|
20
|
+
return `#${body}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractTokenFactsFromSpec(specText) {
|
|
24
|
+
const output = [];
|
|
25
|
+
const regex = /-\s*([^:\n]+?)\s*:\s*(#[0-9a-fA-F]{3,8})/g;
|
|
26
|
+
let match = null;
|
|
27
|
+
while ((match = regex.exec(specText))) {
|
|
28
|
+
const tokenName = String(match[1] || "").trim();
|
|
29
|
+
const tokenValue = normalizeHexColor(String(match[2] || ""));
|
|
30
|
+
if (!tokenName && !tokenValue) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
output.push({
|
|
34
|
+
tokenName,
|
|
35
|
+
tokenValue,
|
|
36
|
+
source: "spec.md",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return output;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectTokenFactsFromVariableDefs(value, pathPrefix, output) {
|
|
43
|
+
if (value == null) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof value === "string") {
|
|
48
|
+
const hex = normalizeHexColor(value);
|
|
49
|
+
if (hex) {
|
|
50
|
+
output.push({
|
|
51
|
+
tokenName: pathPrefix,
|
|
52
|
+
tokenValue: hex,
|
|
53
|
+
source: "mcp-raw-get-variable-defs",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
value.forEach((entry, index) => {
|
|
61
|
+
const nextPrefix = pathPrefix ? `${pathPrefix}[${index}]` : `[${index}]`;
|
|
62
|
+
collectTokenFactsFromVariableDefs(entry, nextPrefix, output);
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof value === "object") {
|
|
68
|
+
Object.entries(value).forEach(([key, entry]) => {
|
|
69
|
+
const nextPrefix = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
70
|
+
collectTokenFactsFromVariableDefs(entry, nextPrefix, output);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isPlaceholderCell(input) {
|
|
76
|
+
const value = String(input || "").trim().toLowerCase();
|
|
77
|
+
if (!value || /^-+$/.test(value)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return /^(todo|tbd|n\/?a|none|\(none\)|待补充|待完善|待确认|占位|补充)/i.test(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractStatesFromStateMap(stateMapText) {
|
|
84
|
+
const states = new Set();
|
|
85
|
+
const lines = stateMapText.split(/\r?\n/);
|
|
86
|
+
let inStatesSection = false;
|
|
87
|
+
|
|
88
|
+
lines.forEach((line) => {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (/^##\s+States\b/i.test(trimmed)) {
|
|
91
|
+
inStatesSection = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (/^##\s+/i.test(trimmed) && !/^##\s+States\b/i.test(trimmed)) {
|
|
95
|
+
inStatesSection = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!inStatesSection || !trimmed.startsWith("|")) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const cells = trimmed
|
|
103
|
+
.split("|")
|
|
104
|
+
.slice(1, -1)
|
|
105
|
+
.map((cell) => cell.trim());
|
|
106
|
+
if (!cells.length) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const first = String(cells[0] || "").trim().toLowerCase();
|
|
111
|
+
if (!first || first === "state" || /^-+$/.test(first)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Ignore scaffold rows where visual/data columns are still placeholders.
|
|
116
|
+
const visualCell = cells.length > 1 ? cells[1] : "";
|
|
117
|
+
const dataCell = cells.length > 2 ? cells[2] : "";
|
|
118
|
+
if (isPlaceholderCell(visualCell) && isPlaceholderCell(dataCell)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
states.add(first);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return [...states];
|
|
126
|
+
}
|
|
127
|
+
function dedupeTokenFacts(facts) {
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
const output = [];
|
|
130
|
+
facts.forEach((fact) => {
|
|
131
|
+
const name = String(fact.tokenName || "").trim().toLowerCase();
|
|
132
|
+
const value = normalizeHexColor(String(fact.tokenValue || ""));
|
|
133
|
+
const key = `${name}@@${value}`;
|
|
134
|
+
if (seen.has(key)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
seen.add(key);
|
|
138
|
+
output.push({
|
|
139
|
+
tokenName: fact.tokenName,
|
|
140
|
+
tokenValue: value,
|
|
141
|
+
source: fact.source,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
return output;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mergeContractWithOverride(contract, override, hardErrors, cacheKey) {
|
|
148
|
+
const base = contract && typeof contract === "object" ? contract : {};
|
|
149
|
+
const nodeOverride = override && typeof override === "object" ? override : {};
|
|
150
|
+
const merged = {
|
|
151
|
+
...base,
|
|
152
|
+
tokenMappings: [...(Array.isArray(base.tokenMappings) ? base.tokenMappings : [])],
|
|
153
|
+
stateMappings: {
|
|
154
|
+
...(base.stateMappings && typeof base.stateMappings === "object" ? base.stateMappings : {}),
|
|
155
|
+
},
|
|
156
|
+
layoutRules: [...(Array.isArray(base.layoutRules) ? base.layoutRules : [])],
|
|
157
|
+
typographyRules: [...(Array.isArray(base.typographyRules) ? base.typographyRules : [])],
|
|
158
|
+
interactionRules: [...(Array.isArray(base.interactionRules) ? base.interactionRules : [])],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (!nodeOverride || Object.keys(nodeOverride).length === 0) {
|
|
162
|
+
return merged;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const overrideTokenMappings = Array.isArray(nodeOverride.tokenMappings)
|
|
166
|
+
? nodeOverride.tokenMappings
|
|
167
|
+
: [];
|
|
168
|
+
overrideTokenMappings.forEach((overrideMapping) => {
|
|
169
|
+
const tokenName = String(overrideMapping.figmaToken || "")
|
|
170
|
+
.trim()
|
|
171
|
+
.toLowerCase();
|
|
172
|
+
const global = merged.tokenMappings.find(
|
|
173
|
+
(entry) => String(entry.figmaToken || "").trim().toLowerCase() === tokenName
|
|
174
|
+
);
|
|
175
|
+
if (
|
|
176
|
+
global &&
|
|
177
|
+
global.projectBinding &&
|
|
178
|
+
overrideMapping.projectBinding &&
|
|
179
|
+
String(global.projectBinding.value || "") !== String(overrideMapping.projectBinding.value || "")
|
|
180
|
+
) {
|
|
181
|
+
hardErrors.push(
|
|
182
|
+
`node override conflict: ${cacheKey} token '${overrideMapping.figmaToken}' projectBinding differs from global contract`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
merged.tokenMappings.push(...overrideTokenMappings);
|
|
187
|
+
|
|
188
|
+
const overrideStateMappings =
|
|
189
|
+
nodeOverride.stateMappings && typeof nodeOverride.stateMappings === "object"
|
|
190
|
+
? nodeOverride.stateMappings
|
|
191
|
+
: {};
|
|
192
|
+
Object.entries(overrideStateMappings).forEach(([key, value]) => {
|
|
193
|
+
const global = merged.stateMappings[key];
|
|
194
|
+
if (global && Array.isArray(global.requiredStates) && value && Array.isArray(value.requiredStates)) {
|
|
195
|
+
const missing = global.requiredStates.filter(
|
|
196
|
+
(state) =>
|
|
197
|
+
!value.requiredStates.map((v) => String(v || "").toLowerCase()).includes(String(state || "").toLowerCase())
|
|
198
|
+
);
|
|
199
|
+
if (missing.length) {
|
|
200
|
+
hardErrors.push(
|
|
201
|
+
`node override conflict: ${cacheKey} stateMappings.${key} misses global requiredStates: ${missing.join(", ")}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
merged.stateMappings[key] = value;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (Array.isArray(nodeOverride.layoutRules)) {
|
|
209
|
+
merged.layoutRules.push(...nodeOverride.layoutRules);
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(nodeOverride.typographyRules)) {
|
|
212
|
+
merged.typographyRules.push(...nodeOverride.typographyRules);
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(nodeOverride.interactionRules)) {
|
|
215
|
+
merged.interactionRules.push(...nodeOverride.interactionRules);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return merged;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function evaluateRuleSet(ruleSet, sourceText, cacheKey, hardErrors, warnings) {
|
|
222
|
+
if (!Array.isArray(ruleSet)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
ruleSet.forEach((rule, indexNo) => {
|
|
226
|
+
if (!rule || typeof rule !== "object") {
|
|
227
|
+
hardErrors.push(`rule[${indexNo}] is not object`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const pattern = String(rule.pattern || "").trim();
|
|
231
|
+
const ruleId = String(rule.id || `rule-${indexNo}`);
|
|
232
|
+
const required = rule.required !== false;
|
|
233
|
+
if (!pattern) {
|
|
234
|
+
hardErrors.push(`${ruleId}: pattern missing`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
let matched = false;
|
|
238
|
+
try {
|
|
239
|
+
matched = new RegExp(pattern, "i").test(sourceText);
|
|
240
|
+
} catch {
|
|
241
|
+
hardErrors.push(`${ruleId}: invalid regex pattern '${pattern}'`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!matched && required) {
|
|
245
|
+
hardErrors.push(`${cacheKey}: missing required rule '${ruleId}'`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!matched && !required) {
|
|
249
|
+
warnings.push(`${cacheKey}: optional rule not matched '${ruleId}'`);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildContractCheckReport(options, deps) {
|
|
255
|
+
const {
|
|
256
|
+
cacheKey = "",
|
|
257
|
+
warnUnmappedTokens = false,
|
|
258
|
+
warnUnmappedStates = false,
|
|
259
|
+
} = options || {};
|
|
260
|
+
|
|
261
|
+
const {
|
|
262
|
+
index,
|
|
263
|
+
contract,
|
|
264
|
+
readTextOrEmpty,
|
|
265
|
+
readJsonOrNull,
|
|
266
|
+
resolveMaybeAbsolutePath,
|
|
267
|
+
normalizeSlash,
|
|
268
|
+
} = deps;
|
|
269
|
+
|
|
270
|
+
const hardErrors = [];
|
|
271
|
+
const warnings = [];
|
|
272
|
+
|
|
273
|
+
if (!index || typeof index !== "object") {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
hardErrors: ["index not found or invalid JSON"],
|
|
277
|
+
warnings: [],
|
|
278
|
+
checkedItems: 0,
|
|
279
|
+
checkedCacheKeys: [],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!contract || typeof contract !== "object") {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
hardErrors: ["adapter contract not found or invalid JSON"],
|
|
287
|
+
warnings: [],
|
|
288
|
+
checkedItems: 0,
|
|
289
|
+
checkedCacheKeys: [],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const items = index.items && typeof index.items === "object" ? index.items : {};
|
|
294
|
+
const keys = Object.keys(items);
|
|
295
|
+
const targetKeys = cacheKey
|
|
296
|
+
? keys.filter((key) => key === cacheKey)
|
|
297
|
+
: keys;
|
|
298
|
+
|
|
299
|
+
if (cacheKey && !targetKeys.length) {
|
|
300
|
+
hardErrors.push(`cacheKey not found: ${cacheKey}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const missingTokenMappings = [];
|
|
304
|
+
const missingStateMappings = [];
|
|
305
|
+
|
|
306
|
+
targetKeys.forEach((key) => {
|
|
307
|
+
const item = items[key];
|
|
308
|
+
const completeness = Array.isArray(item.completeness) ? item.completeness : [];
|
|
309
|
+
|
|
310
|
+
if (!item || !item.paths || !item.paths.meta) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const metaAbs = resolveMaybeAbsolutePath(item.paths.meta);
|
|
315
|
+
const nodeDir = require("path").dirname(metaAbs);
|
|
316
|
+
|
|
317
|
+
const specText = item.paths && item.paths.spec
|
|
318
|
+
? readTextOrEmpty(resolveMaybeAbsolutePath(item.paths.spec))
|
|
319
|
+
: "";
|
|
320
|
+
const stateMapText = item.paths && item.paths.stateMap
|
|
321
|
+
? readTextOrEmpty(resolveMaybeAbsolutePath(item.paths.stateMap))
|
|
322
|
+
: "";
|
|
323
|
+
const rawText = item.paths && item.paths.raw
|
|
324
|
+
? readTextOrEmpty(resolveMaybeAbsolutePath(item.paths.raw))
|
|
325
|
+
: "";
|
|
326
|
+
const overridePath = require("path").join(nodeDir, "ui-override.json");
|
|
327
|
+
const nodeOverride = readJsonOrNull(overridePath);
|
|
328
|
+
const targetContract = mergeContractWithOverride(contract, nodeOverride, hardErrors, key);
|
|
329
|
+
const tokenMappings = Array.isArray(targetContract.tokenMappings)
|
|
330
|
+
? targetContract.tokenMappings
|
|
331
|
+
: [];
|
|
332
|
+
const stateMappings =
|
|
333
|
+
targetContract.stateMappings && typeof targetContract.stateMappings === "object"
|
|
334
|
+
? targetContract.stateMappings
|
|
335
|
+
: {};
|
|
336
|
+
|
|
337
|
+
const mappedTokenNames = new Set();
|
|
338
|
+
const mappedTokenValues = new Set();
|
|
339
|
+
const mappedStates = new Set();
|
|
340
|
+
tokenMappings.forEach((mapping, indexNo) => {
|
|
341
|
+
if (!mapping || typeof mapping !== "object") {
|
|
342
|
+
hardErrors.push(`tokenMappings[${indexNo}] is not an object`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const tokenName = String(mapping.figmaToken || "").trim().toLowerCase();
|
|
346
|
+
const tokenValue = normalizeHexColor(String(mapping.figmaValue || ""));
|
|
347
|
+
if (tokenName) {
|
|
348
|
+
mappedTokenNames.add(tokenName);
|
|
349
|
+
}
|
|
350
|
+
if (tokenValue) {
|
|
351
|
+
mappedTokenValues.add(tokenValue);
|
|
352
|
+
}
|
|
353
|
+
if (mapping.required === true) {
|
|
354
|
+
const binding = mapping.projectBinding;
|
|
355
|
+
if (!binding || typeof binding !== "object") {
|
|
356
|
+
hardErrors.push(`required mapping '${mapping.id || indexNo}' missing projectBinding`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
Object.values(stateMappings).forEach((entry) => {
|
|
361
|
+
if (!entry || typeof entry !== "object") {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const requiredStates = Array.isArray(entry.requiredStates) ? entry.requiredStates : [];
|
|
365
|
+
requiredStates.forEach((state) => {
|
|
366
|
+
const normalized = String(state || "").trim().toLowerCase();
|
|
367
|
+
if (normalized) {
|
|
368
|
+
mappedStates.add(normalized);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
if (!tokenMappings.length) {
|
|
373
|
+
hardErrors.push("tokenMappings is empty");
|
|
374
|
+
}
|
|
375
|
+
if (!Object.keys(stateMappings).length) {
|
|
376
|
+
hardErrors.push("stateMappings is empty");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const manifestPath = require("path").join(nodeDir, "mcp-raw", "mcp-raw-manifest.json");
|
|
380
|
+
const manifest = readJsonOrNull(manifestPath);
|
|
381
|
+
let variableFacts = [];
|
|
382
|
+
if (
|
|
383
|
+
manifest &&
|
|
384
|
+
manifest.files &&
|
|
385
|
+
typeof manifest.files === "object" &&
|
|
386
|
+
manifest.files.get_variable_defs
|
|
387
|
+
) {
|
|
388
|
+
const variableDefsPath = require("path").join(
|
|
389
|
+
nodeDir,
|
|
390
|
+
"mcp-raw",
|
|
391
|
+
String(manifest.files.get_variable_defs)
|
|
392
|
+
);
|
|
393
|
+
const variableDefs = readJsonOrNull(variableDefsPath);
|
|
394
|
+
if (variableDefs && typeof variableDefs === "object") {
|
|
395
|
+
const output = [];
|
|
396
|
+
collectTokenFactsFromVariableDefs(variableDefs, "", output);
|
|
397
|
+
variableFacts = output;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const tokenFacts = dedupeTokenFacts([
|
|
402
|
+
...extractTokenFactsFromSpec(specText),
|
|
403
|
+
...variableFacts,
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
if (completeness.includes("tokens") || tokenFacts.length) {
|
|
407
|
+
tokenFacts.forEach((fact) => {
|
|
408
|
+
const name = String(fact.tokenName || "").trim().toLowerCase();
|
|
409
|
+
const value = normalizeHexColor(String(fact.tokenValue || ""));
|
|
410
|
+
const matchedByName = name && mappedTokenNames.has(name);
|
|
411
|
+
const matchedByValue = value && mappedTokenValues.has(value);
|
|
412
|
+
|
|
413
|
+
if (!matchedByName && !matchedByValue) {
|
|
414
|
+
missingTokenMappings.push(
|
|
415
|
+
`token unmapped: ${key} :: ${fact.tokenName || "(empty)"} :: ${
|
|
416
|
+
value || "(none)"
|
|
417
|
+
} (${fact.source})`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (completeness.includes("states") || stateMapText) {
|
|
424
|
+
const states = extractStatesFromStateMap(stateMapText);
|
|
425
|
+
states.forEach((state) => {
|
|
426
|
+
if (!mappedStates.has(state)) {
|
|
427
|
+
missingStateMappings.push(`state unmapped: ${key} :: ${state} (state-map.md)`);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const ruleSource = `${specText}\n${stateMapText}\n${rawText}`;
|
|
433
|
+
evaluateRuleSet(targetContract.layoutRules, ruleSource, key, hardErrors, warnings);
|
|
434
|
+
evaluateRuleSet(targetContract.typographyRules, ruleSource, key, hardErrors, warnings);
|
|
435
|
+
evaluateRuleSet(targetContract.interactionRules, ruleSource, key, hardErrors, warnings);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (missingTokenMappings.length) {
|
|
439
|
+
if (warnUnmappedTokens) {
|
|
440
|
+
warnings.push(...missingTokenMappings);
|
|
441
|
+
} else {
|
|
442
|
+
hardErrors.push(...missingTokenMappings);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (missingStateMappings.length) {
|
|
447
|
+
if (warnUnmappedStates) {
|
|
448
|
+
warnings.push(...missingStateMappings);
|
|
449
|
+
} else {
|
|
450
|
+
hardErrors.push(...missingStateMappings);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
ok: hardErrors.length === 0,
|
|
456
|
+
hardErrors,
|
|
457
|
+
warnings,
|
|
458
|
+
checkedItems: targetKeys.length,
|
|
459
|
+
checkedCacheKeys: targetKeys,
|
|
460
|
+
contract: normalizeSlash(String(options.contractPath || "")),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
module.exports = {
|
|
465
|
+
buildContractCheckReport,
|
|
466
|
+
};
|
|
@@ -89,6 +89,28 @@ function copyCursorBootstrap(options, deps) {
|
|
|
89
89
|
copied += 1;
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
const requiredExampleTemplates = [
|
|
93
|
+
"examples/ui-adapter.contract.template.json",
|
|
94
|
+
"examples/ui-1to1-preflight.template.md",
|
|
95
|
+
"examples/ui-override.template.json",
|
|
96
|
+
"examples/ui-execution-template.fast.md",
|
|
97
|
+
"examples/ui-execution-template.strict.md",
|
|
98
|
+
];
|
|
99
|
+
requiredExampleTemplates.forEach((relPath) => {
|
|
100
|
+
const absFrom = path.join(CURSOR_BOOTSTRAP_DIR, relPath);
|
|
101
|
+
const absTo = path.join(ROOT, "cursor-bootstrap", relPath);
|
|
102
|
+
if (!fs.existsSync(absFrom)) {
|
|
103
|
+
console.error(`[figma-cache] missing template file: ${normalizeSlash(absFrom)}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
fs.mkdirSync(path.dirname(absTo), { recursive: true });
|
|
107
|
+
if (fs.existsSync(absTo) && !overwrite) {
|
|
108
|
+
skipped += 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
fs.copyFileSync(absFrom, absTo);
|
|
112
|
+
copied += 1;
|
|
113
|
+
});
|
|
92
114
|
const retiredDeleted = retired
|
|
93
115
|
.map((relPath) => {
|
|
94
116
|
const abs = path.join(ROOT, relPath);
|