@design-twin/dtwin 0.1.0-alpha.0 → 0.1.1

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.
@@ -0,0 +1,1526 @@
1
+ // GENERATED from design-to-code/ by claude-plugin/build-scripts.js — edit the source, then rebuild.
2
+
3
+
4
+ // design-to-code/kinds.ts
5
+ var TYPE_TO_KIND = { VARIANT: "enum", BOOLEAN: "boolean", TEXT: "string", INSTANCE_SWAP: "instance", SLOT: "instance" };
6
+
7
+ // bridge/src/json-util.ts
8
+ function nullProto() {
9
+ return /* @__PURE__ */ Object.create(null);
10
+ }
11
+ function isRecord(x) {
12
+ return typeof x === "object" && x !== null && !Array.isArray(x);
13
+ }
14
+ function isStringArray(x) {
15
+ return Array.isArray(x) && x.every((v) => typeof v === "string");
16
+ }
17
+ function ifDefined(key, v) {
18
+ const o = {};
19
+ if (v !== void 0) o[key] = v;
20
+ return o;
21
+ }
22
+
23
+ // bridge/src/design-system-layout.ts
24
+ var COMPONENTS_DIR = "components";
25
+ var TOKENS = "tokens.json";
26
+ var COMPONENTS_LOCAL = "components.local.json";
27
+ var COMPONENTS_LIBRARY = "components.library.json";
28
+ var HYGIENE = "hygiene.json";
29
+ var MANIFEST = "design-system.json";
30
+ var STYLE_KIND_ROWS = {
31
+ paint: { kind: "paint", file: "styles.paint.json", key: "stylesPaint" },
32
+ text: { kind: "text", file: "styles.text.json", key: "stylesText" },
33
+ effect: { kind: "effect", file: "styles.effect.json", key: "stylesEffect" },
34
+ grid: { kind: "grid", file: "styles.grid.json", key: "stylesGrid" },
35
+ animation: { kind: "animation", file: "styles.animation.json", key: "stylesAnimation" }
36
+ // local custom animation styles
37
+ };
38
+ var STYLE_KINDS = Object.values(STYLE_KIND_ROWS);
39
+ var DESIGN_SYSTEM_FILES = { TOKENS, COMPONENTS_LOCAL, COMPONENTS_LIBRARY, COMPONENTS_DIR, HYGIENE, MANIFEST };
40
+ var DESIGN_SYSTEM_FILE_NAMES = [TOKENS, ...STYLE_KINDS.map((row) => row.file), COMPONENTS_LOCAL, COMPONENTS_LIBRARY, HYGIENE, MANIFEST];
41
+
42
+ // bridge/src/project-layout.ts
43
+ import path from "node:path";
44
+ var DESIGN_DIR = "design";
45
+ var EXPORT_SUBDIR = "export";
46
+ var EXPORT_DIR = path.join(DESIGN_DIR, EXPORT_SUBDIR);
47
+ var TARGET_FILE = path.join(DESIGN_DIR, "target.json");
48
+ var MAP_FILE = path.join(DESIGN_DIR, "codeconnect.local.json");
49
+ var PLAN_DIR = path.join(DESIGN_DIR, "plan");
50
+ var AUDIT_DIR = path.join(DESIGN_DIR, "audit");
51
+ var VERIFY_DIR = path.join(DESIGN_DIR, "verify");
52
+ var SNAPSHOT_DIR = path.join(DESIGN_DIR, ".sync");
53
+ var TAILWIND_SOURCE_NOT_NOTE = `Tailwind v4 scans every file git does not ignore, ${DESIGN_DIR}/ included, so class names quoted in ${DESIGN_DIR}/ notes, audits and plans end up in your CSS. Next to \`@import "tailwindcss";\` in your CSS entry, add \`@source not "<path from that CSS file to ${DESIGN_DIR}/>";\` (e.g. \`@source not "../${DESIGN_DIR}";\` for src/app.css) \u2014 Tailwind v4.1+`;
54
+ var VITE_WATCH_IGNORED_NOTE = `With Tailwind v4's automatic source detection, rewriting an existing text file under ${DESIGN_DIR}/ (a re-export, a verify report) makes Vite fully reload the open page. Either add \`server: { watch: { ignored: ['**/${DESIGN_DIR}/**'] } }\` in vite.config (merge it with any existing \`server.watch\` options), or the Tailwind \`@source not\` above \u2014 both stop it`;
55
+ var slashed = (p) => p.split(path.sep).join("/");
56
+ var VERIFY_GITIGNORE_NOTE = `${slashed(VERIFY_DIR)}/ is regenerated on every verify run (measurements, screenshots, reports) \u2014 consider adding \`${slashed(VERIFY_DIR)}/\` to .gitignore; decisions live in ${slashed(PLAN_DIR)}/ and are not affected`;
57
+
58
+ // bridge/src/read-json.ts
59
+ import fs from "node:fs";
60
+
61
+ // bridge/src/errmsg.ts
62
+ var errMsg = (e) => typeof e === "string" ? e : String(e && e.message || e);
63
+ var errCode = (e) => e && typeof e === "object" && "code" in e && typeof e.code === "string" ? e.code : void 0;
64
+
65
+ // bridge/src/read-json.ts
66
+ var anyJson = (_x) => true;
67
+ function readFailure(e) {
68
+ const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
69
+ if (code === "ENOENT") return { error: "does not exist", missing: true };
70
+ return {
71
+ error: code === "EISDIR" ? "is a directory, not a file" : code === "EACCES" ? "is not readable (permission denied)" : `could not be read (${String(code || e)})`
72
+ };
73
+ }
74
+ function readJson(file, guard) {
75
+ let buf;
76
+ try {
77
+ buf = fs.readFileSync(file);
78
+ } catch (e) {
79
+ return readFailure(e);
80
+ }
81
+ if (buf.length >= 2 && (buf[0] === 255 && buf[1] === 254 || buf[0] === 254 && buf[1] === 255)) {
82
+ return { error: "is UTF-16, not UTF-8 \u2014 re-save it as UTF-8" };
83
+ }
84
+ let raw = buf.toString("utf8");
85
+ if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
86
+ let parsed;
87
+ try {
88
+ const value = JSON.parse(raw);
89
+ parsed = value;
90
+ } catch (e) {
91
+ return { error: `is not valid JSON \u2014 ${errMsg(e)}` };
92
+ }
93
+ if (!guard(parsed)) return { error: `is not ${guard.expected || "the expected kind of document"}` };
94
+ return { doc: parsed };
95
+ }
96
+ function readJsonOrNull(file, guard) {
97
+ const r = readJson(file, guard);
98
+ return "doc" in r ? r.doc : null;
99
+ }
100
+
101
+ // bridge/src/export-verdict.ts
102
+ var STALE_MS = 24 * 60 * 60 * 1e3;
103
+ function field(doc, key) {
104
+ return doc && typeof doc === "object" && key in doc ? doc[key] : void 0;
105
+ }
106
+ var asText = (v) => v ? typeof v === "string" ? v : String(v) : void 0;
107
+ function snapshotAge(doc, now) {
108
+ const exportedAt = asText(field(doc, "exportedAt"));
109
+ if (!exportedAt) return { exportedAt: void 0, ageMs: void 0, problem: "missing" };
110
+ const t = Date.parse(exportedAt);
111
+ if (Number.isNaN(t)) return { exportedAt, ageMs: void 0, problem: "unparseable" };
112
+ return { exportedAt, ageMs: (now || Date.now()) - t };
113
+ }
114
+
115
+ // design-to-code/color.ts
116
+ var HEX = /^#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
117
+ function normHex(v) {
118
+ if (typeof v !== "string") return null;
119
+ const g = HEX.exec(v.trim())?.[1];
120
+ if (g === void 0) return null;
121
+ const h = g.toLowerCase();
122
+ return "#" + (h.length <= 4 ? h.split("").map((c) => c + c).join("") : h);
123
+ }
124
+ function colorKey(v) {
125
+ const h = normHex(v);
126
+ return h === null ? null : h.length === 7 ? h + "ff" : h;
127
+ }
128
+ function parseHex(v) {
129
+ const k = colorKey(v);
130
+ if (k === null) return null;
131
+ const n = (i) => parseInt(k.slice(i, i + 2), 16);
132
+ return { r: n(1), g: n(3), b: n(5), a: n(7) / 255 };
133
+ }
134
+ var NUM = "[+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:e[+-]?\\d+)?";
135
+ var PCT = `${NUM}%`;
136
+ var CH = `${NUM}%?`;
137
+ var CH_OR_NONE = `(?:${CH}|none)`;
138
+ var legacy = (ch) => `\\(\\s*(${ch})\\s*,\\s*(${ch})\\s*,\\s*(${ch})\\s*(?:,\\s*(${CH})\\s*)?\\)`;
139
+ var RGB_LEGACY = new RegExp(`^rgba?(?:${legacy(PCT)}|${legacy(NUM)})$`);
140
+ var RGB_MODERN = new RegExp(`^rgba?\\(\\s*(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s*(?:/\\s*(${CH_OR_NONE})\\s*)?\\)$`);
141
+ var OK = new RegExp(`^(oklab|oklch)\\(\\s*(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s*(?:/\\s*(${CH_OR_NONE})\\s*)?\\)$`);
142
+ var SRGB = new RegExp(`^color\\(\\s*srgb\\s+(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s*(?:/\\s*(${CH_OR_NONE})\\s*)?\\)$`);
143
+ function compositeOver(fg, bg) {
144
+ return {
145
+ r: fg.r * fg.a + bg.r * (1 - fg.a),
146
+ g: fg.g * fg.a + bg.g * (1 - fg.a),
147
+ b: fg.b * fg.a + bg.b * (1 - fg.a),
148
+ a: 1
149
+ };
150
+ }
151
+
152
+ // design-to-code/visible-tree.ts
153
+ var hiddenSelf = (node) => !!(node && typeof node === "object" && "hidden" in node && node.hidden);
154
+ var isHidden = (node, ancestorHidden) => !!ancestorHidden || hiddenSelf(node);
155
+ var WHITE = { r: 255, g: 255, b: 255, a: 1 };
156
+ var NOTHING = { color: null, complex: false, token: null };
157
+ var paintToken = (p) => {
158
+ const t = p.tokens && p.tokens.color;
159
+ const s = Array.isArray(t) ? t[0] : t;
160
+ return typeof s === "string" && s ? s : null;
161
+ };
162
+ function paintOver(base, n) {
163
+ if (n.type === "TEXT") return base;
164
+ const fills = (Array.isArray(n.fills) ? n.fills : []).filter(Boolean);
165
+ const nodeTok = n.tokens && typeof n.tokens.fills === "string" && n.tokens.fills ? n.tokens.fills : null;
166
+ if (!fills.length) return nodeTok ? { ...base, token: nodeTok } : base;
167
+ let { color, complex, token } = base;
168
+ for (const f of fills) {
169
+ if (f.opacity === 0 || f.visible === false) continue;
170
+ if (f.type !== "solid") {
171
+ complex = true;
172
+ token = null;
173
+ continue;
174
+ }
175
+ const c = parseHex(f.color);
176
+ const tok = paintToken(f) ?? (fills.length === 1 ? nodeTok : null);
177
+ if (!c) {
178
+ token = tok;
179
+ continue;
180
+ }
181
+ if (c.a <= 0) continue;
182
+ color = color ? compositeOver(c, color) : c.a >= 1 ? c : compositeOver(c, WHITE);
183
+ if (c.a >= 1) {
184
+ complex = false;
185
+ token = tok;
186
+ } else token = null;
187
+ }
188
+ return { color, complex, token };
189
+ }
190
+ var INK_TYPES = /* @__PURE__ */ new Set(["VECTOR", "BOOLEAN_OPERATION", "LINE", "FRAME", "INSTANCE", "GROUP", "COMPONENT"]);
191
+ function isIconInk(n) {
192
+ if (!(n.asset || n.geometry || n.assetSkipped) || !INK_TYPES.has(String(n.type))) return false;
193
+ return !(Array.isArray(n.fills) && n.fills.some((f) => f.type === "image" || f.type === "gradient"));
194
+ }
195
+ function overlaps(a, b) {
196
+ if (!a || !b || typeof a.x !== "number" || typeof a.y !== "number" || typeof b.x !== "number" || typeof b.y !== "number") return true;
197
+ return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
198
+ }
199
+ var paintsSurface = (n) => !hiddenSelf(n) && n.type !== "TEXT" && !isIconInk(n) && Array.isArray(n.fills) && n.fills.length > 0;
200
+ function beneathOf(parent, kids, i) {
201
+ const child = kids[i];
202
+ if (!child) return [];
203
+ const stacks = !parent.layout || parent.layout.mode === "absolute";
204
+ if (!stacks && !child.absolute) return [];
205
+ return kids.slice(0, i).filter((s) => s && typeof s === "object" && paintsSurface(s) && overlaps(s.box, child.box));
206
+ }
207
+ function memo(fn) {
208
+ let cell = null;
209
+ return () => (cell ??= { v: fn() }).v;
210
+ }
211
+ function walk(root, fn, prune, opts) {
212
+ const pathOf = opts && opts.pathOf || ((n, i) => n.name || n.type || String(i));
213
+ (function go(node, parentHidden, path4, ancestors, under, beneath) {
214
+ if (!node || typeof node !== "object") return;
215
+ const hidden = isHidden(node, parentHidden);
216
+ if (hidden && prune) return;
217
+ const backdrop = memo(() => beneath().reduce(paintOver, under()));
218
+ const parentFrame = ancestors[ancestors.length - 1];
219
+ const frame = { node, hidden, get backdrop() {
220
+ return backdrop();
221
+ }, get beneath() {
222
+ return beneath();
223
+ } };
224
+ fn(node, {
225
+ hidden,
226
+ parentHidden: !!parentHidden,
227
+ path: path4,
228
+ parent: parentFrame ? parentFrame.node : null,
229
+ depth: ancestors.length,
230
+ ancestors,
231
+ get backdrop() {
232
+ return backdrop();
233
+ },
234
+ get beneath() {
235
+ return beneath();
236
+ }
237
+ });
238
+ const kids = Array.isArray(node.children) ? node.children : [];
239
+ if (!kids.length) return;
240
+ const inside = memo(() => paintOver(backdrop(), node));
241
+ const chain = [...ancestors, frame];
242
+ for (const [i, kid] of kids.entries()) go(kid, hidden, (path4 ? path4 + " > " : "") + pathOf(kid, i), chain, inside, memo(() => beneathOf(node, kids, i)));
243
+ })(root, false, root ? pathOf(root, 0) : "", [], () => NOTHING, () => []);
244
+ }
245
+ function walkWithHidden(root, fn, opts) {
246
+ walk(root, fn, false, opts);
247
+ }
248
+ function walkVisible(root, fn, opts) {
249
+ walk(root, fn, true, opts);
250
+ }
251
+
252
+ // design-to-code/types.ts
253
+ var PLAN_VERIFICATION_RECORD_SCHEMA = "designtwin/plan-verification-record@2";
254
+ function isJsonObject(x) {
255
+ return typeof x === "object" && x !== null && !Array.isArray(x);
256
+ }
257
+
258
+ // bridge/src/stdio.ts
259
+ import { format, inspect } from "node:util";
260
+ function installQueuedConsole(o = {}) {
261
+ trackWrites(process.stdout);
262
+ trackWrites(process.stderr);
263
+ const out = o.logTo === "stderr" ? process.stderr : process.stdout;
264
+ const err = process.stderr;
265
+ let indent = "";
266
+ const write = (s, text) => {
267
+ s.write((indent ? text.replace(/^/gm, indent) : text) + "\n");
268
+ };
269
+ const line = (s) => (...a) => {
270
+ write(s, format(...a));
271
+ };
272
+ console.log = console.info = console.debug = line(out);
273
+ console.warn = console.error = line(err);
274
+ console.dir = (item, options) => {
275
+ write(out, inspect(item, { customInspect: false, ...options }));
276
+ };
277
+ console.dirxml = line(out);
278
+ console.table = (data) => {
279
+ write(out, inspect(data));
280
+ };
281
+ console.write = (...data) => {
282
+ let n = 0;
283
+ for (const d of data) {
284
+ const b = typeof d === "string" ? Buffer.from(d) : d instanceof ArrayBuffer ? Buffer.from(d) : Buffer.from(d.buffer, d.byteOffset, d.byteLength);
285
+ if (b.length) {
286
+ out.write(b);
287
+ n += b.length;
288
+ }
289
+ }
290
+ return n;
291
+ };
292
+ console.clear = () => {
293
+ if (out.isTTY) out.write("\x1B[1;1H\x1B[0J");
294
+ };
295
+ console.trace = (...a) => {
296
+ const e = new Error(format(...a));
297
+ e.name = "Trace";
298
+ Error.captureStackTrace(e, console.trace);
299
+ write(err, e.stack ?? `Trace: ${e.message}`);
300
+ };
301
+ console.assert = (cond, ...a) => {
302
+ if (!cond) write(err, a.length ? "Assertion failed: " + format(...a) : "Assertion failed");
303
+ };
304
+ console.group = console.groupCollapsed = (...a) => {
305
+ if (a.length) write(out, format(...a));
306
+ indent += " ";
307
+ };
308
+ console.groupEnd = () => {
309
+ indent = indent.slice(0, -2);
310
+ };
311
+ const counts = /* @__PURE__ */ new Map();
312
+ console.count = (label = "default") => {
313
+ const n = (counts.get(label) ?? 0) + 1;
314
+ counts.set(label, n);
315
+ write(out, `${label}: ${n}`);
316
+ };
317
+ console.countReset = (label = "default") => {
318
+ counts.delete(label);
319
+ };
320
+ const timers = /* @__PURE__ */ new Map();
321
+ const elapsed = (label) => {
322
+ const t = timers.get(label);
323
+ return t === void 0 ? null : `${label}: ${(performance.now() - t).toFixed(3)}ms`;
324
+ };
325
+ console.time = (label = "default") => {
326
+ timers.set(label, performance.now());
327
+ };
328
+ console.timeLog = (label = "default", ...a) => {
329
+ const e = elapsed(label);
330
+ if (e === null) write(err, `Warning: No such label '${label}' for console.timeLog()`);
331
+ else write(out, [e, ...a.map((x) => format(x))].join(" "));
332
+ };
333
+ console.timeEnd = (label = "default") => {
334
+ const e = elapsed(label);
335
+ timers.delete(label);
336
+ write(e === null ? err : out, e ?? `Warning: No such label '${label}' for console.timeEnd()`);
337
+ };
338
+ }
339
+ var inFlight = /* @__PURE__ */ new Map();
340
+ function trackWrites(s) {
341
+ if (inFlight.has(s)) return;
342
+ const st = { n: 0, idle: [] };
343
+ inFlight.set(s, st);
344
+ const write = s.write.bind(s);
345
+ const settle = () => {
346
+ if (--st.n === 0) for (const r of st.idle.splice(0)) r();
347
+ };
348
+ s.write = (chunk, encoding, cb) => {
349
+ const done = typeof encoding === "function" ? encoding : cb;
350
+ const enc = typeof encoding === "function" ? void 0 : encoding;
351
+ if (chunk.length === 0) return write(chunk, enc, done);
352
+ st.n++;
353
+ let called = false;
354
+ return write(chunk, enc, (err) => {
355
+ if (!called) {
356
+ called = true;
357
+ settle();
358
+ }
359
+ done?.(err);
360
+ });
361
+ };
362
+ }
363
+ var CliExit = class extends Error {
364
+ code;
365
+ constructor(code) {
366
+ super(`exit ${code}`);
367
+ this.name = "CliExit";
368
+ this.code = code;
369
+ }
370
+ };
371
+ var cliDepth = 0;
372
+ function exitCli(code) {
373
+ if (cliDepth > 0) throw new CliExit(code);
374
+ process.exit(code);
375
+ }
376
+ function runCli(main2, o, argv = process.argv.slice(2)) {
377
+ installQueuedConsole();
378
+ const done = o.done ?? ((code) => {
379
+ process.exitCode = code;
380
+ });
381
+ const fail = (e) => {
382
+ if (e instanceof CliExit) return done(e.code);
383
+ console.error(o.errorLine ? o.errorLine(e) : `${o.name}: ${e instanceof Error && e.stack || String(e)}`);
384
+ done(o.errorCode ?? 1);
385
+ };
386
+ cliDepth++;
387
+ let settled = false;
388
+ const leave = () => {
389
+ if (!settled) {
390
+ settled = true;
391
+ cliDepth--;
392
+ }
393
+ };
394
+ try {
395
+ const r = main2(argv);
396
+ if (typeof r === "number") {
397
+ leave();
398
+ done(r);
399
+ } else r.then((c) => {
400
+ leave();
401
+ done(c);
402
+ }, (e) => {
403
+ leave();
404
+ fail(e);
405
+ });
406
+ } catch (e) {
407
+ leave();
408
+ fail(e);
409
+ }
410
+ }
411
+
412
+ // design-to-code/proc.ts
413
+ var pause = new Int32Array(new SharedArrayBuffer(4));
414
+
415
+ // design-to-code/catalog-input.ts
416
+ function isManifest(doc, payloadKey) {
417
+ return isJsonObject(doc) && isJsonObject(doc.files) && !Array.isArray(doc[payloadKey]);
418
+ }
419
+ function manifestProblem(doc, givenPath, payloadKey, wantFile) {
420
+ if (!isManifest(doc, payloadKey)) return null;
421
+ return `'${givenPath}' is the design-system MANIFEST (a pointer map), not the '${payloadKey}' catalog.
422
+ Since the design-system split it carries only a stamp, a \`files\` map and \`counts\`.
423
+ Pass the split file instead \u2014 e.g. ${doc.files[payloadKey === "variables" ? "tokens" : "componentsLocal"] || wantFile} (relative to the export dir that holds ${givenPath}).`;
424
+ }
425
+ function loadDocFile(file, what, guard, hint, readAt = file) {
426
+ const r = readJson(readAt, guard);
427
+ if ("doc" in r) return r;
428
+ const ioFailure = r.missing || /^(is a directory|is not readable|could not be read)/.test(r.error);
429
+ return { error: `${what}: '${file}' ${r.error}${ioFailure ? "." : ""}` + (ioFailure && hint ? `
430
+ ${hint}` : "") };
431
+ }
432
+ function loadSplitFile(file, what, guard, payloadKey, wantFile, hint) {
433
+ const r = loadDocFile(file, what, anyJson, hint);
434
+ if ("error" in r) return r;
435
+ const manifest = manifestProblem(r.doc, file, payloadKey, wantFile);
436
+ if (manifest !== null) return { error: manifest };
437
+ if (guard(r.doc)) return { doc: r.doc };
438
+ return { error: `${what}: '${file}' is not ${guard.expected || "the expected kind of document"}` };
439
+ }
440
+ var NO_DESIGN_SYSTEM_HINT = "A single-screen pull (`dtwin pull --node <id>`) exports only that screen \u2014 it does not\n write design/export/design-system/. Run `dtwin pull --design-system` to create it.";
441
+
442
+ // design-to-code/doc-guards.ts
443
+ function optArrayOf(x, each) {
444
+ return x === void 0 || Array.isArray(x) && x.every(each);
445
+ }
446
+ var optObj = (x) => x === void 0 || isJsonObject(x);
447
+ var optStr = (x) => x === void 0 || typeof x === "string";
448
+ var anyObject = (x) => isJsonObject(x);
449
+ function isVariable(x) {
450
+ return isJsonObject(x) && typeof x.name === "string" && typeof x.type === "string" && isJsonObject(x.values) && optStr(x.collection);
451
+ }
452
+ function isVariableCollection(x) {
453
+ return isJsonObject(x) && typeof x.name === "string" && isStringArray(x.modes);
454
+ }
455
+ function isTokensDoc(x) {
456
+ return isJsonObject(x) && optArrayOf(x.variables, isVariable) && optArrayOf(x.collections, isVariableCollection) && optArrayOf(x._slices, anyObject) && optArrayOf(x._conflicts, anyObject) && (x.hygiene === void 0 || isStringArray(x.hygiene));
457
+ }
458
+ isTokensDoc.expected = "a token catalog: an object whose `variables` (each {name, type, values}) and `collections` (each {name, modes[]}), when present, are arrays";
459
+ function isCatalogComponent(x) {
460
+ return isJsonObject(x) && typeof x.name === "string" && optStr(x.key) && optStr(x.id) && optObj(x.props) && optArrayOf(x.variants, anyObject);
461
+ }
462
+ function isComponentsCatalog(x) {
463
+ return isJsonObject(x) && Array.isArray(x.components) && x.components.every(isCatalogComponent);
464
+ }
465
+ isComponentsCatalog.expected = "a component catalog: an object with a `components` array of {name, type, key?, id?, props?}";
466
+ function isComponentDetailFile(x) {
467
+ return isJsonObject(x) && typeof x.name === "string" && optArrayOf(x.variants, anyObject) && optObj(x.node);
468
+ }
469
+ isComponentDetailFile.expected = "a component detail file: an object with a `name` and `variants[]` or `node`";
470
+ function isTextStylesDoc(x) {
471
+ return isJsonObject(x) && Array.isArray(x.styles) && x.styles.every((s) => isJsonObject(s) && typeof s.name === "string");
472
+ }
473
+ isTextStylesDoc.expected = "a text-style sheet: an object with a `styles` array of {name, \u2026}";
474
+ function isScreenAssetsDoc(x) {
475
+ return isJsonObject(x) && optArrayOf(x.heavy, (h) => isJsonObject(h) && typeof h.file === "string" && typeof h.bytes === "number" && (h.paths === void 0 || typeof h.paths === "number") && (h.embeddedRaster === void 0 || typeof h.embeddedRaster === "number")) && optArrayOf(x.files, (f) => isJsonObject(f) && typeof f.file === "string" && optStr(f.node));
476
+ }
477
+ isScreenAssetsDoc.expected = "a screen asset manifest: an object whose `heavy` ({file, bytes}) and `files` ({file, node?}), when present, are arrays";
478
+ function isLibrariesIndex(x) {
479
+ return isJsonObject(x) && Array.isArray(x.libraries) && x.libraries.every((r) => isJsonObject(r) && typeof r.dir === "string" && optStr(r.libraryName) && (r.collectionKeys === void 0 || isStringArray(r.collectionKeys)));
480
+ }
481
+ isLibrariesIndex.expected = "a library index: an object with a `libraries` array of {dir, libraryName?, collectionKeys?}";
482
+ var SEVERITIES = ["blocker", "warning", "info"];
483
+ function isAuditOverridesDoc(x) {
484
+ return isJsonObject(x) && Array.isArray(x.overrides) && x.overrides.every((o) => isJsonObject(o) && typeof o.code === "string" && typeof o.severity === "string" && SEVERITIES.includes(o.severity) && typeof o.reason === "string" && o.reason.trim() !== "" && ["nodeId", "token", "component", "collection", "mode", "category", "state", "screen", "decidedBy", "decidedAt"].every((k) => optStr(o[k])));
485
+ }
486
+ isAuditOverridesDoc.expected = "an audit overrides file: { overrides: [{ code, severity: blocker|warning|info, reason (non-empty), nodeId?, token?, component?, collection?, mode?, category?, state?, screen?, decidedBy?, decidedAt? }] }";
487
+ function isAuditReport(x) {
488
+ return isJsonObject(x) && isJsonObject(x.summary) && Array.isArray(x.findings) && x.findings.every((f) => isJsonObject(f) && typeof f.severity === "string" && typeof f.code === "string");
489
+ }
490
+ isAuditReport.expected = "an audit report (written by the audit script's --out): an object with `summary` and a `findings` array of {severity, code, message}";
491
+ function isProposal(x) {
492
+ return isJsonObject(x) && typeof x.name === "string";
493
+ }
494
+ function isProposalList(x) {
495
+ return Array.isArray(x) && x.every(isProposal);
496
+ }
497
+ isProposalList.expected = "a list of component proposals: an array of {name, catalog, confirmed, \u2026}";
498
+ function isVerifyExpectation(x) {
499
+ return isJsonObject(x) && isJsonObject(x.frame) && Array.isArray(x.nodes) && x.nodes.every((n) => isJsonObject(n) && typeof n.nodeId === "string") && optArrayOf(x.instances, anyObject) && optArrayOf(x.interactions, anyObject) && optArrayOf(x.notComparable, anyObject);
500
+ }
501
+ isVerifyExpectation.expected = "a verify expectation (the verify-screen script's --expect output): an object with `frame` and a `nodes` array of {nodeId, \u2026}";
502
+ var isNameVersion = (x) => isJsonObject(x) && typeof x.version === "string" && (typeof x.package === "string" || typeof x.name === "string");
503
+ function isProbeIdentity(x) {
504
+ return isJsonObject(x) && typeof x.name === "string" && (x.version === null || typeof x.version === "string") && typeof x.sha256 === "string" && isNameVersion(x.playwright) && isNameVersion(x.browser);
505
+ }
506
+ function isBuildIdentity(x) {
507
+ return isJsonObject(x) && typeof x.url === "string" && (x.mode === "vite-dev" || x.mode === "static" || x.mode === "unknown") && typeof x.assets === "number" && typeof x.assetsSha256 === "string" && (x.unhashed === void 0 || typeof x.unhashed === "number") && (x.gitHead === null || typeof x.gitHead === "string") && (x.gitDirty === null || typeof x.gitDirty === "boolean");
508
+ }
509
+ var isProbeFrame = (x) => isJsonObject(x) && typeof x.nodeId === "string" && typeof x.selector === "string" && typeof x.via === "string" && isJsonObject(x.rect);
510
+ var isCountMap = (x) => isJsonObject(x) && Object.values(x).every((v) => typeof v === "number");
511
+ var isNavigation = (x) => isJsonObject(x) && Array.isArray(x.events) && typeof x.afterInitialLoad === "number" && typeof x.reruns === "number";
512
+ var isTagsNotInExpectation = (x) => isJsonObject(x) && typeof x.count === "number" && Array.isArray(x.ids) && x.ids.every((r) => isJsonObject(r) && typeof r.id === "string" && typeof r.elements === "number");
513
+ var isReasonMap = (x) => isJsonObject(x) && Object.values(x).every((v) => typeof v === "string");
514
+ var isNum = (x) => typeof x === "number" && Number.isFinite(x);
515
+ function isProbeReach(x) {
516
+ return isJsonObject(x) && Array.isArray(x.steps) && x.steps.every(isJsonObject) && typeof x.sha256 === "string" && typeof x.source === "string" && typeof x.url === "string";
517
+ }
518
+ function isPageOverflow(x) {
519
+ return isJsonObject(x) && isJsonObject(x.viewport) && isNum(x.viewport.w) && isNum(x.viewport.h) && isNum(x.scrollWidth) && isNum(x.clientWidth) && typeof x.overflowX === "string" && typeof x.scrollable === "boolean" && Array.isArray(x.offenders) && x.offenders.every((o) => isJsonObject(o) && typeof o.path === "string" && (o.dt === null || typeof o.dt === "string") && isNum(o.right)) && optStr(x.compatMode);
520
+ }
521
+ var BEHAVIOUR_STATUSES = ["pass", "fail", "warn", "not-run", "unsupported"];
522
+ var isBehaviourStatus = (x) => typeof x === "string" && BEHAVIOUR_STATUSES.some((s) => s === x);
523
+ function isBehaviourCheck(x) {
524
+ return isJsonObject(x) && typeof x.id === "string" && isBehaviourStatus(x.status) && typeof x.detail === "string";
525
+ }
526
+ function isMeasuredBehaviour(x) {
527
+ return isJsonObject(x) && x.version === 1 && typeof x.ran === "boolean" && (x.ran ? Array.isArray(x.checks) && x.checks.every(isBehaviourCheck) : typeof x.why === "string");
528
+ }
529
+ function isMeasuredVisual(x) {
530
+ return isJsonObject(x) && x.version === 1 && typeof x.ran === "boolean" && (x.ran ? isNum(x.differingPct) && isNum(x.shiftTolerantPct) && Array.isArray(x.regions) : typeof x.why === "string");
531
+ }
532
+ var MEASURED_EXTRAS = [
533
+ ["probe", isProbeIdentity, "the shipped probe's identity {name, version, sha256, playwright:{package, version}, browser:{name, version}} \u2014 read as probe: unknown"],
534
+ ["frame", isProbeFrame, "a probe frame {nodeId, selector, via, rect}"],
535
+ ["frames", (x) => Array.isArray(x) && x.every(isProbeFrame), "a list of probe frames {nodeId, selector, via, rect}"],
536
+ ["navigation", isNavigation, "a navigation log {events[], afterInitialLoad, reruns}"],
537
+ ["matchedByCensus", isCountMap, "a {rule: count} map"],
538
+ ["notMeasured", Array.isArray, "a list \u2014 the probe's reasons for unmatched nodes are not used"],
539
+ // the run it belongs to and the build it was served
540
+ ["runId", (x) => typeof x === "string" && x !== "", "a run id (string) \u2014 the measurement is tied to no verify run"],
541
+ ["build", isBuildIdentity, "a build identity {url, mode: vite-dev|static|unknown, assets, assetsSha256, gitHead, gitDirty} \u2014 read as build: unknown"],
542
+ // the shipped probe's foreign tags
543
+ ["tagsNotInExpectation", isTagsNotInExpectation, "a foreign-tag list {count, ids: [{id, elements}]}"],
544
+ // the steps replayed, the page's overflow, the behaviour/a11y block
545
+ ["reach", isProbeReach, "the probe's steps {steps[], sha256, source, url}"],
546
+ ["page", isPageOverflow, "a page overflow {viewport:{w,h}, scrollWidth, clientWidth, overflowX, scrollable, offenders[]} \u2014 page overflow not measured"],
547
+ ["behaviour", isMeasuredBehaviour, "a behaviour block {version: 1, ran: true, checks: [{id, status: pass|fail|warn|not-run|unsupported, detail}], \u2026} or {version: 1, ran: false, why} \u2014 behaviour/a11y not reported"],
548
+ // the visual diff (informational)
549
+ ["visual", isMeasuredVisual, "a visual block {version: 1, ran: true, differingPct, shiftTolerantPct, regions: [\u2026], \u2026} or {version: 1, ran: false, why} \u2014 the visual diff not reported"]
550
+ ];
551
+ function isMeasuredCore(x) {
552
+ return isJsonObject(x) && optArrayOf(x.nodes, (n) => isJsonObject(n) && typeof n.nodeId === "string") && optArrayOf(x.components, anyObject) && optArrayOf(x.interactions, anyObject) && (x.artifacts === void 0 || Array.isArray(x.artifacts)) && optStr(x.mode) && optStr(x.expectationSha256);
553
+ }
554
+ function isVerifyMeasured(x) {
555
+ return isMeasuredCore(x) && MEASURED_EXTRAS.every(([k, ok]) => x[k] === void 0 || ok(x[k])) && (x.nodes === void 0 || Array.isArray(x.nodes) && x.nodes.every((n) => !isJsonObject(n) || n.unmeasured === void 0 || isReasonMap(n.unmeasured)));
556
+ }
557
+ isVerifyMeasured.expected = "probe measurements: an object whose `nodes` (each {nodeId, styles}), `components`, `interactions` and `artifacts`, when present, are arrays";
558
+ function isEvidence(x) {
559
+ return isJsonObject(x) && typeof x.nodeId === "string";
560
+ }
561
+ function isInteractionEvidenceList(x) {
562
+ return Array.isArray(x) && x.every(isEvidence);
563
+ }
564
+ isInteractionEvidenceList.expected = "interaction evidence: a JSON array of {nodeId, trigger, ok, selector, selectorCount, detail}";
565
+ function isMeasuredComponentList(x) {
566
+ return Array.isArray(x) && x.every((c) => isJsonObject(c) && optStr(c.setName) && optStr(c.name) && optStr(c.nodeId) && (c.present === void 0 || typeof c.present === "boolean"));
567
+ }
568
+ isMeasuredComponentList.expected = "component evidence: a JSON array of {setName|nodeId, present: true|false}";
569
+ function isVerifyReport(x) {
570
+ return isJsonObject(x) && optStr(x.schema) && optStr(x.verdict) && optStr(x.screen) && optStr(x.nodeId) && optStr(x.headline) && (x.why === void 0 || isStringArray(x.why)) && (x.integrity === void 0 || isStringArray(x.integrity)) && optArrayOf(x.deltas, anyObject) && optObj(x.inputs);
571
+ }
572
+ isVerifyReport.expected = "a verify report (the verify-screen script's --compare output): an object with `verdict`, `why[]`, `deltas[]`, `inputs`";
573
+ var PLAN_ARRAYS = ["files", "tokens", "components", "hidden", "anchorsSuggested", "deviations", "allowedLiterals", "waivers", "descopes"];
574
+ var PLAN_OBJECTS = ["anchors", "verification", "counts"];
575
+ var PLAN_STRINGS = ["schema", "screen", "screenName", "nodeId", "route", "file", "exportedAt", "status"];
576
+ function planProblem(x) {
577
+ if (!isJsonObject(x)) return "is not a plan (the file holds " + (Array.isArray(x) ? "an array" : x === null ? "null" : typeof x) + ", not an object)";
578
+ for (const k of PLAN_ARRAYS) if (x[k] !== void 0 && !Array.isArray(x[k])) return `is not a valid plan: \`${k}\` must be an array`;
579
+ for (const k of PLAN_OBJECTS) if (x[k] !== void 0 && !isJsonObject(x[k])) return `is not a valid plan: \`${k}\` must be an object`;
580
+ for (const k of PLAN_STRINGS) if (x[k] !== void 0 && x[k] !== null && typeof x[k] !== "string") return `is not a valid plan: \`${k}\` must be a string`;
581
+ if (x.files !== void 0 && !isStringArray(x.files)) return "is not a valid plan: `files` must be an array of paths (strings)";
582
+ for (const k of ["tokens", "components", "allowedLiterals", "deviations", "hidden", "anchorsSuggested", "waivers", "descopes"]) {
583
+ const list = x[k];
584
+ if (Array.isArray(list) && !list.every(isJsonObject)) return `is not a valid plan: every \`${k}\` entry must be an object`;
585
+ }
586
+ if (Array.isArray(x.tokens) && !x.tokens.every((t) => isJsonObject(t) && (t.figmaName === null || optStr(t.figmaName)))) return "is not a valid plan: a `tokens` row's `figmaName` must be a string";
587
+ if (Array.isArray(x.components) && !x.components.every((c) => isJsonObject(c) && typeof c.name === "string")) return "is not a valid plan: every `components` row needs its `name`";
588
+ if (isJsonObject(x.anchors) && !Object.values(x.anchors).every(isJsonObject)) return "is not a valid plan: every `anchors` entry must be an object";
589
+ if (x.auditGate !== void 0 && x.auditGate !== null && !isJsonObject(x.auditGate)) return "is not a valid plan: `auditGate` must be an object or null";
590
+ if (x.target !== void 0 && x.target !== null && typeof x.target !== "string" && !isJsonObject(x.target)) return "is not a valid plan: `target` must be a profile name, an object or null";
591
+ if (x.tagging !== void 0 && x.tagging !== null && !(isJsonObject(x.tagging) && (x.tagging.off === void 0 || typeof x.tagging.off === "boolean") && optStr(x.tagging.reason))) return 'is not a valid plan: `tagging` must be {"off": true, "reason": "\u2026"}';
592
+ if (Array.isArray(x.tokens) && !x.tokens.every((t) => isJsonObject(t) && optStr(t.acknowledged))) return "is not a valid plan: a `tokens` row's `acknowledged` must be a string (the reason)";
593
+ if (isJsonObject(x.verification) && x.verification.hook !== void 0 && !isJsonObject(x.verification.hook)) return "is not a valid plan: `verification.hook` must be an object";
594
+ if (isJsonObject(x.verification) && x.verification.recorded !== void 0 && !isJsonObject(x.verification.recorded)) return "is not a valid plan: `verification.recorded` must be an object";
595
+ for (const k of ["navigate", "interactions"]) if (x[k] !== void 0 && !Array.isArray(x[k])) return `is not a valid plan: \`${k}\` must be an array`;
596
+ return null;
597
+ }
598
+ function isPlan(x) {
599
+ return planProblem(x) === null;
600
+ }
601
+ isPlan.expected = "a plan (started by the plan-skeleton script): an object whose files/tokens/components/deviations are arrays of objects and whose anchors/verification are objects";
602
+ var recIsCount = (x) => typeof x === "number" && Number.isFinite(x);
603
+ var recIsStr = (v) => typeof v === "string";
604
+ var recNeed = (ok) => ({ need: true, ok });
605
+ var recOpt = (ok) => ({ need: false, ok });
606
+ var RECORD_COUNTS = Object.fromEntries(["high", "medium", "low", "accepted", "notMeasured", "inferred"].map((k) => [k, recNeed(recIsCount)]));
607
+ var RECORD_DELTA = { nodeId: recNeed(recIsStr), field: recNeed(recIsStr), severity: recNeed((v) => v === "high" || v === "medium"), expected: recOpt(() => true), actual: recOpt(() => true) };
608
+ var RECORD_A11Y = { tool: recNeed(recIsStr), violations: recNeed(recIsCount), warnings: recNeed(recIsCount), report: recNeed(recIsStr) };
609
+ var RECORD_FIELDS = {
610
+ schema: recNeed((v) => v === PLAN_VERIFICATION_RECORD_SCHEMA),
611
+ by: recNeed(recIsStr),
612
+ at: recNeed(recIsStr),
613
+ report: recNeed(recIsStr),
614
+ reportSha256: recNeed(recIsStr),
615
+ verdict: recNeed(recIsStr),
616
+ headline: recNeed(recIsStr),
617
+ behaviourHeadline: recOpt(recIsStr),
618
+ counts: recNeed(isJsonObject),
619
+ mode: recNeed((v) => v === "rendered" || v === "static-only"),
620
+ reason: recOpt(recIsStr),
621
+ renderer: recOpt(recIsStr),
622
+ artifacts: recNeed(isStringArray),
623
+ deltas: recNeed(Array.isArray),
624
+ a11y: recOpt(isJsonObject)
625
+ };
626
+ var RECORD_KEYS = Object.keys(RECORD_FIELDS);
627
+ var reqStr = (v) => typeof v === "string" && v.trim() !== "";
628
+ function isPlanWaiver(x) {
629
+ return isJsonObject(x) && reqStr(x.nodeId) && reqStr(x.field) && x.designed !== void 0 && x.built !== void 0 && reqStr(x.exportContentSha256) && reqStr(x.reason) && reqStr(x.decidedBy) && reqStr(x.decidedAt) && (x.tolerance === void 0 || typeof x.tolerance === "number" && x.tolerance >= 0) && optStr(x.cause);
630
+ }
631
+ isPlanWaiver.expected = "a plan waiver {nodeId, field, designed, built, exportContentSha256, reason, decidedBy, decidedAt, tolerance?, cause?}";
632
+ function isPlanDescope(x) {
633
+ return isJsonObject(x) && reqStr(x.nodeId) && reqStr(x.trigger) && optStr(x.destinationId) && reqStr(x.exportContentSha256) && reqStr(x.reason) && reqStr(x.decidedBy) && reqStr(x.decidedAt);
634
+ }
635
+ isPlanDescope.expected = "a plan descope {nodeId, trigger, destinationId?, exportContentSha256, reason, decidedBy, decidedAt}";
636
+ function isStringRecord(x) {
637
+ return isJsonObject(x) && Object.values(x).every((v) => typeof v === "string");
638
+ }
639
+ isStringRecord.expected = "an object of strings";
640
+
641
+ // design-to-code/cli-args.ts
642
+ import path2 from "node:path";
643
+ import { fileURLToPath } from "node:url";
644
+ import { parseArgs } from "node:util";
645
+ var SELF = fileURLToPath(import.meta.url);
646
+ var shellQuote = (p) => /["$`\\!]/.test(p) ? `'${p.replaceAll("'", `'\\''`)}'` : `"${p}"`;
647
+ var SCRIPT_RUNNER = "bun --no-install --no-env-file";
648
+ var shellArg = (a) => /^[\w@%+=:,./-]+$/.test(a) ? a : shellQuote(a);
649
+ var scriptCmd = (name) => `${SCRIPT_RUNNER} ${shellQuote(path2.join(path2.dirname(SELF), name + path2.extname(SELF)))}`;
650
+ function joinNegativeValues(argv, options) {
651
+ const out = [];
652
+ for (let i = 0; i < argv.length; i++) {
653
+ const tok = argv[i], next = argv[i + 1];
654
+ if (tok === void 0) continue;
655
+ if (tok === "--") {
656
+ out.push(...argv.slice(i));
657
+ break;
658
+ }
659
+ const name = tok.startsWith("--") ? tok.slice(2) : void 0;
660
+ if (name !== void 0 && next !== void 0 && options[name]?.type === "string" && /^-\d/.test(next)) {
661
+ out.push(`${tok}=${next}`);
662
+ i++;
663
+ } else out.push(tok);
664
+ }
665
+ return out;
666
+ }
667
+ function badShortWords(args, options) {
668
+ const shorts = /* @__PURE__ */ new Map();
669
+ for (const o of Object.values(options)) if (o.short) shorts.set(o.short, o.type);
670
+ const bad = [];
671
+ let takesNext = false;
672
+ for (const a of args) {
673
+ if (a === "--") break;
674
+ if (takesNext) {
675
+ takesNext = false;
676
+ continue;
677
+ }
678
+ if (a.startsWith("--")) {
679
+ takesNext = !a.includes("=") && options[a.slice(2)]?.type === "string";
680
+ continue;
681
+ }
682
+ if (!/^-[^-]/.test(a)) continue;
683
+ let ok = true;
684
+ for (const [i, c] of [...a.slice(1)].entries()) {
685
+ const type = shorts.get(c);
686
+ if (type === void 0) {
687
+ ok = false;
688
+ break;
689
+ }
690
+ if (type === "string") {
691
+ takesNext = i === a.length - 2;
692
+ break;
693
+ }
694
+ }
695
+ if (!ok) bad.push(a);
696
+ }
697
+ return bad;
698
+ }
699
+ function cliParse(tool, argv, options, usage, exitCode, parse) {
700
+ const args = joinNegativeValues(argv, options);
701
+ const unknownFlags = () => {
702
+ const { tokens } = parseArgs({ args, options, strict: false, allowPositionals: true, tokens: true });
703
+ const long = tokens.flatMap((t) => t.kind === "option" && t.rawName.startsWith("--") && args[t.index]?.startsWith("--") && !(t.name in options) ? [{ name: t.rawName, at: t.index }] : []);
704
+ const short = badShortWords(args, options);
705
+ const hint = short.some((w) => w.length > 2) ? ' (a value starting with "-": put -- before it, or use --flag=value)' : "";
706
+ const named = [...long, ...short.map((w) => ({ name: w, at: args.indexOf(w) }))].sort((a, b) => a.at - b.at).map((n) => n.name);
707
+ return `${tool}: unknown flag ${[...new Set(named)].join(", ")}${hint}
708
+ ${usage}`;
709
+ };
710
+ let failure;
711
+ try {
712
+ const parsed = parse(args);
713
+ if (!badShortWords(args, options).length) return parsed;
714
+ failure = unknownFlags();
715
+ } catch (e) {
716
+ const code = errCode(e);
717
+ if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
718
+ failure = unknownFlags();
719
+ } else if (code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") {
720
+ const msg = e instanceof Error ? e.message : "";
721
+ const names = /Option '([^']*)'/.exec(msg)?.[1]?.match(/--?[\w-]+/g) ?? [];
722
+ const typed = names.find((n) => args.some((a) => a === n || a.startsWith(n + "="))) ?? names.at(-1);
723
+ failure = `${tool}: ${typed ?? "an option"} ${/does not take an argument/.test(msg) ? "takes no value" : "needs a value"}
724
+ ${usage}`;
725
+ } else {
726
+ failure = `${tool}: ${errMsg(e)}
727
+ ${usage}`;
728
+ }
729
+ }
730
+ console.error(failure);
731
+ exitCli(exitCode);
732
+ }
733
+
734
+ // design-to-code/drift-lint.ts
735
+ import { parseArgs as parseArgs2 } from "node:util";
736
+
737
+ // design-to-code/export-shape.ts
738
+ function isIrNode(x) {
739
+ return isJsonObject(x) && typeof x.id === "string" && typeof x.type === "string" && (x.children === void 0 || Array.isArray(x.children));
740
+ }
741
+ function isScreenExport(x) {
742
+ return isJsonObject(x) && Array.isArray(x.nodes) && x.nodes.every(isIrNode);
743
+ }
744
+ function isLayerFile(x) {
745
+ return isJsonObject(x) && !Array.isArray(x.nodes) && isIrNode(x.tree);
746
+ }
747
+ function isScreenDoc(x) {
748
+ return isScreenExport(x) || isLayerFile(x) || isIrNode(x);
749
+ }
750
+ isScreenDoc.expected = "a screen export: {nodes:[\u2026]} whose every node has a string id and type, a layer file {tree: node}, or a bare node {id, type, \u2026}";
751
+ function screenRoots(doc) {
752
+ if (isScreenExport(doc)) return doc.nodes;
753
+ if (isLayerFile(doc)) return [doc.tree];
754
+ if (isIrNode(doc)) return [doc];
755
+ return [];
756
+ }
757
+
758
+ // design-to-code/map-util.ts
759
+ function getOrInit(m, k, init) {
760
+ const have = m.get(k);
761
+ if (have !== void 0) return have;
762
+ const made = init();
763
+ m.set(k, made);
764
+ return made;
765
+ }
766
+
767
+ // design-to-code/component-match.ts
768
+ function visibleInstances(doc, label) {
769
+ const out = [];
770
+ const visit = (n) => {
771
+ if (n.type === "INSTANCE" && n.mainComponent) {
772
+ const mc = n.mainComponent;
773
+ out.push({
774
+ screen: label,
775
+ nodeId: n.id,
776
+ layer: n.name,
777
+ name: mc.setName || mc.name || n.name,
778
+ ...ifDefined("key", mc.key),
779
+ ...ifDefined("setKey", mc.setKey),
780
+ remote: mc.remote === true,
781
+ variant: parseVariant(n.component) || parseVariant(mc.variant),
782
+ props: n.props && typeof n.props === "object" ? n.props : {}
783
+ });
784
+ }
785
+ };
786
+ for (const r of screenRoots(doc)) walkVisible(r, visit);
787
+ return out;
788
+ }
789
+ function parseVariant(s) {
790
+ if (isJsonObject(s)) {
791
+ const out2 = Object.fromEntries(Object.entries(s).filter((e) => typeof e[1] === "string"));
792
+ return Object.keys(out2).length ? out2 : null;
793
+ }
794
+ if (!s || typeof s !== "string" || !s.includes("=")) return null;
795
+ const out = {};
796
+ for (const part of s.split(/,\s*(?=[^,=]+=)/)) {
797
+ const i = part.indexOf("=");
798
+ if (i < 0) continue;
799
+ out[part.slice(0, i).trim()] = part.slice(i + 1).trim();
800
+ }
801
+ return Object.keys(out).length ? out : null;
802
+ }
803
+ var baseProp = (p) => String(p).split("#")[0] ?? "";
804
+ var SHARED_PAGE = /shared|style ?guide|foundation|core|global|design.?system|librar|banner|badge/i;
805
+ function signature(inst, cand) {
806
+ const props = {};
807
+ for (const [k, v] of Object.entries(cand.props || {})) props[baseProp(k)] = v;
808
+ const reasons = [];
809
+ let score = 40, verified = true;
810
+ reasons.push("name matches catalog entry verbatim");
811
+ const variant = inst.variant;
812
+ if (variant) {
813
+ const axes = Object.keys(variant);
814
+ const known = axes.filter((k) => props[k] && props[k].type === "VARIANT");
815
+ if (known.length === axes.length) {
816
+ score += 25;
817
+ reasons.push(`all ${axes.length} variant prop name(s) exist as VARIANT props`);
818
+ } else {
819
+ verified = false;
820
+ score += known.length ? 10 : -15;
821
+ reasons.push(known.length ? `${known.length}/${axes.length} variant prop names exist` : "variant prop names are NOT on this candidate");
822
+ }
823
+ const bad = Object.entries(variant).filter(([k, val]) => {
824
+ const d = props[k];
825
+ return d && d.type === "VARIANT" && !(d.options || []).includes(val);
826
+ }).map(([k]) => k);
827
+ if (known.length && !bad.length) {
828
+ score += 25;
829
+ reasons.push(`variant value(s) ${known.map((k) => `${k}=${variant[k]}`).join(", ")} are in the option list`);
830
+ } else if (bad.length) {
831
+ verified = false;
832
+ score -= 20;
833
+ reasons.push(`variant value(s) not in option list (${bad.map((k) => `${k}=${variant[k]}`).join(", ")})`);
834
+ }
835
+ } else if (cand.type === "COMPONENT") {
836
+ score += 15;
837
+ reasons.push("instance has no variant string and the catalog entry is a plain COMPONENT");
838
+ } else {
839
+ verified = false;
840
+ reasons.push(`instance sets no variant but the catalog entry is a ${cand.type}`);
841
+ }
842
+ const own = Object.keys(inst.props || {}).filter((k) => !variant || !(baseProp(k) in variant));
843
+ if (own.length) {
844
+ const typed = own.filter((k) => {
845
+ const d = props[baseProp(k)], v = inst.props[k];
846
+ if (!d) return false;
847
+ if (typeof v === "boolean") return d.type === "BOOLEAN";
848
+ return d.type === "TEXT" || d.type === "INSTANCE_SWAP" || d.type === "VARIANT";
849
+ });
850
+ if (typed.length === own.length) {
851
+ score += 15;
852
+ reasons.push(`all ${own.length} non-variant prop name(s) exist on the definition`);
853
+ } else {
854
+ verified = false;
855
+ score += typed.length ? 5 : -10;
856
+ reasons.push(typed.length ? `${typed.length}/${own.length} non-variant prop names exist with a matching type` : "none of the instance prop names exist on this definition");
857
+ }
858
+ }
859
+ if (SHARED_PAGE.test(String(cand.page || ""))) {
860
+ score += 5;
861
+ reasons.push(`lives on a shared page (${cand.page})`);
862
+ }
863
+ return { verified, score, reasons };
864
+ }
865
+ var sigOf = (c) => JSON.stringify(Object.entries(c.props || {}).map(([k, v]) => [baseProp(k), v.type, v.options || null]).sort());
866
+ function matchByNameAndSignature(instances, catalog, library) {
867
+ const comps = catalog && catalog.components || [];
868
+ const byName = /* @__PURE__ */ new Map();
869
+ comps.forEach((c, i) => {
870
+ getOrInit(byName, c.name, () => []).push(Object.assign({ _order: i }, c));
871
+ });
872
+ const catKeys = new Set(comps.map((c) => c.key).filter((k) => !!k));
873
+ const libKeys = new Set((library && library.components || []).map((c) => c.key).filter((k) => !!k));
874
+ const libNames = new Set((library && library.components || []).map((c) => c.name));
875
+ const groups = /* @__PURE__ */ new Map();
876
+ for (const inst of instances) {
877
+ const group = groups.get(inst.name);
878
+ if (group) group.push(inst);
879
+ else groups.set(inst.name, [inst]);
880
+ }
881
+ const rows = [];
882
+ for (const [name, list] of groups) {
883
+ const byKey = list.some((i) => catKeys.has(i.key ?? "") || catKeys.has(i.setKey ?? ""));
884
+ const row = {
885
+ name,
886
+ instances: list.length,
887
+ nodeIds: list.map((i) => i.nodeId),
888
+ screens: [...new Set(list.map((i) => i.screen).filter((s) => !!s))],
889
+ instanceKeys: [...new Set(list.map((i) => i.setKey || i.key).filter((k) => !!k))],
890
+ remote: list.every((i) => i.remote),
891
+ byKey,
892
+ match: null,
893
+ alternatives: [],
894
+ reasons: []
895
+ };
896
+ const cands = byName.get(name) || [];
897
+ if (!cands.length) {
898
+ const inLibrary = list.some((i) => libKeys.has(i.key ?? "") || libKeys.has(i.setKey ?? ""));
899
+ row.reasons.push(`no catalog entry named ${JSON.stringify(name)}`);
900
+ row.reasons.push(inLibrary ? "its key IS in components.library.json \u2014 a third-party library both files consume, not the design system's own component" : libNames.has(name) ? "the name appears in components.library.json \u2014 a third-party library the design system consumes, not one of its own components" : "not a component of the exported design system (typically an external icon library) \u2014 expected, not a miss");
901
+ rows.push(row);
902
+ continue;
903
+ }
904
+ if (cands.length > 1) row.candidates = cands.map((c) => ({ ...ifDefined("id", c.id), ...ifDefined("key", c.key), name: c.name }));
905
+ const [firstInst, ...otherInsts] = list;
906
+ const scored = cands.map((c) => {
907
+ const first = signature(firstInst, c);
908
+ const per = [first, ...otherInsts.map((i) => signature(i, c))];
909
+ return { c, verified: per.every((p) => p.verified), score: Math.min(...per.map((p) => p.score)), reasons: first.reasons, failing: per.find((p) => !p.verified) };
910
+ }).sort((a, b) => Number(b.verified) - Number(a.verified) || b.score - a.score || a.c._order - b.c._order);
911
+ const best = scored[0];
912
+ if (best === void 0) throw new Error("component-match: a non-empty candidate list scored to nothing");
913
+ if (!best.verified) {
914
+ row.reasons.push(`${cands.length} catalog entr${cands.length === 1 ? "y is" : "ies are"} named ${JSON.stringify(name)}, but no prop signature agrees: ` + (best.failing ? best.failing.reasons.filter((r) => /NOT|not in|none of|\d+\/\d+|sets no variant/.test(r)).join("; ") : "signature mismatch") + " \u2014 a name alone is not a match");
915
+ rows.push(row);
916
+ continue;
917
+ }
918
+ const verifiedOnes = scored.filter((s) => s.verified);
919
+ const runnerUp = verifiedOnes[1];
920
+ const ties = verifiedOnes.filter((s) => s.score === best.score && s !== best);
921
+ row.match = { ...ifDefined("id", best.c.id), ...ifDefined("key", best.c.key), name: best.c.name, type: best.c.type, ...ifDefined("page", best.c.page) };
922
+ row.evidence = list.some((i) => i.variant || Object.keys(i.props || {}).length) ? "name+signature" : "name+no-props";
923
+ row.reasons = best.reasons.slice();
924
+ if (ties.length) {
925
+ const identical = ties.every((t) => sigOf(t.c) === sigOf(best.c));
926
+ row.reasons.push(identical ? `${ties.length + 1} catalog entries named "${name}" score identically \u2014 they are duplicates of one definition (same props, same variants); the first in the catalog is used` : `${ties.length + 1} catalog entries named "${name}" score identically with DIFFERENT signatures \u2014 the first in the catalog is used; confirm which one`);
927
+ row.tie = identical ? "duplicate-definitions" : "different-signatures";
928
+ } else if (scored.length > 1) {
929
+ row.reasons.push(`beat ${scored.length - 1} same-named candidate(s)${runnerUp ? ` by ${best.score - runnerUp.score} pts` : " (their prop signatures do not agree)"}`);
930
+ }
931
+ row.alternatives = verifiedOnes.filter((s) => s !== best).map((s) => ({ ...ifDefined("id", s.c.id), ...ifDefined("key", s.c.key), ...ifDefined("page", s.c.page), score: s.score }));
932
+ row.reasons.push("key lookup: " + (byKey ? "matched" : "NO MATCH (the instance's key is not in the catalog \u2014 re-keyed)"));
933
+ rows.push(row);
934
+ }
935
+ const proposals = rows.filter((r) => !!r.match && !r.byKey);
936
+ return {
937
+ rows,
938
+ proposals,
939
+ summary: {
940
+ instances: instances.length,
941
+ names: rows.length,
942
+ byKey: rows.filter((r) => r.byKey).length,
943
+ proposed: proposals.length,
944
+ proposedWithSignature: proposals.filter((r) => r.evidence === "name+signature").length,
945
+ withCandidates: rows.filter((r) => (byName.get(r.name) || []).length).length,
946
+ unmatched: rows.filter((r) => !r.match).length,
947
+ remote: instances.filter((i) => i.remote).length
948
+ }
949
+ };
950
+ }
951
+ var REKEY_MIN_PROPOSALS = 3;
952
+ var REKEY_MIN_SHARE = 0.5;
953
+ function isRekeyed(result) {
954
+ const s = result.summary;
955
+ return s.names > 0 && s.byKey / s.names <= 0.05 && s.proposedWithSignature >= REKEY_MIN_PROPOSALS && s.withCandidates > 0 && s.proposedWithSignature / s.withCandidates >= REKEY_MIN_SHARE;
956
+ }
957
+
958
+ // design-to-code/map-validate-core.ts
959
+ var STATUSES = ["active", "deprecated", "needs-review"];
960
+ var KEYS = {
961
+ root: ["version", "figmaFileKey", "components"],
962
+ entry: ["figma", "code", "props", "variantOverrides", "childrenByLayer", "status", "note"],
963
+ figma: ["key", "id", "name", "unstable"],
964
+ code: ["module", "export", "targets"],
965
+ target: ["module", "export"],
966
+ vo: ["when", "code"],
967
+ voCode: ["module", "export"],
968
+ children: ["layerNamePattern", "slot"],
969
+ prop: Object.assign(nullProto(), {
970
+ enum: ["kind", "codeProp", "values", "default", "omitDefault"],
971
+ boolean: ["kind", "codeProp", "default", "omitDefault"],
972
+ string: ["kind", "codeProp"],
973
+ instance: ["kind", "codeProp", "slot"]
974
+ })
975
+ };
976
+ var PROP_KINDS = Object.keys(KEYS.prop);
977
+ var isStr = (v) => typeof v === "string";
978
+ var isBool = (v) => typeof v === "boolean";
979
+ var noExtra = (obj, allowed, at, err) => {
980
+ if (!Array.isArray(allowed)) return;
981
+ for (const k of Object.keys(obj)) if (!allowed.includes(k)) err(`${at}.${k}`, "unknown property (additionalProperties:false)");
982
+ };
983
+ var optStrings = (obj, keys, at, err) => {
984
+ for (const k of keys) if (obj[k] !== void 0 && !isStr(obj[k])) err(`${at}.${k}`, "must be a string");
985
+ };
986
+ function validateMap(map) {
987
+ const errors = [];
988
+ const err = (path4, message) => errors.push({ path: path4, message });
989
+ if (!isRecord(map)) return { ok: false, errors: [{ path: "", message: "map must be an object" }] };
990
+ noExtra(map, KEYS.root, "", err);
991
+ if (map.version !== 1) err("version", "must be 1");
992
+ if (map.figmaFileKey !== void 0 && !isStr(map.figmaFileKey)) err("figmaFileKey", "must be a string");
993
+ if (!isRecord(map.components)) {
994
+ err("components", "must be an object");
995
+ return { ok: false, errors };
996
+ }
997
+ for (const key of Object.keys(map.components)) {
998
+ const e = map.components[key];
999
+ const at = `components.${key}`;
1000
+ if (!isRecord(e)) {
1001
+ err(at, "entry must be an object");
1002
+ continue;
1003
+ }
1004
+ noExtra(e, KEYS.entry, at, err);
1005
+ if (!isRecord(e.figma)) err(`${at}.figma`, "required object");
1006
+ else {
1007
+ noExtra(e.figma, KEYS.figma, `${at}.figma`, err);
1008
+ if (!isStr(e.figma.name)) err(`${at}.figma.name`, "required string");
1009
+ optStrings(e.figma, ["key", "id"], `${at}.figma`, err);
1010
+ if (e.figma.unstable !== void 0 && !isBool(e.figma.unstable)) err(`${at}.figma.unstable`, "must be a boolean");
1011
+ }
1012
+ if (!isRecord(e.code)) err(`${at}.code`, "required object");
1013
+ else {
1014
+ noExtra(e.code, KEYS.code, `${at}.code`, err);
1015
+ if (!isStr(e.code.module)) err(`${at}.code.module`, "required string");
1016
+ if (!isStr(e.code.export)) err(`${at}.code.export`, "required string");
1017
+ if (e.code.targets !== void 0) {
1018
+ if (!isRecord(e.code.targets)) err(`${at}.code.targets`, "must be an object");
1019
+ else for (const t of Object.keys(e.code.targets)) {
1020
+ const tv = e.code.targets[t], ta = `${at}.code.targets.${t}`;
1021
+ if (!isRecord(tv)) err(ta, "must be an object");
1022
+ else {
1023
+ noExtra(tv, KEYS.target, ta, err);
1024
+ optStrings(tv, ["module", "export"], ta, err);
1025
+ }
1026
+ }
1027
+ }
1028
+ }
1029
+ if (e.status !== void 0 && (!isStr(e.status) || !STATUSES.includes(e.status))) err(`${at}.status`, `must be one of ${STATUSES.join("|")}`);
1030
+ optStrings(e, ["note"], at, err);
1031
+ if (e.props !== void 0) {
1032
+ if (!isRecord(e.props)) err(`${at}.props`, "must be an object");
1033
+ else for (const pn of Object.keys(e.props)) validateProp(e.props[pn], `${at}.props.${pn}`, err);
1034
+ }
1035
+ if (e.variantOverrides !== void 0) {
1036
+ if (!Array.isArray(e.variantOverrides)) err(`${at}.variantOverrides`, "must be an array");
1037
+ else e.variantOverrides.forEach((vo, i) => {
1038
+ const va = `${at}.variantOverrides[${i}]`;
1039
+ if (!isRecord(vo)) {
1040
+ err(va, "must be an object");
1041
+ return;
1042
+ }
1043
+ noExtra(vo, KEYS.vo, va, err);
1044
+ if (!isRecord(vo.when)) err(`${va}.when`, "required object");
1045
+ else for (const wk of Object.keys(vo.when)) if (!isStr(vo.when[wk])) err(`${va}.when.${wk}`, "value must be a string");
1046
+ if (!isRecord(vo.code)) err(`${va}.code`, "required object");
1047
+ else {
1048
+ noExtra(vo.code, KEYS.voCode, `${va}.code`, err);
1049
+ if (!isStr(vo.code.module)) err(`${va}.code.module`, "required string");
1050
+ if (!isStr(vo.code.export)) err(`${va}.code.export`, "required string");
1051
+ }
1052
+ });
1053
+ }
1054
+ if (e.childrenByLayer !== void 0) {
1055
+ if (!isRecord(e.childrenByLayer)) err(`${at}.childrenByLayer`, "must be an object");
1056
+ else {
1057
+ noExtra(e.childrenByLayer, KEYS.children, `${at}.childrenByLayer`, err);
1058
+ optStrings(e.childrenByLayer, ["layerNamePattern", "slot"], `${at}.childrenByLayer`, err);
1059
+ }
1060
+ }
1061
+ }
1062
+ return { ok: errors.length === 0, errors };
1063
+ }
1064
+ function validateProp(p, at, err) {
1065
+ if (!isRecord(p)) {
1066
+ err(at, "prop must be an object");
1067
+ return;
1068
+ }
1069
+ const allowed = typeof p.kind === "string" ? KEYS.prop[p.kind] : void 0;
1070
+ if (!Array.isArray(allowed)) {
1071
+ err(`${at}.kind`, `must be one of ${PROP_KINDS.join("|")}`);
1072
+ return;
1073
+ }
1074
+ noExtra(p, allowed, at, err);
1075
+ if (p.kind !== "instance" && !isStr(p.codeProp)) err(`${at}.codeProp`, "required string");
1076
+ if (p.kind === "instance" && p.codeProp !== void 0 && !isStr(p.codeProp)) err(`${at}.codeProp`, "must be a string");
1077
+ if (p.kind === "instance" && p.slot !== void 0 && !isStr(p.slot)) err(`${at}.slot`, "must be a string");
1078
+ if (p.kind === "enum") {
1079
+ if (p.values !== void 0 && !isRecord(p.values)) err(`${at}.values`, "must be an object (VARIANT option -> code value)");
1080
+ else if (isRecord(p.values)) for (const k of Object.keys(p.values)) {
1081
+ const t = typeof p.values[k];
1082
+ if (p.values[k] !== null && !["string", "number", "boolean"].includes(t)) err(`${at}.values.${k}`, "value must be string|number|boolean|null");
1083
+ }
1084
+ if (p.default !== void 0 && !["string", "number", "boolean"].includes(typeof p.default)) err(`${at}.default`, "must be string|number|boolean");
1085
+ }
1086
+ if (p.kind === "boolean" && p.default !== void 0 && !isBool(p.default)) err(`${at}.default`, "must be a boolean");
1087
+ if ((p.kind === "enum" || p.kind === "boolean") && p.omitDefault !== void 0 && !isBool(p.omitDefault)) err(`${at}.omitDefault`, "must be a boolean");
1088
+ }
1089
+ function isCodeConnectMap(x) {
1090
+ return validateMap(x).ok;
1091
+ }
1092
+ isCodeConnectMap.expected = "a valid component map (the map-validate script lists what is wrong with it)";
1093
+
1094
+ // design-to-code/design-system-dir.ts
1095
+ import fs2 from "node:fs";
1096
+ import path3 from "node:path";
1097
+
1098
+ // bridge/src/library-layout.ts
1099
+ var ROOT = "libraries";
1100
+ var INDEX = "index.json";
1101
+ var { TOKENS: TOKENS2, HYGIENE: HYGIENE2 } = DESIGN_SYSTEM_FILES;
1102
+ var COMPONENTS = "components.json";
1103
+ var LIBRARY_FILE_NAMES = [TOKENS2, ...STYLE_KINDS.map((row) => row.file), COMPONENTS, HYGIENE2, INDEX];
1104
+
1105
+ // design-to-code/design-system-dir.ts
1106
+ var { TOKENS: TOKENS3, COMPONENTS_LOCAL: COMPONENTS_LOCAL2, COMPONENTS_LIBRARY: COMPONENTS_LIBRARY2 } = DESIGN_SYSTEM_FILES;
1107
+ var STYLES_TEXT = STYLE_KIND_ROWS.text.file;
1108
+ function exportRootOf(screenFile, dsDir) {
1109
+ const roots = [];
1110
+ if (screenFile) {
1111
+ const pages = path3.dirname(path3.dirname(screenFile));
1112
+ if (path3.basename(pages) === "pages") roots.push(path3.dirname(pages));
1113
+ }
1114
+ if (dsDir) {
1115
+ const parent = path3.dirname(path3.normalize(dsDir));
1116
+ roots.push(path3.basename(parent) === ROOT ? path3.dirname(parent) : parent);
1117
+ }
1118
+ return roots.find((r) => fs2.existsSync(path3.join(r, ROOT, INDEX))) ?? null;
1119
+ }
1120
+ function findLibraryExports(screenFile, dsDir) {
1121
+ const root = exportRootOf(screenFile, dsDir);
1122
+ if (!root) return [];
1123
+ const index = readJsonOrNull(path3.join(root, ROOT, INDEX), isLibrariesIndex);
1124
+ if (!index) return [];
1125
+ return index.libraries.filter((r) => r.dir && r.dir !== "." && r.dir !== ".." && !/[\\/]/.test(r.dir)).map((r) => {
1126
+ const rel = path3.join(root, ROOT, r.dir).split(path3.sep).join("/");
1127
+ return { rel, name: r.libraryName || r.dir, collectionKeys: r.collectionKeys || [], components: readJsonOrNull(path3.join(rel, COMPONENTS), isComponentsCatalog) };
1128
+ });
1129
+ }
1130
+ function discoverCatalogs(namedFile, screenFile) {
1131
+ const dir = path3.dirname(namedFile);
1132
+ const out = [];
1133
+ const seen = /* @__PURE__ */ new Set([path3.resolve(namedFile)]);
1134
+ const add = (file, role, catalog) => {
1135
+ if (seen.has(path3.resolve(file)) || !fs2.existsSync(file)) return;
1136
+ seen.add(path3.resolve(file));
1137
+ out.push({ file, role, catalog });
1138
+ };
1139
+ for (const lib of findLibraryExports(screenFile, dir)) add(path3.join(lib.rel, COMPONENTS), "library", lib.components);
1140
+ const sample = path3.join(dir, COMPONENTS_LIBRARY2);
1141
+ add(sample, "library-sample", readJsonOrNull(sample, isComponentsCatalog));
1142
+ return out;
1143
+ }
1144
+ function loadCatalogSet(namedFile, named, extraFiles, screenFile) {
1145
+ const sources = [{ file: namedFile, catalog: named, role: "named" }];
1146
+ const skipped = [];
1147
+ const seen = /* @__PURE__ */ new Set([path3.resolve(namedFile)]);
1148
+ for (const f of extraFiles) {
1149
+ if (seen.has(path3.resolve(f))) continue;
1150
+ seen.add(path3.resolve(f));
1151
+ const r = loadSplitFile(f, "component catalog (--catalog)", isComponentsCatalog, "components", "design-system/components.library.json");
1152
+ if ("error" in r) return r;
1153
+ sources.push({ file: f, role: "extra", catalog: r.doc });
1154
+ }
1155
+ for (const d of discoverCatalogs(namedFile, screenFile)) {
1156
+ if (seen.has(path3.resolve(d.file))) continue;
1157
+ seen.add(path3.resolve(d.file));
1158
+ if (d.catalog) sources.push({ file: d.file, role: d.role, catalog: d.catalog });
1159
+ else skipped.push(d.file);
1160
+ }
1161
+ return { sources, skipped };
1162
+ }
1163
+ function unionCatalog(sources) {
1164
+ const first = sources[0];
1165
+ const keys = /* @__PURE__ */ new Set();
1166
+ const components = [];
1167
+ for (const s of sources) for (const c of s.catalog.components) {
1168
+ if (c.key) {
1169
+ if (keys.has(c.key)) continue;
1170
+ keys.add(c.key);
1171
+ }
1172
+ components.push(c);
1173
+ }
1174
+ return { ...first ? first.catalog : {}, components };
1175
+ }
1176
+ var catalogSetLine = (sources) => `catalogs read (${sources.length}): ` + sources.map((s) => `${s.file} [${s.role}, ${s.catalog.components.length} component(s)]`).join(", ");
1177
+
1178
+ // design-to-code/drift-lint.ts
1179
+ var stripSuffix = (k) => String(k).split("#")[0] ?? "";
1180
+ var DEFAULT_MAX_AGE_MS = STALE_MS;
1181
+ function checkFreshness(catalog, push, warnings, opts = {}) {
1182
+ const maxAgeMs = opts.maxAgeMs !== void 0 && opts.maxAgeMs > 0 ? opts.maxAgeMs : DEFAULT_MAX_AGE_MS;
1183
+ const { exportedAt, ageMs, problem } = snapshotAge(catalog, opts.now || Date.now());
1184
+ if (problem === "missing") {
1185
+ const w = { code: "unknown-freshness", message: "design-system.json has no `exportedAt` timestamp \u2014 freshness cannot be verified. Findings are checked against whatever snapshot is on disk, which may be stale; re-export with the current plugin to get a timestamp." };
1186
+ push(warnings, w.code, w.message, {});
1187
+ return w;
1188
+ }
1189
+ if (problem === "unparseable") {
1190
+ const w = { code: "unknown-freshness", message: `design-system.json's \`exportedAt\` ('${exportedAt}') is not a parseable timestamp \u2014 freshness cannot be verified.` };
1191
+ push(warnings, w.code, w.message, { ...ifDefined("exportedAt", exportedAt) });
1192
+ return w;
1193
+ }
1194
+ if (typeof ageMs === "number" && ageMs > maxAgeMs) {
1195
+ const ageH = (ageMs / 36e5).toFixed(1);
1196
+ const maxH = (maxAgeMs / 36e5).toFixed(1);
1197
+ const w = {
1198
+ code: "stale-snapshot",
1199
+ message: `STALE SNAPSHOT: design-system.json was exported ${ageH}h ago (max-age ${maxH}h)${catalog && catalog.file ? ` from '${catalog.file}'` : ""}. Every finding below is checked against that on-disk snapshot, NOT the live Figma file \u2014 re-run dtwin / the export tool before trusting them.`
1200
+ };
1201
+ push(warnings, w.code, w.message, { ...ifDefined("exportedAt", exportedAt), ageMs, maxAgeMs });
1202
+ return w;
1203
+ }
1204
+ return void 0;
1205
+ }
1206
+ function checkExtraFreshness(x, push, warnings, opts) {
1207
+ const maxAgeMs = opts.maxAgeMs !== void 0 && opts.maxAgeMs > 0 ? opts.maxAgeMs : DEFAULT_MAX_AGE_MS;
1208
+ const { exportedAt, ageMs, problem } = snapshotAge(x.catalog, opts.now || Date.now());
1209
+ if (problem) push(warnings, "unknown-freshness", `${x.file} has ${problem === "missing" ? "no `exportedAt` timestamp" : `an unparseable \`exportedAt\` ('${exportedAt}')`} \u2014 map entries resolved in it may be stale.`, { ...ifDefined("exportedAt", exportedAt) });
1210
+ else if (typeof ageMs === "number" && ageMs > maxAgeMs) push(warnings, "stale-snapshot", `${x.file} was exported ${(ageMs / 36e5).toFixed(1)}h ago (max-age ${(maxAgeMs / 36e5).toFixed(1)}h) \u2014 map entries resolved in it are checked against that snapshot.`, { ...ifDefined("exportedAt", exportedAt), ageMs, maxAgeMs });
1211
+ }
1212
+ function indexPropsByBase(rawProps, valKey, subject, push, warnings, mapKey) {
1213
+ const ambiguous = /* @__PURE__ */ new Set(), out = nullProto();
1214
+ for (const k of Object.keys(rawProps || {})) {
1215
+ const b = stripSuffix(k);
1216
+ const prev = out[b];
1217
+ if (prev !== void 0 && prev.orig !== k) {
1218
+ push(warnings, "ambiguous-prop", `${subject} has two props with base name '${b}' ('${prev.orig}', '${k}') \u2014 comparison skipped`, { mapKey, prop: b });
1219
+ ambiguous.add(b);
1220
+ }
1221
+ out[b] = { orig: k, [valKey]: (rawProps || {})[k] };
1222
+ }
1223
+ return { props: out, ambiguous };
1224
+ }
1225
+ function driftLint(map, catalog, opts) {
1226
+ const errors = [], warnings = [];
1227
+ const push = (arr, code, message, extra) => arr.push(Object.assign({ code, message }, extra || {}));
1228
+ const freshness = checkFreshness(catalog, push, warnings, opts || {});
1229
+ const extras = opts && opts.extraCatalogs || [];
1230
+ for (const x of extras) checkExtraFreshness(x, push, warnings, opts || {});
1231
+ const comps = catalog && catalog.components || [];
1232
+ const byKey = /* @__PURE__ */ new Map(), byId = /* @__PURE__ */ new Map(), byName = /* @__PURE__ */ new Map();
1233
+ const index = (list, primary, file) => {
1234
+ const own = /* @__PURE__ */ new Map(), claimed = /* @__PURE__ */ new Set();
1235
+ for (const c of list) {
1236
+ if (c.key) {
1237
+ const dup = own.get(c.key);
1238
+ if (dup) push(warnings, "duplicate-key", `two catalog components share key '${c.key}' ('${dup.name}' and '${c.name}')${file ? ` in ${file}` : ""}`, { key: c.key });
1239
+ own.set(c.key, c);
1240
+ if (!byKey.has(c.key) || claimed.has(c.key)) {
1241
+ byKey.set(c.key, c);
1242
+ claimed.add(c.key);
1243
+ }
1244
+ }
1245
+ if (c.id && primary) byId.set(c.id, c);
1246
+ if (c.name) getOrInit(byName, c.name, () => []).push(c);
1247
+ }
1248
+ };
1249
+ index(comps, true);
1250
+ for (const x of extras) index(x.catalog.components || [], false, x.file);
1251
+ const inNamed = new Set(comps);
1252
+ const mappedIds = /* @__PURE__ */ new Set();
1253
+ const compToEntries = /* @__PURE__ */ new Map();
1254
+ const entries = map && map.components || {};
1255
+ for (const [mapKey, e] of Object.entries(entries)) {
1256
+ const f = e.figma || {};
1257
+ let comp;
1258
+ if (f.key) comp = byKey.get(f.key);
1259
+ else if (f.id) comp = byId.get(f.id);
1260
+ else comp = byKey.get(mapKey) || byId.get(mapKey);
1261
+ if (!comp) {
1262
+ const sameName = f.name && byName.get(f.name);
1263
+ const onlyNamed = sameName && sameName.length === 1 ? sameName[0] : void 0;
1264
+ const suggestedKey = onlyNamed ? onlyNamed.key : void 0;
1265
+ push(
1266
+ errors,
1267
+ "orphaned-entry",
1268
+ `map entry '${mapKey}' (was '${f.name || "?"}'${f.id ? ", id " + f.id : ""}) has no matching component in Figma \u2014 deleted, moved, or unpublished${suggestedKey ? ` (a component named '${f.name}' exists with key ${suggestedKey} \u2014 verify before re-pointing)` : ""}`,
1269
+ { mapKey, ...ifDefined("suggestedKey", suggestedKey) }
1270
+ );
1271
+ continue;
1272
+ }
1273
+ if (comp.key) mappedIds.add(comp.key);
1274
+ if (comp.id && inNamed.has(comp)) mappedIds.add(comp.id);
1275
+ getOrInit(compToEntries, comp, () => []).push(mapKey);
1276
+ if (f.name && comp.name && f.name !== comp.name) {
1277
+ push(warnings, "stale-name", `map entry '${mapKey}' remembers name '${f.name}' but component is now '${comp.name}' (rename \u2014 refresh advisory metadata)`, { mapKey, from: f.name, to: comp.name });
1278
+ }
1279
+ const { props: catProps, ambiguous: catAmbiguous } = indexPropsByBase(comp.props, "def", `component '${comp.name}'`, push, warnings, mapKey);
1280
+ const { props: mapProps, ambiguous: mapAmbiguous } = indexPropsByBase(e.props, "prop", `map entry '${mapKey}'`, push, warnings, mapKey);
1281
+ for (const [pn, mp] of Object.entries(mapProps)) if (!(pn in catProps)) push(errors, "stale-prop", `'${mapKey}'.props.${mp.orig} does not exist on the Figma component`, { mapKey, prop: pn });
1282
+ for (const [pn, cp] of Object.entries(catProps)) if (!(pn in mapProps)) push(warnings, "uncovered-prop", `'${mapKey}': Figma prop '${cp.orig}' (${cp.def.type}) is not mapped`, { mapKey, prop: pn });
1283
+ for (const [pn, mp] of Object.entries(mapProps)) {
1284
+ const cp = catProps[pn];
1285
+ if (!cp || catAmbiguous.has(pn) || mapAmbiguous.has(pn)) continue;
1286
+ const prop = mp.prop;
1287
+ const kind = prop.kind, expected = TYPE_TO_KIND[cp.def.type];
1288
+ if (expected && kind && kind !== expected) {
1289
+ push(errors, "kind-mismatch", `'${mapKey}'.props.${mp.orig}: map kind '${kind}' but Figma type is ${cp.def.type} (expected '${expected}')`, { mapKey, prop: pn });
1290
+ continue;
1291
+ }
1292
+ if (prop.kind === "enum") {
1293
+ const values = prop.values || {};
1294
+ if (!Array.isArray(cp.def.options)) {
1295
+ push(warnings, "no-variant-options", `'${mapKey}'.props.${mp.orig}: component exposes no variant options \u2014 enum values can't be verified`, { mapKey, prop: pn });
1296
+ continue;
1297
+ }
1298
+ for (const opt of Object.keys(values)) if (!cp.def.options.includes(opt)) push(errors, "unknown-variant-value", `'${mapKey}'.props.${mp.orig}: maps option '${opt}' that is not a Figma variant option`, { mapKey, prop: pn });
1299
+ for (const opt of cp.def.options) if (!(opt in values)) push(warnings, "unmapped-variant-value", `'${mapKey}'.props.${mp.orig}: variant option '${opt}' has no code mapping (will be omitted)`, { mapKey, prop: pn });
1300
+ }
1301
+ }
1302
+ }
1303
+ for (const [comp, keys] of compToEntries) if (keys.length > 1) push(errors, "double-mapped", `component '${comp.name}' is mapped by ${keys.length} entries (${keys.join(", ")}) \u2014 only one code target may win`, { keys });
1304
+ let catalogComponents = 0;
1305
+ for (const c of comps) {
1306
+ if (c.type !== "COMPONENT" && c.type !== "COMPONENT_SET") continue;
1307
+ catalogComponents++;
1308
+ if (c.key && mappedIds.has(c.key) || c.id && mappedIds.has(c.id)) continue;
1309
+ push(warnings, "unmapped-component", `component '${c.name}'${c.key ? " (key " + c.key + ")" : " (unpublished \u2014 no key)"} has no map entry`, { ...ifDefined("key", c.key), name: c.name });
1310
+ }
1311
+ let mapped = 0, mappedElsewhere = 0;
1312
+ for (const comp of compToEntries.keys()) if (inNamed.has(comp)) mapped++;
1313
+ else mappedElsewhere++;
1314
+ return { errors, warnings, freshness, summary: { entries: Object.keys(entries).length, catalogComponents, mapped, mappedElsewhere, errorCount: errors.length, warningCount: warnings.length } };
1315
+ }
1316
+ var LIVE_META_TIMEOUT_MS = 1e4;
1317
+ async function checkLiveFreshness(fileKey, token, exportedAt) {
1318
+ if (!fileKey || !token) return null;
1319
+ const res = await fetch(`https://api.figma.com/v1/files/${encodeURIComponent(fileKey)}/meta`, { headers: { "X-Figma-Token": token }, signal: AbortSignal.timeout(LIVE_META_TIMEOUT_MS) });
1320
+ if (!res.ok) throw new Error(`GET /v1/files/${fileKey}/meta -> ${res.status} ${res.statusText}`);
1321
+ const body = await res.json();
1322
+ const lastModified = isJsonObject(body) && isJsonObject(body.file) && typeof body.file.last_modified === "string" ? body.file.last_modified : void 0;
1323
+ if (!lastModified) return { lastModified: void 0, aheadOfSnapshot: void 0 };
1324
+ const aheadOfSnapshot = exportedAt ? Date.parse(lastModified) > Date.parse(exportedAt) : void 0;
1325
+ return { lastModified, aheadOfSnapshot };
1326
+ }
1327
+ function screenCoverage(map, catalog, screenDocs) {
1328
+ const entries = map && map.components || {};
1329
+ const mapKeys = /* @__PURE__ */ new Set();
1330
+ for (const [k, entry] of Object.entries(entries)) {
1331
+ const f = entry.figma || {};
1332
+ if (f.key) mapKeys.add(f.key);
1333
+ if (f.id) mapKeys.add(f.id);
1334
+ mapKeys.add(k);
1335
+ }
1336
+ const catKeys = new Set((catalog && catalog.components || []).map((c) => c.key).filter((k) => !!k));
1337
+ const used = /* @__PURE__ */ new Map();
1338
+ for (const doc of screenDocs || []) {
1339
+ for (const r of screenRoots(doc)) walkWithHidden(r, (n, c) => {
1340
+ if (n.type !== "INSTANCE" || !n.mainComponent) return;
1341
+ const mc = n.mainComponent;
1342
+ const id = mc.setKey || mc.key;
1343
+ if (!id) return;
1344
+ const u = getOrInit(used, id, () => ({ setName: mc.setName || mc.name, instances: 0, hiddenInstances: 0, key: id, variantKey: mc.key }));
1345
+ u.instances++;
1346
+ if (c.hidden) u.hiddenInstances++;
1347
+ });
1348
+ }
1349
+ const rows = [...used.values()].map((u) => ({
1350
+ ...u,
1351
+ inCatalog: catKeys.has(u.key) || !!u.variantKey && catKeys.has(u.variantKey),
1352
+ inMap: mapKeys.has(u.key) || !!u.variantKey && mapKeys.has(u.variantKey)
1353
+ }));
1354
+ const instances = rows.reduce((n, r) => n + r.instances, 0);
1355
+ const hiddenInstances = rows.reduce((n, r) => n + r.hiddenInstances, 0);
1356
+ const hiddenOnly = rows.filter((r) => r.instances === r.hiddenInstances).length;
1357
+ const inCatalog = rows.filter((r) => r.inCatalog).length;
1358
+ const inMap = rows.filter((r) => r.inMap).length;
1359
+ return {
1360
+ distinct: rows.length,
1361
+ instances,
1362
+ hiddenInstances,
1363
+ hiddenOnly,
1364
+ inCatalog,
1365
+ inMap,
1366
+ catalogPct: rows.length ? Math.round(inCatalog / rows.length * 100) : null,
1367
+ mapPct: rows.length ? Math.round(inMap / rows.length * 100) : null,
1368
+ unmapped: rows.filter((r) => !r.inMap).map((r) => ({ setName: r.setName, key: r.key, instances: r.instances }))
1369
+ };
1370
+ }
1371
+ function screenCoverageReport(input) {
1372
+ const { map, catalogFile, catalog: named, screens, extraCatalogs, union } = input;
1373
+ const cov = screenCoverage(map, union, screens.map((r) => r.doc));
1374
+ const warnings = [];
1375
+ if (!cov.distinct) return { coverage: cov, warnings };
1376
+ const none = [];
1377
+ const others = { components: extraCatalogs.flatMap((x) => x.catalog.components) };
1378
+ const rekey = cov.mapPct === 0 ? matchByNameAndSignature(none.concat(...screens.map((r) => visibleInstances(r.doc, r.file))), named, others) : null;
1379
+ if (cov.mapPct === 0 && rekey && isRekeyed(rekey)) {
1380
+ warnings.push({
1381
+ code: "catalog-rekeyed",
1382
+ message: `NONE of the ${cov.distinct} components on this screen resolve to your map or catalog by key \u2014 but ${rekey.summary.proposed} of the ${rekey.summary.withCandidates} visible component name(s) that exist in the catalog also match it by prop signature.
1383
+ That is the SAME library under new keys (one of the Figma files is a duplicate, or the library was re-published), not a foreign one.
1384
+ Get the confirmation list with \`${scriptCmd("cross-check")} <screen.json> --design-system <dir> --out design/audit/<screen>.cross\`, have the user
1385
+ confirm it (set "confirmed": true per entry), then \`${scriptCmd("map-bootstrap")} <components.local.json> --out <map> --from-proposals design/audit/<screen>.cross.json\`.
1386
+ Confirm: ${rekey.summary.proposed} component(s) match the catalog by name and prop signature but not by key (a duplicated or re-published file) \u2014 are they the same components? Until confirmed, build every instance as new.`
1387
+ });
1388
+ } else if (cov.mapPct === 0) {
1389
+ warnings.push({
1390
+ code: "screen-coverage",
1391
+ message: `NONE of the ${cov.distinct} components on this screen resolve to your map by key` + (cov.inCatalog ? ` (${cov.inCatalog} of them are in the catalog(s) read \u2014 \`${scriptCmd("map-bootstrap")} ${catalogFile} --screen <screen.json> --out <map>\` stubs those).
1392
+ ` : `, nor to any catalog read.
1393
+ `) + ` The catalog you exported is not the library this screen is built from \u2014 a green "mapped" count above measures
1394
+ the catalog against itself. Open an instance in Figma and use "Go to main component" to find the owning file,
1395
+ then export it with \`dtwin pull --as-library "<name>"\` (CLI only \u2014 the MCP server has no library export).
1396
+ Confirm: is ${catalogFile}${extraCatalogs.length ? ` (or any of the ${extraCatalogs.length} other catalog(s) read)` : ""} the component library this screen is built from? Until confirmed, build every instance as new.`
1397
+ });
1398
+ } else if (cov.unmapped.length) {
1399
+ warnings.push({
1400
+ code: "screen-coverage",
1401
+ message: `${cov.unmapped.length} component(s) on this screen have no map entry: ` + cov.unmapped.slice(0, 6).map((u) => `'${u.setName}' (${u.instances}x)`).join(", ") + (cov.unmapped.length > 6 ? ", \u2026" : "")
1402
+ });
1403
+ }
1404
+ return { coverage: cov, warnings };
1405
+ }
1406
+ function runDriftLint(input) {
1407
+ const { mapFile, catalogFile } = input;
1408
+ const named = loadSplitFile(
1409
+ catalogFile,
1410
+ "component catalog",
1411
+ isComponentsCatalog,
1412
+ "components",
1413
+ "design-system/components.local.json",
1414
+ NO_DESIGN_SYSTEM_HINT + "\n Or build without a component map: every instance then counts as new (build-screen, step 1)."
1415
+ );
1416
+ if ("error" in named) return { status: "unusable", message: named.error };
1417
+ const mapRaw = loadDocFile(mapFile, "component map", anyJson, `Scaffold one with \`${scriptCmd("map-bootstrap")} <components.local.json> --out ${shellArg(mapFile)}\`.`);
1418
+ if ("error" in mapRaw) return { status: "unusable", message: mapRaw.error };
1419
+ const valid = validateMap(mapRaw.doc);
1420
+ if (!valid.ok || !isCodeConnectMap(mapRaw.doc)) {
1421
+ const lines = valid.errors.map((e) => `ERROR [map-invalid] ${mapFile}: ${e.path || "(root)"}: ${e.message}`);
1422
+ return { status: "map-invalid", errors: valid.errors, message: lines.join("\n") + `
1423
+
1424
+ ${mapFile} is not a valid component map (${valid.errors.length} error(s)) \u2014 fix it, or check it with \`${scriptCmd("map-validate")} ${shellArg(mapFile)}\`.` };
1425
+ }
1426
+ const map = mapRaw.doc;
1427
+ const screens = [];
1428
+ for (const file of input.screenFiles || []) {
1429
+ const r = loadDocFile(file, "screen export", isScreenDoc);
1430
+ if ("error" in r) return { status: "unusable", message: r.error };
1431
+ screens.push({ file, doc: r.doc });
1432
+ }
1433
+ const set = loadCatalogSet(catalogFile, named.doc, input.extraCatalogFiles || [], screens[0]?.file);
1434
+ if ("error" in set) return { status: "unusable", message: set.error };
1435
+ const extraCatalogs = set.sources.slice(1).map((x) => ({ file: x.file, catalog: x.catalog }));
1436
+ const res = driftLint(map, named.doc, { ...ifDefined("maxAgeMs", input.maxAgeMs), ...ifDefined("now", input.now), extraCatalogs });
1437
+ const skipped = set.skipped.map((file) => ({
1438
+ code: "catalog-unreadable",
1439
+ file,
1440
+ message: `${file} is not a readable component catalog \u2014 skipped (map entries that live only there will show as orphaned).`
1441
+ }));
1442
+ const warnings = [...skipped, ...res.warnings];
1443
+ const coverage = screens.length ? screenCoverageReport({ map, catalogFile, catalog: named.doc, screens, extraCatalogs, union: unionCatalog(set.sources) }) : null;
1444
+ return { status: "linted", ...res, warnings, summary: { ...res.summary, warningCount: warnings.length }, catalogs: set.sources, coverage };
1445
+ }
1446
+ async function main(argv) {
1447
+ const USAGE = `usage: ${scriptCmd("drift-lint")} <map.json> <design-system/components.local.json> [--screen design/pages/<Page>/<Screen>.json]... [--catalog <components.json>]... [--max-age <hours>]`;
1448
+ const OPTIONS = { "max-age": { type: "string" }, screen: { type: "string", multiple: true }, catalog: { type: "string", multiple: true }, help: { type: "boolean", short: "h" } };
1449
+ const { values: flags, positionals } = cliParse("drift-lint", argv, OPTIONS, USAGE, 2, (args) => parseArgs2({ args, options: OPTIONS, allowPositionals: true }));
1450
+ if (flags.help) {
1451
+ console.log(USAGE);
1452
+ return 0;
1453
+ }
1454
+ let maxAgeHours;
1455
+ if (flags["max-age"] !== void 0) {
1456
+ maxAgeHours = Number(flags["max-age"]);
1457
+ if (!(maxAgeHours > 0)) {
1458
+ console.error("--max-age expects a positive number of hours");
1459
+ return 2;
1460
+ }
1461
+ } else if (process.env.DRIFT_MAX_AGE_HOURS) {
1462
+ maxAgeHours = Number(process.env.DRIFT_MAX_AGE_HOURS);
1463
+ if (!(maxAgeHours > 0)) {
1464
+ console.error("DRIFT_MAX_AGE_HOURS expects a positive number of hours");
1465
+ return 2;
1466
+ }
1467
+ }
1468
+ const [mapFile, catalogFile] = positionals;
1469
+ if (!mapFile || !catalogFile) {
1470
+ console.error(USAGE);
1471
+ return 2;
1472
+ }
1473
+ const run = runDriftLint({ mapFile, catalogFile, extraCatalogFiles: flags.catalog || [], screenFiles: flags.screen || [], ...ifDefined("maxAgeMs", maxAgeHours ? maxAgeHours * 36e5 : void 0) });
1474
+ if (run.status === "unusable") {
1475
+ console.error(`error ${run.message}`);
1476
+ return 2;
1477
+ }
1478
+ if (run.status === "map-invalid") {
1479
+ console.error(run.message);
1480
+ return 1;
1481
+ }
1482
+ console.error(catalogSetLine(run.catalogs));
1483
+ run.errors.forEach((e) => console.error(`ERROR [${e.code}] ${e.message}`));
1484
+ run.warnings.forEach((w) => console.error(`warn [${w.code}] ${w.message}`));
1485
+ const s = run.summary;
1486
+ console.error(`
1487
+ ${s.mapped}/${s.catalogComponents} components mapped${s.mappedElsewhere ? ` (+${s.mappedElsewhere} map entr${s.mappedElsewhere === 1 ? "y" : "ies"} resolved in the other catalog(s))` : ""} \xB7 ${s.errorCount} error(s), ${s.warningCount} warning(s)`);
1488
+ console.error(` (that number is the CATALOG measured against the map \u2014 it says nothing about any particular screen.)`);
1489
+ if (run.coverage) {
1490
+ const { coverage: cov, warnings } = run.coverage;
1491
+ if (!cov.distinct) {
1492
+ console.error(`
1493
+ SCREEN COVERAGE: the given screen export(s) contain no INSTANCE nodes \u2014 nothing to reuse either way.`);
1494
+ } else {
1495
+ console.error(
1496
+ `
1497
+ SCREEN COVERAGE: ${cov.inMap}/${cov.distinct} (${cov.mapPct}%) of the component sets placed on this screen are in your map (by component key) \xB7 ${cov.inCatalog}/${cov.distinct} (${cov.catalogPct}%) are even in the catalog \xB7 ${cov.instances} instance(s) total, ${cov.hiddenInstances} of them on hidden layers` + (cov.hiddenOnly ? ` (${cov.hiddenOnly} set(s) appear ONLY on hidden layers and will not be built)` : "")
1498
+ );
1499
+ console.error(` (this says which components you can REUSE by key \u2014 not which ones the build contains; that is verify's job.)`);
1500
+ for (const w of warnings) console.error(`warn [${w.code}] ${w.message}`);
1501
+ }
1502
+ } else {
1503
+ console.error(`note pass --screen <screen.json> to get the number that matters: how much of THAT screen your map covers.`);
1504
+ }
1505
+ const fileKey = process.env.FIGMA_FILE_KEY;
1506
+ const token = process.env.FIGMA_TOKEN;
1507
+ const exportedAt = run.catalogs[0]?.catalog.exportedAt;
1508
+ if (fileKey && token) {
1509
+ try {
1510
+ const live = await checkLiveFreshness(fileKey, token, exportedAt);
1511
+ if (live && live.aheadOfSnapshot) console.error(`warn [live-meta] the live Figma file was modified (${live.lastModified}) AFTER this snapshot was exported (${exportedAt}) \u2014 it is confirmed stale, not just old.`);
1512
+ } catch (e) {
1513
+ console.error(`warn [live-meta] could not verify against the live file: ${errMsg(e)}`);
1514
+ }
1515
+ }
1516
+ return run.errors.length ? 1 : 0;
1517
+ }
1518
+ if (import.meta.main) runCli(main, { name: "drift-lint" });
1519
+ export {
1520
+ DEFAULT_MAX_AGE_MS,
1521
+ checkFreshness,
1522
+ checkLiveFreshness,
1523
+ driftLint,
1524
+ runDriftLint,
1525
+ screenCoverage
1526
+ };