brandspec 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,718 @@
1
+ // cli/parser.ts
2
+ import yaml from "js-yaml";
3
+ function parse(content) {
4
+ const errors = [];
5
+ const warnings = [];
6
+ let parsed;
7
+ try {
8
+ parsed = yaml.load(content);
9
+ } catch (e) {
10
+ return {
11
+ success: false,
12
+ errors: [`Invalid YAML: ${e instanceof Error ? e.message : "Unknown error"}`],
13
+ warnings: []
14
+ };
15
+ }
16
+ if (!parsed || typeof parsed !== "object") {
17
+ return { success: false, errors: ["YAML must be an object"], warnings: [] };
18
+ }
19
+ const doc = parsed;
20
+ if (!doc.meta || typeof doc.meta !== "object") {
21
+ errors.push("Missing required field: meta");
22
+ return { success: false, errors, warnings };
23
+ }
24
+ const meta = doc.meta;
25
+ if (!meta.name || typeof meta.name !== "string") {
26
+ errors.push("Missing required field: meta.name");
27
+ return { success: false, errors, warnings };
28
+ }
29
+ if (doc.assets !== void 0) {
30
+ if (!Array.isArray(doc.assets)) {
31
+ errors.push("Field 'assets' must be an array");
32
+ } else {
33
+ doc.assets.forEach((asset, i) => {
34
+ if (!asset || typeof asset !== "object") {
35
+ errors.push(`assets[${i}] must be an object`);
36
+ } else {
37
+ const a = asset;
38
+ if (!a.file || typeof a.file !== "string") {
39
+ errors.push(`assets[${i}].file is required and must be a string`);
40
+ }
41
+ }
42
+ });
43
+ }
44
+ }
45
+ if (!doc.core) {
46
+ warnings.push("No 'core' section \u2014 brand essence, personality, and voice are recommended");
47
+ }
48
+ if (!doc.tokens) {
49
+ warnings.push("No 'tokens' section \u2014 design tokens are recommended");
50
+ }
51
+ if (errors.length > 0) {
52
+ return { success: false, errors, warnings };
53
+ }
54
+ return {
55
+ success: true,
56
+ data: doc,
57
+ errors: [],
58
+ warnings
59
+ };
60
+ }
61
+ function serialize(data) {
62
+ return yaml.dump(data, {
63
+ lineWidth: -1,
64
+ noRefs: true,
65
+ quotingType: '"',
66
+ forceQuotes: false
67
+ });
68
+ }
69
+
70
+ // cli/validate.ts
71
+ import Ajv from "ajv";
72
+ import addFormats from "ajv-formats";
73
+
74
+ // cli/schema.ts
75
+ var schema = {
76
+ $schema: "https://json-schema.org/draft/2020-12/schema",
77
+ $id: "https://brandspec.tools/schema/v0.1.0",
78
+ title: "brandspec",
79
+ description: "Brand Identity specification format",
80
+ type: "object",
81
+ required: ["meta"],
82
+ properties: {
83
+ meta: {
84
+ type: "object",
85
+ description: "Brand metadata",
86
+ required: ["name"],
87
+ properties: {
88
+ name: { type: "string", description: "Brand name" },
89
+ version: { type: "string", description: "Brand spec version (semver)" },
90
+ updated: { type: "string", format: "date", description: "Last updated date" },
91
+ description: { type: "string", description: "Brief brand description" },
92
+ url: { type: "string", format: "uri", description: "Brand website" }
93
+ },
94
+ additionalProperties: true
95
+ },
96
+ core: {
97
+ type: "object",
98
+ description: "Brand essence, personality, and voice",
99
+ properties: {
100
+ essence: { type: "string" },
101
+ tagline: { type: "string" },
102
+ mission: { type: "string" },
103
+ vision: { type: "string" },
104
+ values: { type: "array", items: { type: "string" } },
105
+ personality: { type: "array", items: { type: "string" } },
106
+ voice: {
107
+ type: "object",
108
+ properties: {
109
+ tone: { type: "array", items: { type: "string" } },
110
+ principles: { type: "array", items: { type: "string" } }
111
+ },
112
+ additionalProperties: true
113
+ }
114
+ },
115
+ additionalProperties: true
116
+ },
117
+ tokens: {
118
+ type: "object",
119
+ description: "Design tokens (W3C DTCG compliant)",
120
+ additionalProperties: true
121
+ },
122
+ assets: {
123
+ type: "array",
124
+ description: "Brand assets",
125
+ items: {
126
+ type: "object",
127
+ required: ["file"],
128
+ properties: {
129
+ file: { type: "string" },
130
+ id: { type: "string" },
131
+ role: { type: "string" },
132
+ variant: { type: "string" },
133
+ context: { type: "string" },
134
+ description: { type: "string" },
135
+ formats: {
136
+ type: "array",
137
+ items: {
138
+ type: "object",
139
+ properties: {
140
+ path: { type: "string" },
141
+ width: { type: "integer" },
142
+ height: { type: "integer" }
143
+ },
144
+ additionalProperties: true
145
+ }
146
+ },
147
+ tags: { type: "array", items: { type: "string" } }
148
+ },
149
+ additionalProperties: true
150
+ }
151
+ },
152
+ guidelines: {
153
+ type: "object",
154
+ description: "Usage guidelines",
155
+ additionalProperties: {
156
+ type: "object",
157
+ properties: {
158
+ content: { type: "string" },
159
+ rules: {
160
+ type: "array",
161
+ items: { $ref: "#/$defs/guidelineRule" }
162
+ }
163
+ },
164
+ additionalProperties: true
165
+ }
166
+ },
167
+ extensions: {
168
+ type: "object",
169
+ description: "Custom extensions",
170
+ additionalProperties: true
171
+ }
172
+ },
173
+ additionalProperties: true,
174
+ $defs: {
175
+ guidelineRule: {
176
+ type: "object",
177
+ required: ["description", "severity"],
178
+ properties: {
179
+ id: { type: "string" },
180
+ description: { type: "string" },
181
+ severity: {
182
+ type: "string",
183
+ enum: ["info", "warning", "error"]
184
+ },
185
+ criteria: { type: "array", items: { type: "string" } },
186
+ applies_to: { type: "string" }
187
+ },
188
+ additionalProperties: true
189
+ }
190
+ }
191
+ };
192
+
193
+ // cli/validate.ts
194
+ var ajvInstance = null;
195
+ function getAjv() {
196
+ if (!ajvInstance) {
197
+ ajvInstance = new Ajv({ allErrors: true, strict: false });
198
+ addFormats(ajvInstance);
199
+ }
200
+ return ajvInstance;
201
+ }
202
+ function validate(data) {
203
+ const ajv = getAjv();
204
+ const { $schema: _, $id: __, ...schemaBody } = schema;
205
+ const valid = ajv.validate(schemaBody, data);
206
+ if (valid) {
207
+ return { valid: true, errors: [], warnings: [] };
208
+ }
209
+ const errors = (ajv.errors ?? []).map((err) => {
210
+ const path = err.instancePath || "/";
211
+ return `${path}: ${err.message}`;
212
+ });
213
+ return { valid: false, errors, warnings: [] };
214
+ }
215
+
216
+ // cli/tokens.ts
217
+ function toCss(data) {
218
+ var _a;
219
+ const lines = [];
220
+ if (!data.tokens) return ":root {}\n";
221
+ for (const [group, tokens] of Object.entries(data.tokens)) {
222
+ if (!tokens) continue;
223
+ for (const [name, token] of Object.entries(tokens)) {
224
+ const prefix = group === "colors" ? "" : `${group}-`;
225
+ const varName = `--${prefix}${name}`;
226
+ lines.push(` ${varName}: ${token.$value};`);
227
+ }
228
+ }
229
+ const darkLines = [];
230
+ if (data.tokens.colors) {
231
+ for (const [name, token] of Object.entries(data.tokens.colors)) {
232
+ const dark = (_a = token.$extensions) == null ? void 0 : _a["dark"];
233
+ if (typeof dark === "string") {
234
+ darkLines.push(` --${name}: ${dark};`);
235
+ }
236
+ }
237
+ }
238
+ let output = `:root {
239
+ ${lines.join("\n")}
240
+ }
241
+ `;
242
+ if (darkLines.length > 0) {
243
+ output += `
244
+ .dark {
245
+ ${darkLines.join("\n")}
246
+ }
247
+ `;
248
+ }
249
+ return output;
250
+ }
251
+ function toTailwindCss(data) {
252
+ const lines = [];
253
+ if (!data.tokens) return '@import "tailwindcss";\n\n@theme {}\n';
254
+ if (data.tokens.colors) {
255
+ for (const [name, token] of Object.entries(data.tokens.colors)) {
256
+ lines.push(` --color-${name}: ${token.$value};`);
257
+ }
258
+ }
259
+ if (data.tokens.typography) {
260
+ for (const [name, token] of Object.entries(data.tokens.typography)) {
261
+ lines.push(` --font-${name}: ${token.$value};`);
262
+ }
263
+ }
264
+ if (data.tokens.spacing) {
265
+ for (const [name, token] of Object.entries(data.tokens.spacing)) {
266
+ lines.push(` --spacing-${name}: ${token.$value};`);
267
+ }
268
+ }
269
+ if (data.tokens.radius) {
270
+ for (const [name, token] of Object.entries(data.tokens.radius)) {
271
+ lines.push(` --radius-${name}: ${token.$value};`);
272
+ }
273
+ }
274
+ return `@import "tailwindcss";
275
+
276
+ @theme {
277
+ ${lines.join("\n")}
278
+ }
279
+ `;
280
+ }
281
+ function toFigmaTokens(data) {
282
+ if (!data.tokens) return JSON.stringify({}, null, 2);
283
+ const output = {};
284
+ for (const [group, tokens] of Object.entries(data.tokens)) {
285
+ if (!tokens) continue;
286
+ output[group] = {};
287
+ for (const [name, token] of Object.entries(tokens)) {
288
+ output[group][name] = {
289
+ value: token.$value,
290
+ type: token.$type ?? group,
291
+ ...token.$description && { description: token.$description }
292
+ };
293
+ }
294
+ }
295
+ return JSON.stringify(output, null, 2) + "\n";
296
+ }
297
+ function toStyleDictionary(data) {
298
+ const tokenFile = {};
299
+ if (data.tokens) {
300
+ for (const [group, tokens2] of Object.entries(data.tokens)) {
301
+ if (!tokens2) continue;
302
+ tokenFile[group] = {};
303
+ for (const [name, token] of Object.entries(tokens2)) {
304
+ tokenFile[group][name] = {
305
+ $value: token.$value,
306
+ $type: token.$type ?? group,
307
+ ...token.$description && { $description: token.$description }
308
+ };
309
+ }
310
+ }
311
+ }
312
+ const tokens = JSON.stringify(tokenFile, null, 2) + "\n";
313
+ const brandName = data.meta.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
314
+ const config = {
315
+ source: ["tokens.json"],
316
+ usesDtcg: true,
317
+ platforms: {
318
+ css: {
319
+ transformGroup: "css",
320
+ buildPath: `build/css/`,
321
+ files: [
322
+ {
323
+ destination: "variables.css",
324
+ format: "css/variables",
325
+ options: { outputReferences: true }
326
+ }
327
+ ]
328
+ },
329
+ scss: {
330
+ transformGroup: "scss",
331
+ buildPath: `build/scss/`,
332
+ files: [
333
+ {
334
+ destination: "_variables.scss",
335
+ format: "scss/variables",
336
+ options: { outputReferences: true }
337
+ }
338
+ ]
339
+ },
340
+ ios: {
341
+ transformGroup: "ios-swift",
342
+ buildPath: `build/ios/`,
343
+ files: [
344
+ {
345
+ destination: `${brandName}.swift`,
346
+ format: "ios-swift/class.swift",
347
+ className: data.meta.name.replace(/[^a-zA-Z0-9]/g, "")
348
+ }
349
+ ]
350
+ },
351
+ android: {
352
+ transformGroup: "android",
353
+ buildPath: `build/android/`,
354
+ files: [
355
+ {
356
+ destination: "colors.xml",
357
+ format: "android/resources",
358
+ filter: { $type: "color" }
359
+ },
360
+ {
361
+ destination: "dimens.xml",
362
+ format: "android/resources",
363
+ filter: { $type: "dimension" }
364
+ }
365
+ ]
366
+ }
367
+ }
368
+ };
369
+ return { tokens, config: JSON.stringify(config, null, 2) + "\n" };
370
+ }
371
+ function flattenTokens(data) {
372
+ const result = [];
373
+ if (!data.tokens) return result;
374
+ for (const [group, tokens] of Object.entries(data.tokens)) {
375
+ if (!tokens) continue;
376
+ for (const [name, token] of Object.entries(tokens)) {
377
+ result.push({ group, name, token });
378
+ }
379
+ }
380
+ return result;
381
+ }
382
+
383
+ // cli/remote.ts
384
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
385
+ import { resolve, dirname, join } from "path";
386
+ import { homedir } from "os";
387
+ var API_BASE = process.env.BRANDSPEC_API_URL ?? "https://brandspec.tools";
388
+ function getCredentialsPath() {
389
+ return join(homedir(), ".config", "brandspec", "credentials");
390
+ }
391
+ function loadToken() {
392
+ const envToken = process.env.BRANDSPEC_TOKEN;
393
+ if (envToken) return envToken;
394
+ const credPath = getCredentialsPath();
395
+ if (existsSync(credPath)) {
396
+ const content = readFileSync(credPath, "utf-8").trim();
397
+ if (content) return content;
398
+ }
399
+ return null;
400
+ }
401
+ function parseOrgBrand(str) {
402
+ const parts = str.split("/");
403
+ if (parts.length !== 2) return null;
404
+ const [org, brand] = parts;
405
+ const slugRe = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
406
+ if (!slugRe.test(org) || !slugRe.test(brand)) return null;
407
+ return { org, brand };
408
+ }
409
+ function loadRemote(args) {
410
+ const positional = args.find((a) => !a.startsWith("-") && a.includes("/"));
411
+ if (positional) return parseOrgBrand(positional);
412
+ const rcPath = resolve(".brandspecrc");
413
+ if (existsSync(rcPath)) {
414
+ const content = readFileSync(rcPath, "utf-8");
415
+ const match = content.match(/remote:\s*(.+)/);
416
+ if (match) return parseOrgBrand(match[1].trim());
417
+ }
418
+ return null;
419
+ }
420
+ function ensureBrandspecrc(org, brand) {
421
+ const rcPath = resolve(".brandspecrc");
422
+ if (!existsSync(rcPath)) {
423
+ writeFileSync(rcPath, `remote: ${org}/${brand}
424
+ `, "utf-8");
425
+ }
426
+ }
427
+ function saveCredentials(token) {
428
+ const credPath = getCredentialsPath();
429
+ mkdirSync(dirname(credPath), { recursive: true });
430
+ writeFileSync(credPath, token, { mode: 384 });
431
+ }
432
+
433
+ // cli/color.ts
434
+ function parseColor(color) {
435
+ const hex = color.match(/^#([0-9a-f]{3,8})$/i);
436
+ if (hex) {
437
+ let h = hex[1];
438
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
439
+ return [
440
+ parseInt(h.slice(0, 2), 16),
441
+ parseInt(h.slice(2, 4), 16),
442
+ parseInt(h.slice(4, 6), 16)
443
+ ];
444
+ }
445
+ const rgb = color.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
446
+ if (rgb) {
447
+ return [parseInt(rgb[1]), parseInt(rgb[2]), parseInt(rgb[3])];
448
+ }
449
+ const oklch = color.match(
450
+ /oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)/
451
+ );
452
+ if (oklch) {
453
+ const L = oklch[2] === "%" ? parseFloat(oklch[1]) / 100 : parseFloat(oklch[1]);
454
+ const C = parseFloat(oklch[3]);
455
+ const H = parseFloat(oklch[4]);
456
+ return oklchToSrgb(L, C, H);
457
+ }
458
+ return null;
459
+ }
460
+ function oklchToSrgb(L, C, H) {
461
+ const hRad = H * Math.PI / 180;
462
+ const a = C * Math.cos(hRad);
463
+ const b = C * Math.sin(hRad);
464
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
465
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
466
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
467
+ const l = l_ * l_ * l_;
468
+ const m = m_ * m_ * m_;
469
+ const s = s_ * s_ * s_;
470
+ const rl = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
471
+ const gl = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
472
+ const bl = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
473
+ const gamma = (x) => x <= 31308e-7 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
474
+ const clamp = (x) => Math.max(0, Math.min(255, Math.round(x * 255)));
475
+ return [clamp(gamma(rl)), clamp(gamma(gl)), clamp(gamma(bl))];
476
+ }
477
+ function relativeLuminance(r, g, b) {
478
+ const [rs, gs, bs] = [r, g, b].map((c) => {
479
+ const s = c / 255;
480
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
481
+ });
482
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
483
+ }
484
+ function getContrastRatio(color1, color2) {
485
+ const c1 = parseColor(color1);
486
+ const c2 = parseColor(color2);
487
+ if (!c1 || !c2) return 21;
488
+ const l1 = relativeLuminance(...c1);
489
+ const l2 = relativeLuminance(...c2);
490
+ const lighter = Math.max(l1, l2);
491
+ const darker = Math.min(l1, l2);
492
+ return (lighter + 0.05) / (darker + 0.05);
493
+ }
494
+ function findColorValue(colors, name) {
495
+ const token = colors[name];
496
+ if (isToken(token)) return token.$value;
497
+ if (typeof token === "object" && token !== null) {
498
+ const nested = token;
499
+ if (isToken(nested)) return nested.$value;
500
+ }
501
+ return null;
502
+ }
503
+ function isToken(v) {
504
+ return typeof v === "object" && v !== null && "$value" in v;
505
+ }
506
+
507
+ // cli/lint.ts
508
+ var requiredFields = (spec) => {
509
+ const results = [];
510
+ if (!spec.meta.version) {
511
+ results.push({
512
+ rule: "meta/version-required",
513
+ severity: "warning",
514
+ message: "meta.version is recommended for tracking changes",
515
+ path: "meta.version"
516
+ });
517
+ }
518
+ if (!spec.core) {
519
+ results.push({
520
+ rule: "core/missing",
521
+ severity: "warning",
522
+ message: "core section is missing \u2014 brand identity is undefined",
523
+ path: "core"
524
+ });
525
+ } else {
526
+ if (!spec.core.essence && !spec.core.tagline) {
527
+ results.push({
528
+ rule: "core/identity-missing",
529
+ severity: "warning",
530
+ message: "Neither essence nor tagline is defined",
531
+ path: "core"
532
+ });
533
+ }
534
+ if (!spec.core.personality || spec.core.personality.length === 0) {
535
+ results.push({
536
+ rule: "core/personality-empty",
537
+ severity: "info",
538
+ message: "No personality traits defined",
539
+ path: "core.personality"
540
+ });
541
+ }
542
+ if (!spec.core.voice) {
543
+ results.push({
544
+ rule: "core/voice-missing",
545
+ severity: "info",
546
+ message: "Voice guidelines not defined",
547
+ path: "core.voice"
548
+ });
549
+ }
550
+ }
551
+ if (!spec.tokens) {
552
+ results.push({
553
+ rule: "tokens/missing",
554
+ severity: "warning",
555
+ message: "No design tokens defined",
556
+ path: "tokens"
557
+ });
558
+ } else {
559
+ if (!spec.tokens.colors || Object.keys(spec.tokens.colors).length === 0) {
560
+ results.push({
561
+ rule: "tokens/colors-empty",
562
+ severity: "warning",
563
+ message: "No color tokens defined",
564
+ path: "tokens.colors"
565
+ });
566
+ }
567
+ if (!spec.tokens.typography || Object.keys(spec.tokens.typography).length === 0) {
568
+ results.push({
569
+ rule: "tokens/typography-empty",
570
+ severity: "info",
571
+ message: "No typography tokens defined",
572
+ path: "tokens.typography"
573
+ });
574
+ }
575
+ }
576
+ return results;
577
+ };
578
+ var colorContrast = (spec) => {
579
+ var _a;
580
+ const results = [];
581
+ const colors = (_a = spec.tokens) == null ? void 0 : _a.colors;
582
+ if (!colors) return results;
583
+ const bg = findColorValue(colors, "background");
584
+ const fg = findColorValue(colors, "foreground");
585
+ if (bg && fg) {
586
+ const ratio = getContrastRatio(bg, fg);
587
+ if (ratio < 4.5) {
588
+ results.push({
589
+ rule: "contrast/bg-fg-aa",
590
+ severity: "error",
591
+ message: `Background/foreground contrast ratio is ${ratio.toFixed(1)}:1 (WCAG AA requires 4.5:1)`,
592
+ path: "tokens.colors"
593
+ });
594
+ } else if (ratio < 7) {
595
+ results.push({
596
+ rule: "contrast/bg-fg-aaa",
597
+ severity: "info",
598
+ message: `Background/foreground contrast ratio is ${ratio.toFixed(1)}:1 (WCAG AAA requires 7:1)`,
599
+ path: "tokens.colors"
600
+ });
601
+ }
602
+ }
603
+ const primary = findColorValue(colors, "primary");
604
+ if (primary && bg) {
605
+ const ratio = getContrastRatio(bg, primary);
606
+ if (ratio < 3) {
607
+ results.push({
608
+ rule: "contrast/primary-bg",
609
+ severity: "warning",
610
+ message: `Primary on background contrast ratio is ${ratio.toFixed(1)}:1 (minimum 3:1 for large text)`,
611
+ path: "tokens.colors.primary"
612
+ });
613
+ }
614
+ }
615
+ return results;
616
+ };
617
+ var ASSET_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+)+$/;
618
+ var assetNaming = (spec) => {
619
+ const results = [];
620
+ if (!spec.assets) return results;
621
+ for (const asset of spec.assets) {
622
+ const fileName = asset.file.split("/").pop() ?? asset.file;
623
+ if (!ASSET_PATTERN.test(fileName)) {
624
+ results.push({
625
+ rule: "assets/naming-convention",
626
+ severity: "warning",
627
+ message: `Asset "${asset.file}" doesn't follow {role}-{variant}.{ext} naming convention`,
628
+ path: "assets"
629
+ });
630
+ }
631
+ if (!asset.role) {
632
+ results.push({
633
+ rule: "assets/role-missing",
634
+ severity: "info",
635
+ message: `Asset "${asset.file}" has no role defined`,
636
+ path: "assets"
637
+ });
638
+ }
639
+ }
640
+ return results;
641
+ };
642
+ var KEBAB_CASE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
643
+ var tokenNaming = (spec) => {
644
+ const results = [];
645
+ if (!spec.tokens) return results;
646
+ for (const [group, tokens] of Object.entries(spec.tokens)) {
647
+ if (!tokens || typeof tokens !== "object") continue;
648
+ for (const name of Object.keys(tokens)) {
649
+ if (name.startsWith("$")) continue;
650
+ if (!KEBAB_CASE.test(name)) {
651
+ results.push({
652
+ rule: "tokens/naming-kebab",
653
+ severity: "info",
654
+ message: `Token "${group}.${name}" should use kebab-case`,
655
+ path: `tokens.${group}.${name}`
656
+ });
657
+ }
658
+ }
659
+ }
660
+ return results;
661
+ };
662
+ var essentialColors = (spec) => {
663
+ var _a;
664
+ const results = [];
665
+ const colors = (_a = spec.tokens) == null ? void 0 : _a.colors;
666
+ if (!colors) return results;
667
+ const essential = ["primary", "background", "foreground"];
668
+ for (const name of essential) {
669
+ if (!findColorValue(colors, name)) {
670
+ results.push({
671
+ rule: "tokens/essential-color",
672
+ severity: "warning",
673
+ message: `Essential color token "${name}" is missing`,
674
+ path: `tokens.colors.${name}`
675
+ });
676
+ }
677
+ }
678
+ return results;
679
+ };
680
+ var ALL_RULES = [
681
+ requiredFields,
682
+ colorContrast,
683
+ assetNaming,
684
+ tokenNaming,
685
+ essentialColors
686
+ ];
687
+ function lintBrandspec(spec) {
688
+ const results = ALL_RULES.flatMap((rule) => rule(spec));
689
+ const errors = results.filter((r) => r.severity === "error").length;
690
+ const warnings = results.filter((r) => r.severity === "warning").length;
691
+ const infos = results.filter((r) => r.severity === "info").length;
692
+ const score = Math.max(
693
+ 0,
694
+ Math.min(100, 100 - errors * 10 - warnings * 3 - infos)
695
+ );
696
+ return { score, results, errors, warnings, infos };
697
+ }
698
+ export {
699
+ API_BASE,
700
+ ensureBrandspecrc,
701
+ flattenTokens,
702
+ getContrastRatio,
703
+ getCredentialsPath,
704
+ lintBrandspec,
705
+ loadRemote,
706
+ loadToken,
707
+ parse,
708
+ parseColor,
709
+ parseOrgBrand,
710
+ saveCredentials,
711
+ schema,
712
+ serialize,
713
+ toCss,
714
+ toFigmaTokens,
715
+ toStyleDictionary,
716
+ toTailwindCss,
717
+ validate
718
+ };