@agent-scope/tokens 1.17.1 → 1.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +839 -0
  2. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,839 @@
1
+ # @agent-scope/tokens
2
+
3
+ Design token file parser, validator, and resolution engine for Scope.
4
+
5
+ Parses `reactscope.tokens.json` / `.yaml` files into a flat, fully-resolved `Token[]`, and provides lookup, nearest-match search, compliance auditing, impact analysis, theme overlays, and multi-format export.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @agent-scope/tokens
13
+ ```
14
+
15
+ ---
16
+
17
+ ## What it does / when to use it
18
+
19
+ | Need | Use |
20
+ |------|-----|
21
+ | Parse a token file and resolve all `{path.to.token}` references | `parseTokenFile` / `parseTokenFileSync` |
22
+ | Look up a token by path | `TokenResolver.resolve` |
23
+ | Find the token that matches a CSS value | `TokenResolver.match` / `TokenResolver.nearest` |
24
+ | Audit component styles against the token set | `ComplianceEngine` |
25
+ | Predict which components break when a token changes | `ImpactAnalyzer` |
26
+ | Export tokens to CSS / TypeScript / SCSS / Tailwind / Figma | `exportTokens` |
27
+ | Resolve values across dark/brand themes | `ThemeResolver` |
28
+ | Validate a raw token file object | `validateTokenFile` |
29
+
30
+ ---
31
+
32
+ ## Token file format
33
+
34
+ Token files are JSON or YAML. The top-level shape is:
35
+
36
+ ```jsonc
37
+ {
38
+ "$schema": "https://reactscope.dev/token-schema.json", // optional
39
+ "version": "0.1", // required
40
+ "meta": { // optional
41
+ "name": "My Design Tokens",
42
+ "lastUpdated": "2024-01-01",
43
+ "updatedBy": "designer@example.com"
44
+ },
45
+ "tokens": { ... }, // required
46
+ "themes": { ... } // optional
47
+ }
48
+ ```
49
+
50
+ ### Token tree
51
+
52
+ The `tokens` object is a nested tree. Every leaf node must have `value` and `type`:
53
+
54
+ ```json
55
+ {
56
+ "version": "0.1",
57
+ "tokens": {
58
+ "color": {
59
+ "primary": {
60
+ "500": { "value": "#3B82F6", "type": "color" },
61
+ "600": { "value": "#2563EB", "type": "color" }
62
+ },
63
+ "neutral": {
64
+ "0": { "value": "#FFFFFF", "type": "color" },
65
+ "900": { "value": "#111827", "type": "color" }
66
+ },
67
+ "alias": {
68
+ "brand": { "value": "{color.primary.500}", "type": "color" }
69
+ }
70
+ },
71
+ "spacing": {
72
+ "4": { "value": "16px", "type": "dimension" },
73
+ "8": { "value": "32px", "type": "dimension" }
74
+ },
75
+ "typography": {
76
+ "fontFamily": {
77
+ "sans": { "value": "Inter, sans-serif", "type": "fontFamily", "description": "Primary font" }
78
+ },
79
+ "fontWeight": {
80
+ "bold": { "value": 700, "type": "fontWeight" },
81
+ "normal": { "value": 400, "type": "fontWeight" }
82
+ }
83
+ },
84
+ "radius": { "md": { "value": "6px", "type": "dimension" } },
85
+ "shadow": { "md": { "value": "0 4px 6px rgba(0,0,0,0.1)", "type": "shadow" } },
86
+ "motion": {
87
+ "duration": { "fast": { "value": "150ms", "type": "duration" } },
88
+ "easing": { "standard": { "value": "cubic-bezier(0.4, 0, 0.2, 1)", "type": "cubicBezier" } }
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### Leaf node fields
95
+
96
+ | Field | Type | Required | Description |
97
+ |-------|------|----------|-------------|
98
+ | `value` | `string \| number` | Yes | Raw value. May be a `{path.to.token}` reference. |
99
+ | `type` | `TokenType` | Yes | One of the 8 supported types (see below). |
100
+ | `description` | `string` | No | Human-readable annotation. Passed through to exports. |
101
+
102
+ ### Token types
103
+
104
+ | Type | Example value |
105
+ |------|---------------|
106
+ | `color` | `"#3B82F6"` |
107
+ | `dimension` | `"16px"`, `"1.5rem"` |
108
+ | `fontFamily` | `"Inter, sans-serif"` |
109
+ | `fontWeight` | `700` |
110
+ | `number` | `1.5` |
111
+ | `shadow` | `"0 4px 6px rgba(0,0,0,0.1)"` |
112
+ | `duration` | `"150ms"` |
113
+ | `cubicBezier` | `"cubic-bezier(0.4, 0, 0.2, 1)"` |
114
+
115
+ ---
116
+
117
+ ## Reference syntax — `{path.to.token}`
118
+
119
+ A token's `value` can point to another token using curly-brace dot-notation:
120
+
121
+ ```json
122
+ "color": {
123
+ "primary": { "500": { "value": "#3B82F6", "type": "color" } },
124
+ "alias": {
125
+ "brand": { "value": "{color.primary.500}", "type": "color" }
126
+ }
127
+ }
128
+ ```
129
+
130
+ - The parser resolves `{color.primary.500}` → `"#3B82F6"` and stores it in `resolvedValue`.
131
+ - References can chain: `A → B → C` (all fully resolved).
132
+ - Circular references (e.g. `A → B → A`) throw `TokenParseError` with code `"CIRCULAR_REFERENCE"`.
133
+ - References to non-existent paths throw `TokenParseError` with code `"INVALID_REFERENCE"`.
134
+ - `resolvedValue` is **always a string**, even for numeric tokens (`fontWeight: 700` → `"700"`).
135
+
136
+ ---
137
+
138
+ ## API reference
139
+
140
+ ### `parseTokenFile(input, format?)` — async
141
+
142
+ ```typescript
143
+ async function parseTokenFile(
144
+ input: string,
145
+ format?: "json" | "yaml",
146
+ ): Promise<ParsedTokens>
147
+ ```
148
+
149
+ Parses a token file (JSON or YAML) and returns a flat resolved token array.
150
+
151
+ - If `format` is omitted, JSON is tried first, then YAML as a fallback.
152
+ - Throws `TokenParseError` on reference or circular reference errors.
153
+ - Throws `TokenValidationError` on schema violations.
154
+
155
+ ```typescript
156
+ import { parseTokenFile } from "@agent-scope/tokens";
157
+ import { readFileSync } from "node:fs";
158
+
159
+ const source = readFileSync("tokens.json", "utf8");
160
+ const { tokens, rawFile } = await parseTokenFile(source);
161
+ // tokens: Token[] — flat, resolved
162
+ // rawFile: TokenFile — the validated raw file object
163
+ ```
164
+
165
+ ### `parseTokenFileSync(input)` — sync, JSON only
166
+
167
+ ```typescript
168
+ function parseTokenFileSync(input: string): ParsedTokens
169
+ ```
170
+
171
+ Synchronous version. Only supports JSON (YAML requires an async import).
172
+
173
+ ```typescript
174
+ import { parseTokenFileSync } from "@agent-scope/tokens";
175
+
176
+ const { tokens } = parseTokenFileSync(source);
177
+ // tokens[0] → { path: "color.primary.500", value: "#3B82F6", resolvedValue: "#3B82F6", type: "color" }
178
+ ```
179
+
180
+ ### `ParsedTokens`
181
+
182
+ ```typescript
183
+ interface ParsedTokens {
184
+ tokens: Token[];
185
+ rawFile: TokenFile;
186
+ }
187
+ ```
188
+
189
+ ### `Token`
190
+
191
+ ```typescript
192
+ interface Token {
193
+ path: string; // dot-notation, e.g. "color.primary.500"
194
+ value: string | number; // raw value from the file (may be a reference)
195
+ resolvedValue: string; // fully resolved, always a string
196
+ type: TokenType;
197
+ description?: string;
198
+ }
199
+ ```
200
+
201
+ ---
202
+
203
+ ### `TokenResolver`
204
+
205
+ Wraps a `Token[]` for fast path-based lookup and nearest-match search.
206
+
207
+ ```typescript
208
+ import { TokenResolver } from "@agent-scope/tokens";
209
+
210
+ const resolver = new TokenResolver(tokens);
211
+ ```
212
+
213
+ #### `resolve(path)`
214
+
215
+ ```typescript
216
+ resolve(path: string): string
217
+ ```
218
+
219
+ Returns the resolved value for a known token path. Throws `TokenParseError` (`"INVALID_REFERENCE"`) if not found.
220
+
221
+ ```typescript
222
+ resolver.resolve("color.primary.500"); // "#3B82F6"
223
+ resolver.resolve("spacing.4"); // "16px"
224
+ resolver.resolve("typography.fontWeight.bold"); // "700"
225
+ resolver.resolve("color.alias.brand"); // "#3B82F6" (alias resolved)
226
+ ```
227
+
228
+ #### `match(value, type)`
229
+
230
+ ```typescript
231
+ match(value: string, type: TokenType): TokenMatch | null
232
+ ```
233
+
234
+ Returns an exact-match `TokenMatch` if any token of the given type has a `resolvedValue` equal to `value` (case-insensitive for colors). Returns `null` if no exact match exists.
235
+
236
+ ```typescript
237
+ resolver.match("#3B82F6", "color");
238
+ // { token: { path: "color.primary.500", ... }, exact: true, distance: 0 }
239
+
240
+ resolver.match("#3b82f6", "color"); // case-insensitive — same result
241
+ resolver.match("16px", "dimension");
242
+ // { token: { path: "spacing.4", ... }, exact: true, distance: 0 }
243
+
244
+ resolver.match("#AABBCC", "color"); // null — no match
245
+ ```
246
+
247
+ #### `nearest(value, type)`
248
+
249
+ ```typescript
250
+ nearest(value: string, type: TokenType): TokenMatch
251
+ ```
252
+
253
+ Returns the `TokenMatch` with the smallest computed distance to `value` among all tokens of the given type. Always returns a result (never null); throws if no tokens of the specified type exist.
254
+
255
+ Distance computation per type:
256
+ - `color` — Euclidean distance in CIE Lab space (perceptual)
257
+ - `dimension` / `duration` — `|parsed numeric difference|`
258
+ - `fontWeight` / `number` — `|numeric difference|`
259
+ - `shadow` / `fontFamily` / `cubicBezier` — string equality (0 or 1)
260
+
261
+ ```typescript
262
+ // #3A82F5 is perceptually very close to #3B82F6
263
+ resolver.nearest("#3A82F5", "color");
264
+ // { token: { path: "color.primary.500", ... }, exact: false, distance: 0.42 }
265
+
266
+ // 15px — closest to 16px (spacing.4)
267
+ resolver.nearest("15px", "dimension");
268
+ // { token: { path: "spacing.4", resolvedValue: "16px", ... }, exact: false, distance: 1 }
269
+ ```
270
+
271
+ #### `list(type?, category?)`
272
+
273
+ ```typescript
274
+ list(type?: TokenType, category?: string): Token[]
275
+ ```
276
+
277
+ Returns all tokens, optionally filtered by type and/or category (the first path segment, e.g. `"color"` in `"color.primary.500"`).
278
+
279
+ ```typescript
280
+ resolver.list(); // all tokens
281
+ resolver.list("color"); // only color tokens
282
+ resolver.list(undefined, "spacing"); // all tokens in the "spacing" category
283
+ resolver.list("dimension", "spacing"); // dimension tokens in "spacing"
284
+ ```
285
+
286
+ ### `TokenMatch`
287
+
288
+ ```typescript
289
+ interface TokenMatch {
290
+ token: Token;
291
+ exact: boolean; // true when resolvedValue === queried value
292
+ distance: number; // 0 for exact; computed distance otherwise
293
+ }
294
+ ```
295
+
296
+ ---
297
+
298
+ ### `ComplianceEngine`
299
+
300
+ Audits rendered component CSS styles against the resolved token set. Reports per-property compliance status and an aggregate compliance percentage.
301
+
302
+ ```typescript
303
+ import { ComplianceEngine } from "@agent-scope/tokens";
304
+
305
+ const engine = new ComplianceEngine(resolver);
306
+ // With custom tolerances:
307
+ const engine = new ComplianceEngine(resolver, {
308
+ colorTolerance: 3, // default: max CIE Lab distance for on-system
309
+ dimensionTolerance: 2, // default: max px difference for on-system
310
+ fontWeightTolerance: 0, // default: exact match only
311
+ });
312
+ ```
313
+
314
+ #### `audit(styles)`
315
+
316
+ ```typescript
317
+ audit(styles: ComputedStyles): ComplianceReport
318
+ ```
319
+
320
+ Audits a single component's computed styles.
321
+
322
+ ```typescript
323
+ const report = engine.audit({
324
+ colors: { background: "#3B82F6", color: "#ffffff" },
325
+ spacing: { paddingTop: "16px", gap: "8px" },
326
+ typography: { fontFamily: "Inter, sans-serif", fontSize: "14px", fontWeight: "700" },
327
+ borders: { borderRadius: "4px" },
328
+ shadows: { boxShadow: "0 1px 3px rgba(0,0,0,0.1)" },
329
+ });
330
+
331
+ report.compliance; // 0.83 (fraction of on-system properties)
332
+ report.total; // 8 (properties audited, excluding skipped values)
333
+ report.onSystem; // 7
334
+ report.offSystem; // 1
335
+ ```
336
+
337
+ #### `ComputedStyles`
338
+
339
+ ```typescript
340
+ type ComputedStyles = {
341
+ colors: Record<string, string>; // e.g. { background: "#3B82F6" }
342
+ spacing: Record<string, string>; // e.g. { paddingTop: "16px" }
343
+ typography: Record<string, string>; // fontFamily, fontSize, fontWeight, lineHeight
344
+ borders: Record<string, string>; // borderRadius, borderWidth
345
+ shadows: Record<string, string>; // boxShadow
346
+ };
347
+ ```
348
+
349
+ Skipped values (not counted toward totals): `"none"`, `"inherit"`, `"initial"`, `"unset"`, `"auto"`, `"transparent"`, `"currentColor"`, `""`, `"normal"`.
350
+
351
+ #### `ComplianceReport`
352
+
353
+ ```typescript
354
+ interface ComplianceReport {
355
+ properties: Record<string, PropertyResult>; // per-property result
356
+ total: number; // properties audited
357
+ onSystem: number;
358
+ offSystem: number;
359
+ compliance: number; // onSystem / total (1 when total === 0)
360
+ auditedAt: string; // ISO timestamp
361
+ }
362
+ ```
363
+
364
+ #### `PropertyResult` — on_system example
365
+
366
+ ```typescript
367
+ // report.properties["background"] when background: "#3B82F6" (exact token match)
368
+ {
369
+ property: "background",
370
+ value: "#3B82F6",
371
+ status: "on_system",
372
+ token: "color.primary.500",
373
+ nearest: { token: "color.primary.500", value: "#3B82F6", distance: 0 }
374
+ }
375
+ ```
376
+
377
+ #### `PropertyResult` — OFF_SYSTEM example
378
+
379
+ ```typescript
380
+ // report.properties["background"] when background: "#FF0000" (no match)
381
+ {
382
+ property: "background",
383
+ value: "#FF0000",
384
+ status: "OFF_SYSTEM",
385
+ // token is absent (undefined) for OFF_SYSTEM results
386
+ nearest: { token: "color.neutral.900", value: "#111827", distance: 74.3 }
387
+ }
388
+ ```
389
+
390
+ #### `auditBatch(components)`
391
+
392
+ ```typescript
393
+ auditBatch(components: Map<string, ComputedStyles>): BatchReport
394
+ ```
395
+
396
+ Audits multiple components at once:
397
+
398
+ ```typescript
399
+ const batch = engine.auditBatch(new Map([
400
+ ["Button", { colors: { background: "#3B82F6" }, spacing: {}, typography: {}, borders: {}, shadows: {} }],
401
+ ["Input", { colors: { background: "#FF0000" }, spacing: {}, typography: {}, borders: {}, shadows: {} }],
402
+ ]));
403
+
404
+ batch.aggregateCompliance; // 0.5
405
+ batch.components.Button.compliance; // 1
406
+ batch.components.Input.compliance; // 0
407
+ ```
408
+
409
+ #### `ComplianceEngine.toJSON(report)`
410
+
411
+ ```typescript
412
+ static toJSON(report: ComplianceReport | BatchReport): string
413
+ ```
414
+
415
+ Serializes a report to indented JSON (2-space).
416
+
417
+ ---
418
+
419
+ ### `ImpactAnalyzer`
420
+
421
+ Analyses the downstream effects of a design token change on audited components.
422
+
423
+ ```typescript
424
+ import { ImpactAnalyzer } from "@agent-scope/tokens";
425
+
426
+ const analyzer = new ImpactAnalyzer(resolver, componentReports);
427
+ // componentReports: Map<string, ComplianceReport> — from ComplianceEngine.auditBatch
428
+ ```
429
+
430
+ #### `impactOf(tokenPath, newValue)`
431
+
432
+ ```typescript
433
+ impactOf(tokenPath: string, newValue: string): ImpactReport
434
+ ```
435
+
436
+ Returns an `ImpactReport` describing which components and properties would be affected by changing the specified token to `newValue`.
437
+
438
+ ```typescript
439
+ const report = analyzer.impactOf("color.primary.500", "#1D4ED8");
440
+
441
+ report.tokenPath; // "color.primary.500"
442
+ report.oldValue; // "#3B82F6"
443
+ report.newValue; // "#1D4ED8"
444
+ report.tokenType; // "color"
445
+ report.colorDelta; // CIE Lab distance (only for color tokens)
446
+ report.affectedComponentCount; // e.g. 2
447
+ report.overallSeverity; // "subtle" | "moderate" | "significant" | "none"
448
+ report.components; // AffectedComponent[]
449
+ ```
450
+
451
+ #### `ImpactReport`
452
+
453
+ ```typescript
454
+ interface ImpactReport {
455
+ tokenPath: string;
456
+ oldValue: string;
457
+ newValue: string;
458
+ tokenType: TokenType;
459
+ affectedComponentCount: number;
460
+ components: AffectedComponent[];
461
+ overallSeverity: VisualSeverity; // max severity across all components
462
+ colorDelta?: number; // CIE Lab distance (color tokens only)
463
+ }
464
+
465
+ interface AffectedComponent {
466
+ name: string;
467
+ affectedProperties: string[]; // e.g. ["background", "borderColor"]
468
+ severity: VisualSeverity;
469
+ }
470
+
471
+ type VisualSeverity = "none" | "subtle" | "moderate" | "significant";
472
+ ```
473
+
474
+ Severity thresholds for color tokens (CIE Lab distance):
475
+ - `"none"` — distance 0 (no change)
476
+ - `"subtle"` — distance < 5
477
+ - `"moderate"` — distance < 20
478
+ - `"significant"` — distance ≥ 20
479
+
480
+ For dimension tokens: `≤2px` → subtle, `≤8px` → moderate, `>8px` → significant.
481
+
482
+ ---
483
+
484
+ ### `exportTokens(tokens, format, options?)`
485
+
486
+ ```typescript
487
+ function exportTokens(
488
+ tokens: Token[],
489
+ format: ExportFormat,
490
+ options?: ExportOptions,
491
+ ): string
492
+ ```
493
+
494
+ Exports a resolved token set to the specified format.
495
+
496
+ ```typescript
497
+ type ExportFormat = "css" | "ts" | "scss" | "tailwind" | "flat-json" | "figma";
498
+
499
+ interface ExportOptions {
500
+ themes?: Map<string, Map<string, string>>; // theme name → (tokenPath → overrideValue)
501
+ prefix?: string; // CSS/SCSS: prefix for custom property / variable names
502
+ rootSelector?: string; // CSS: override ":root" selector
503
+ }
504
+ ```
505
+
506
+ #### CSS export
507
+
508
+ ```typescript
509
+ exportTokens(tokens, "css");
510
+ // :root {
511
+ // --color-primary-500: #3B82F6;
512
+ // --color-primary-600: #2563EB;
513
+ // --spacing-4: 16px;
514
+ // ...
515
+ // }
516
+
517
+ exportTokens(tokens, "css", { prefix: "scope" });
518
+ // :root { --scope-color-primary-500: #3B82F6; ... }
519
+
520
+ exportTokens(tokens, "css", { rootSelector: "html" });
521
+ // html { --color-primary-500: #3B82F6; ... }
522
+
523
+ // With themes:
524
+ exportTokens(tokens, "css", { themes: themeMap });
525
+ // :root { --color-primary-500: #3B82F6; ... }
526
+ // [data-theme="dark"] { --color-primary-500: #60A5FA; ... }
527
+ // [data-theme="brand-b"] { --color-primary-500: #8B5CF6; ... }
528
+ ```
529
+
530
+ #### TypeScript export
531
+
532
+ ```typescript
533
+ exportTokens(tokens, "ts");
534
+ // // Auto-generated design tokens — do not edit manually
535
+ //
536
+ // export const colorPrimary500 = "#3B82F6" as const;
537
+ // export const colorPrimary600 = "#2563EB" as const;
538
+ // export const spacing4 = "16px" as const;
539
+ // ...
540
+
541
+ // With themes:
542
+ // export const themes = {
543
+ // "dark": { colorPrimary500: "#60A5FA" as const, ... },
544
+ // "brand-b": { colorPrimary500: "#8B5CF6" as const, ... },
545
+ // } as const;
546
+ ```
547
+
548
+ #### SCSS export
549
+
550
+ ```typescript
551
+ exportTokens(tokens, "scss");
552
+ // // Auto-generated design tokens — do not edit manually
553
+ //
554
+ // $color-primary-500: #3B82F6;
555
+ // $spacing-4: 16px;
556
+ // ...
557
+
558
+ exportTokens(tokens, "scss", { prefix: "tok" });
559
+ // $tok-color-primary-500: #3B82F6;
560
+
561
+ // With themes — emits [data-theme] blocks using CSS custom properties:
562
+ // [data-theme="dark"] { --color-primary-500: #60A5FA; }
563
+ ```
564
+
565
+ #### Tailwind export
566
+
567
+ ```typescript
568
+ exportTokens(tokens, "tailwind");
569
+ // // Auto-generated design tokens — do not edit manually
570
+ // module.exports = {
571
+ // "theme": {
572
+ // "extend": {
573
+ // "color": { "primary": { "500": "#3B82F6", "600": "#2563EB" } },
574
+ // "spacing": { "4": "16px", "8": "32px" }
575
+ // }
576
+ // }
577
+ // };
578
+ ```
579
+
580
+ #### flat-json export
581
+
582
+ ```typescript
583
+ exportTokens(tokens, "flat-json");
584
+ // {
585
+ // "color.primary.500": "#3B82F6",
586
+ // "color.primary.600": "#2563EB",
587
+ // "spacing.4": "16px"
588
+ // }
589
+ ```
590
+
591
+ #### Figma export
592
+
593
+ ```typescript
594
+ exportTokens(tokens, "figma");
595
+ // {
596
+ // "global": {
597
+ // "color": {
598
+ // "primary": {
599
+ // "500": { "value": "#3B82F6", "type": "color" }
600
+ // }
601
+ // },
602
+ // "typography": {
603
+ // "fontFamily": {
604
+ // "sans": { "value": "Inter, sans-serif", "type": "fontFamily", "description": "Primary font" }
605
+ // }
606
+ // }
607
+ // },
608
+ // "dark": { "color": { "primary": { "500": { "value": "#60A5FA", "type": "color" } } } }
609
+ // }
610
+ ```
611
+
612
+ ---
613
+
614
+ ### `ThemeResolver`
615
+
616
+ Extends `TokenResolver` with named theme overlays.
617
+
618
+ #### Token file format — themes
619
+
620
+ Two supported theme formats:
621
+
622
+ **Flat override map** (original format):
623
+
624
+ ```json
625
+ {
626
+ "version": "0.1",
627
+ "tokens": { ... },
628
+ "themes": {
629
+ "dark": {
630
+ "color.primary.500": "#60A5FA",
631
+ "color.neutral.0": "#0F172A"
632
+ },
633
+ "brand-b": {
634
+ "color.primary.500": "#8B5CF6"
635
+ }
636
+ }
637
+ }
638
+ ```
639
+
640
+ **Nested DTCG-style** (structured format):
641
+
642
+ ```json
643
+ {
644
+ "version": "0.1",
645
+ "tokens": { ... },
646
+ "themes": {
647
+ "dark": {
648
+ "color": {
649
+ "primary": { "500": { "$value": "#60A5FA" } },
650
+ "neutral": { "0": { "$value": "#0F172A" } }
651
+ }
652
+ }
653
+ }
654
+ }
655
+ ```
656
+
657
+ #### `ThemeResolver.fromTokenFile(baseResolver, rawFile)`
658
+
659
+ ```typescript
660
+ static fromTokenFile(baseResolver: TokenResolver, rawFile: ThemedTokenFile): ThemeResolver
661
+ ```
662
+
663
+ Constructs a `ThemeResolver` from a `TokenResolver` and a raw token file (supports both flat and nested formats).
664
+
665
+ ```typescript
666
+ import { ThemeResolver } from "@agent-scope/tokens";
667
+
668
+ const { tokens, rawFile } = parseTokenFileSync(source);
669
+ const resolver = new TokenResolver(tokens);
670
+ const themeResolver = ThemeResolver.fromTokenFile(resolver, rawFile);
671
+
672
+ themeResolver.listThemes(); // ["dark", "brand-b"]
673
+ themeResolver.resolveThemed("color.primary.500", "dark"); // "#60A5FA"
674
+ themeResolver.resolveThemed("spacing.4", "dark"); // "16px" (falls back to base)
675
+ themeResolver.resolveAllThemes("color.primary.500");
676
+ // { base: "#3B82F6", dark: "#60A5FA", "brand-b": "#8B5CF6" }
677
+ ```
678
+
679
+ #### `ThemeResolver.fromThemeMap(baseResolver, themes)`
680
+
681
+ ```typescript
682
+ static fromThemeMap(
683
+ baseResolver: TokenResolver,
684
+ themes: Map<string, Map<string, string>>,
685
+ ): ThemeResolver
686
+ ```
687
+
688
+ Programmatic construction from a pre-built theme map.
689
+
690
+ #### `resolveThemed(path, themeName)`
691
+
692
+ Returns the value for the path in the given theme, falling back to base if the theme doesn't override it. Throws if the theme name is not registered.
693
+
694
+ #### `resolveAllThemes(path)`
695
+
696
+ Returns `{ base: string, [themeName]: string, ... }` — the resolved value in every theme.
697
+
698
+ #### `buildThemedTokens(themeName)`
699
+
700
+ Returns a full `Token[]` with the theme overrides applied (base values for non-overridden tokens).
701
+
702
+ #### Delegated methods
703
+
704
+ `ThemeResolver` also exposes `resolve(path)` and `list(type?, category?)` which delegate to the underlying `TokenResolver`.
705
+
706
+ ---
707
+
708
+ ### `validateTokenFile(raw)`
709
+
710
+ ```typescript
711
+ function validateTokenFile(raw: unknown): asserts raw is TokenFile
712
+ ```
713
+
714
+ Validates a raw parsed object against the `TokenFile` schema. Throws `TokenValidationError` with all collected issues if validation fails (collects all errors before throwing, not just the first).
715
+
716
+ Validation rules:
717
+ - Root value must be a non-null object
718
+ - `version` must be a string field
719
+ - `tokens` must be an object field
720
+ - Every leaf node inside `tokens` must have `value` (string or number) and `type` (one of the 8 valid types)
721
+ - `meta`, if present, must be an object
722
+ - `themes`, if present, must be a `Record<string, Record<string, string>>`
723
+
724
+ ```typescript
725
+ import { validateTokenFile, TokenValidationError } from "@agent-scope/tokens";
726
+
727
+ try {
728
+ validateTokenFile(raw);
729
+ // raw is now asserted as TokenFile
730
+ } catch (err) {
731
+ if (err instanceof TokenValidationError) {
732
+ for (const error of err.errors) {
733
+ console.error(`${error.path}: ${error.message} [${error.code}]`);
734
+ }
735
+ }
736
+ }
737
+ ```
738
+
739
+ ---
740
+
741
+ ### Error types
742
+
743
+ ```typescript
744
+ class TokenParseError extends Error {
745
+ readonly code: "CIRCULAR_REFERENCE" | "INVALID_REFERENCE" | "INVALID_SCHEMA" | "PARSE_ERROR";
746
+ readonly path?: string;
747
+ }
748
+
749
+ class TokenValidationError extends Error {
750
+ readonly errors: ValidationError[];
751
+ }
752
+
753
+ interface ValidationError {
754
+ path: string;
755
+ message: string;
756
+ code: string;
757
+ }
758
+ ```
759
+
760
+ ---
761
+
762
+ ## Complete example
763
+
764
+ ```typescript
765
+ import {
766
+ parseTokenFileSync,
767
+ TokenResolver,
768
+ ComplianceEngine,
769
+ ImpactAnalyzer,
770
+ exportTokens,
771
+ ThemeResolver,
772
+ } from "@agent-scope/tokens";
773
+ import { readFileSync } from "node:fs";
774
+
775
+ // 1. Parse token file
776
+ const source = readFileSync("reactscope.tokens.json", "utf8");
777
+ const { tokens, rawFile } = parseTokenFileSync(source);
778
+
779
+ // 2. Build resolver
780
+ const resolver = new TokenResolver(tokens);
781
+ resolver.resolve("color.primary.500"); // "#3B82F6"
782
+ resolver.match("#3B82F6", "color"); // exact match → TokenMatch
783
+ resolver.nearest("#3A80F0", "color"); // perceptually closest color
784
+
785
+ // 3. Export tokens
786
+ const css = exportTokens(tokens, "css");
787
+ const ts = exportTokens(tokens, "ts");
788
+ const scss = exportTokens(tokens, "scss");
789
+
790
+ // 4. Compliance audit
791
+ const engine = new ComplianceEngine(resolver);
792
+ const report = engine.audit({
793
+ colors: { background: "#3B82F6" },
794
+ spacing: { paddingTop: "16px" },
795
+ typography: { fontFamily: "Inter, sans-serif" },
796
+ borders: { borderRadius: "6px" },
797
+ shadows: { boxShadow: "0 4px 6px rgba(0,0,0,0.1)" },
798
+ });
799
+ console.log(report.compliance); // e.g. 1
800
+
801
+ // 5. Batch audit + impact analysis
802
+ const batchReport = engine.auditBatch(new Map([
803
+ ["Button", { colors: { background: "#3B82F6", color: "#FFFFFF" }, spacing: {}, typography: {}, borders: {}, shadows: {} }],
804
+ ["Card", { colors: { background: "#FFFFFF" }, spacing: { gap: "32px" }, typography: {}, borders: { borderRadius: "6px" }, shadows: {} }],
805
+ ]));
806
+
807
+ const analyzer = new ImpactAnalyzer(resolver, new Map(Object.entries(batchReport.components)));
808
+ const impact = analyzer.impactOf("color.primary.500", "#1D4ED8");
809
+ console.log(impact.affectedComponentCount); // 1
810
+ console.log(impact.overallSeverity); // "moderate"
811
+
812
+ // 6. Theme resolution
813
+ const themeResolver = ThemeResolver.fromTokenFile(resolver, rawFile);
814
+ themeResolver.resolveThemed("color.primary.500", "dark"); // "#60A5FA"
815
+ themeResolver.resolveAllThemes("color.primary.500"); // { base, dark, "brand-b" }
816
+ ```
817
+
818
+ ---
819
+
820
+ ## Internal architecture
821
+
822
+ | Module | Responsibility |
823
+ |--------|----------------|
824
+ | `types.ts` | All TypeScript types and error classes |
825
+ | `validator.ts` | Schema validation — collects all errors before throwing |
826
+ | `parser.ts` | JSON/YAML parsing → `flattenTokens` → `resolveValue` (DFS with cycle detection) |
827
+ | `resolver.ts` | `TokenResolver` — path lookup, `match`, `nearest`, `list` |
828
+ | `compliance.ts` | `ComplianceEngine` — style auditing against the token set |
829
+ | `impact.ts` | `ImpactAnalyzer` — downstream change analysis |
830
+ | `export.ts` | `exportTokens` — CSS, TS, SCSS, Tailwind, flat-JSON, Figma |
831
+ | `themes.ts` | `ThemeResolver` — flat and DTCG-style theme overlay resolution |
832
+ | `color-utils.ts` | `hexToLab`, `labDistance`, `parseColorToLab` — perceptual color math |
833
+
834
+ ---
835
+
836
+ ## Used by
837
+
838
+ - `@agent-scope/cli` — token compliance commands (`scope tokens compliance`, `scope tokens export`, `scope tokens impact`, `scope tokens preview`)
839
+ - `@agent-scope/site` — type imports for the Scope web UI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-scope/tokens",
3
- "version": "1.17.1",
3
+ "version": "1.17.3",
4
4
  "description": "Design token file parser, validator, and resolution engine for Scope",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "module": "./dist/index.js",
21
21
  "types": "./dist/index.d.ts",
22
22
  "files": [
23
- "dist"
23
+ "dist",
24
+ "README.md"
24
25
  ],
25
26
  "scripts": {
26
27
  "build": "tsup",