remarque-tokens 0.19.0 → 0.20.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/AGENT_RULES.md CHANGED
@@ -227,6 +227,91 @@ Before considering any implementation complete, verify:
227
227
 
228
228
  ---
229
229
 
230
+ ## Machine-Readable Output
231
+
232
+ Two tools emit structured JSON for agents/tooling instead of colored
233
+ stdout — pass `--json` and parse stdout as a single JSON document. Exit
234
+ codes are unchanged in both cases; `--json` only changes what stdout
235
+ contains (all human progress lines, including error lines normally on
236
+ stderr, are suppressed — nothing but the JSON document is written).
237
+
238
+ ### `remarque-audit --json`
239
+
240
+ ```
241
+ node scripts/audit.mjs --palette <file> --src <dir> --json
242
+ npx remarque-audit --palette <file> --src <dir> --json
243
+ ```
244
+
245
+ ```ts
246
+ {
247
+ version: string; // the remarque-tokens package version running the audit
248
+ palette: string; // the --palette path as given
249
+ src: string; // the --src path as given
250
+ passed: boolean; // true iff failures.length === 0
251
+ contrast: Array<{
252
+ theme: 'light' | 'dark';
253
+ fg: string; // fg token name, no leading --
254
+ bg: string; // bg token name, no leading --
255
+ label: string; // human label from the CHECKS table (e.g. "primary text")
256
+ required: number; // minimum ratio (e.g. 4.5, 7.0, 3.0)
257
+ actual: number | null; // computed ratio rounded to 2dp, or null if fg/bg was unresolvable
258
+ ok: boolean;
259
+ error?: string; // present only when actual is null (unresolved var()/missing token)
260
+ }>;
261
+ gamut: Array<{
262
+ theme: 'light' | 'dark';
263
+ token: string; // token name, no leading --
264
+ value: string; // e.g. "oklch(0.5 0.14 250)"
265
+ ok: boolean; // false = outside sRGB gamut (browsers will clip it)
266
+ }>;
267
+ srcScans: {
268
+ fontFloor: Array<{ file: string; line: number; size: number; unit: 'rem' | 'px' }>;
269
+ unverifiableFontSize: Array<{ file: string; line: number; snippet: string }>;
270
+ hardcodedColors: Array<{ file: string; line: number; snippet: string }>;
271
+ oklchLiteral: Array<{ file: string; line: number; snippet: string }>;
272
+ };
273
+ failures: string[]; // every failure as a human-readable message, same text the non-JSON run prints
274
+ }
275
+ ```
276
+
277
+ `contrast` and `gamut` list EVERY check performed (passing and failing);
278
+ only `srcScans` and `failures` are violation-only lists, since a source
279
+ scan that finds nothing has nothing to report per line.
280
+
281
+ ### `remarque-drift --json`
282
+
283
+ ```
284
+ node scripts/drift-check.mjs --css-file <path> --package-dir <dir> --json
285
+ npx remarque-drift --css-file <path> --package-dir <dir> --json
286
+ ```
287
+
288
+ ```ts
289
+ {
290
+ cssFile: string;
291
+ packageDir: string; // resolved node_modules/remarque-tokens path
292
+ installedVersion: string; // version of the INSTALLED remarque-tokens being checked against
293
+ deviationDoc: string | null; // path to DESIGN-DEVIATIONS.md / DESIGN-NOTES.md, or null if none found
294
+ passed: boolean; // true iff fail.length === 0
295
+ fail: Array<{ name: string; theme: 'light' | 'dark'; canonical: string; value: string }>;
296
+ warn: Array<{ name: string; theme: 'light' | 'dark'; canonical: string; value: string; doc: string }>;
297
+ info: Array<{ name: string; theme: 'light' | 'dark'; canonical: string; value: string }>;
298
+ summary: { fail: number; warn: number; info: number }; // counts of distinct TOKENS with an entry, not records (one token can produce two records — light + dark)
299
+ }
300
+ ```
301
+
302
+ `fail`/`warn`/`info` are one record per (token, theme) mismatch — see
303
+ `scripts/drift-check.mjs`'s doc comment and REMARQUE.md's "Token Tiers"
304
+ for the FAIL/WARN/INFO classification these mirror.
305
+
306
+ ### `tokens.json` / `tokens.schema.json`
307
+
308
+ `remarque-tokens/tokens.json` is always machine-readable (no flag
309
+ needed) and now ships with a published JSON Schema — see REMARQUE.md
310
+ "DTCG Conformance" for the schema's location, the `$schema` pointer, and
311
+ the documented DTCG divergences.
312
+
313
+ ---
314
+
230
315
  ## How to Reference This System
231
316
 
232
317
  In prompts, issues, or agent instructions, use:
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to `remarque-tokens` are documented here. Token value
4
4
  changes always state the design rationale — downstream sites pin against
5
5
  these entries when syncing.
6
6
 
7
+ ## 0.20.0 — 2026-07-23
8
+
9
+ Machine-readable surface: `--json` for `remarque-audit`/`remarque-drift`, and a published JSON Schema for `tokens.json` (closes #98, closes #99).
10
+
11
+ ### Added
12
+ - **`--json` on `remarque-audit`** (issue #98). Suppresses all human console output (both the progress lines and the error lines normally on stderr) and emits ONE JSON document to stdout instead — exit codes unchanged. Shape: `{ version, palette, src, passed, contrast: [{theme,fg,bg,label,required,actual,ok,error?}], gamut: [{theme,token,value,ok}], srcScans: {fontFloor,unverifiableFontSize,hardcodedColors,oklchLiteral}, failures: [] }`. `contrast`/`gamut` list every check performed (pass and fail); `srcScans`/`failures` are violation-only. Documented in `AGENT_RULES.md` ("Machine-Readable Output") — agents are the intended audience, parsing structure instead of scraping colored stdout.
13
+ - **`--json` on `remarque-drift`** (issue #98). Same suppression contract; emits `{ cssFile, packageDir, installedVersion, deviationDoc, passed, fail: [], warn: [], info: [], summary: {fail,warn,info} }` — one record per (token, theme) mismatch, mirroring the FAIL/WARN/INFO classification the human report already prints.
14
+ - **`tokens.schema.json`** (issue #99). JSON Schema (draft 2020-12) describing `tokens.json`'s actual shape, generated by `scripts/tokens-json.mjs` in the same pass as `tokens.json`/`tokens.d.ts`, same `--check` freshness gate. Token *names* are open (`patternProperties` on the kebab-case grammar); the `$value`/`$type`/light-dark VALUE shapes are fixed. Published as the `remarque-tokens/tokens.schema.json` export, shipped in `files`, and served by the demo site at `/tokens.schema.json` (alongside the existing `/tokens.json`) via an extended `site/scripts/copy-tokens-json.mjs` — shadcn's schema-URL precedent. `tokens.json` now carries a `"$schema"` pointer to that URL.
15
+ - **DTCG conformance note** (issue #99, ratified option ii). `tokens.json`'s `$extensions.remarque.dtcg` documents that the file is conformant in spirit with the Design Tokens Community Group format (`$value`/`$type` on every token) with two **deliberate** divergences — color values as `oklch()` CSS strings (not DTCG's structured color object) and per-token `light`/`dark` nesting (DTCG has no ratified multi-mode/resolver mechanism yet) — each with a named ratification trigger, so a future agent can't "fix" `tokens.json` toward an unratified target. Same argument restated for humans in REMARQUE.md's new "DTCG Conformance" section (under "Token Tiers").
16
+ - **Schema validation in CI** (`scripts/test-types.mjs`). Real JSON-Schema validation of `tokens.json` against `tokens.schema.json` via `ajv` (new devDependency, pinned to a version with zero known advisories; the `$data` option that the one applicable ajv CVE requires is never used) — plus three negative fixtures (unknown `$extensions` key, invalid `$type` enum value, missing required `dark` side) proving the schema actually rejects malformed input rather than accepting anything.
17
+ - **`--json` fixture coverage** (`scripts/test-audit.mjs`, `scripts/test-drift.mjs`). Parses the JSON output of a passing and a must-fail fixture for each tool; asserts the documented shape and that the must-fail fixture's offending pairing/token is actually present with `ok`/`passed: false`.
18
+
7
19
  ## 0.19.0 — 2026-07-23
8
20
 
9
21
  Palette Deck module (closes #56).
package/README.md CHANGED
@@ -44,6 +44,8 @@ Copy the `fonts/` woff2 files from `node_modules/remarque-tokens/fonts/` into yo
44
44
  npx remarque-audit --palette src/styles/my-palette.css --src src
45
45
  ```
46
46
 
47
+ Add `--json` to either `remarque-audit` or `remarque-drift` for a single structured JSON document on stdout instead of colored console output (exit codes unchanged) — for agents and CI tooling to parse rather than scrape. Shape documented in `AGENT_RULES.md` ("Machine-Readable Output").
48
+
47
49
  **Tailwind v4** projects add the shipped adapter — utilities that track the tokens through every theme switch, no value duplication:
48
50
 
49
51
  ```css
@@ -52,7 +54,7 @@ npx remarque-audit --palette src/styles/my-palette.css --src src
52
54
  @import "remarque-tokens/theme.css";
53
55
  ```
54
56
 
55
- **Tailwind v3** projects use the shipped config (`remarque-tokens/tailwind`) instead. A machine-readable token inventory ships as `remarque-tokens/tokens.json` (generated from the CSS — core/palette tiers, light+dark values) for tooling and AI agents. Prefer copy-paste? Grab `fonts.css`, `tokens.css`, `tokens-core.css`, `tokens-palette.css`, and `fonts/` directly — `tokens.css` aggregates the two tier files, so all three CSS files travel together. Use string-form `@import './tokens.css'` only (some bundlers silently drop `@import url(...)` for local files).
57
+ **Tailwind v3** projects use the shipped config (`remarque-tokens/tailwind`) instead. A machine-readable token inventory ships as `remarque-tokens/tokens.json` (generated from the CSS — core/palette tiers, light+dark values) for tooling and AI agents, with a published JSON Schema at `remarque-tokens/tokens.schema.json` (also referenced by tokens.json's own `$schema` field) so tooling can validate it structurally instead of hand-parsing. tokens.json is conformant in spirit with the [Design Tokens Community Group](https://www.designtokens.org/) format (`$value`/`$type` on every token), with two deliberate divergences — see REMARQUE.md's "DTCG Conformance" section. Prefer copy-paste? Grab `fonts.css`, `tokens.css`, `tokens-core.css`, `tokens-palette.css`, and `fonts/` directly — `tokens.css` aggregates the two tier files, so all three CSS files travel together. Use string-form `@import './tokens.css'` only (some bundlers silently drop `@import url(...)` for local files).
56
58
 
57
59
  ## For AI Agents
58
60
 
@@ -68,7 +70,7 @@ The agent rules define build order, non-negotiable rules, disallowed patterns, a
68
70
  Packaging for agent tooling:
69
71
  - **npm exports:** `remarque-tokens/agent-rules` (→ `AGENT_RULES.md`) and `remarque-tokens/spec` (→ `REMARQUE.md`), alongside the existing `remarque-tokens/tokens.json`, so a project can point an agent at `node_modules/remarque-tokens/AGENT_RULES.md` without hardcoding a filename.
70
72
  - **Claude Code skill:** [`.claude/skills/remarque/SKILL.md`](.claude/skills/remarque/SKILL.md) — triggers on "remarque" / "design system" / new-page work, loads all three files, and states the tier rules, the audit command, and the two build-time pitfalls (unlayered-token-import, string-form `@import`) that pass a green build while silently breaking.
71
- - **Live tokens endpoint:** the demo site serves the current `tokens.json` at **https://williamzujkowski.github.io/remarque/tokens.json** — a remote agent can fetch current token values directly instead of trusting training data.
73
+ - **Live tokens endpoint:** the demo site serves the current `tokens.json` at **https://williamzujkowski.github.io/remarque/tokens.json**, and its schema at **https://williamzujkowski.github.io/remarque/tokens.schema.json** — a remote agent can fetch current token values (and validate their shape) directly instead of trusting training data.
72
74
 
73
75
  ## Files
74
76
 
@@ -80,7 +82,10 @@ Packaging for agent tooling:
80
82
  | `tokens-core.css` | Core tier — type scale, spacing, widths, radius, motion, prose styling. Never overridden |
81
83
  | `tokens-palette.css` | Palette tier — font slots, colors, accent, reading measure. The sanctioned personalization surface |
82
84
  | `prose.css` | `.remarque-prose` long-form styling — own subpath so sites with their own prose system can skip it |
83
- | `scripts/audit.mjs` | `npm run audit` — enforces the spec's contrast/gamut/font-floor/no-hardcoded-color checklist |
85
+ | `scripts/audit.mjs` | `npm run audit` — enforces the spec's contrast/gamut/font-floor/no-hardcoded-color checklist (`--json` for structured output) |
86
+ | `scripts/drift-check.mjs` | `npx remarque-drift` — token drift check for consumers (`--json` for structured output) |
87
+ | `tokens.json` + `tokens.d.ts` | Generated machine-readable token inventory + TypeScript types (`scripts/tokens-json.mjs`) |
88
+ | `tokens.schema.json` | Generated JSON Schema (draft 2020-12) for `tokens.json`, published alongside it |
84
89
  | `fonts.css` + `fonts/` | Self-hosted @font-face declarations and woff2 files (no CDN requests) |
85
90
  | `tailwind.config.js` | Tailwind CSS **v3** configuration (v4 projects use an `@theme` block instead) |
86
91
  | `package.json` | npm package manifest for `remarque-tokens` |
package/REMARQUE.md CHANGED
@@ -68,6 +68,17 @@ Package subpaths for consumers: `remarque-tokens` (aggregator), `remarque-tokens
68
68
 
69
69
  This makes compliance mechanical: a site that overrides only palette-tier tokens is *authored*; one that touches core-tier tokens has *forked*. `tokens.css` imports both tiers, so existing consumers are unaffected.
70
70
 
71
+ ### DTCG Conformance
72
+
73
+ `tokens-core.css` and `tokens-palette.css` remain the single source of truth; `scripts/tokens-json.mjs` regenerates `tokens.json`, `tokens.d.ts`, and `tokens.schema.json` from them in one pass (`node scripts/tokens-json.mjs`; `--check` gates CI freshness). `tokens.json` carries a `$schema` pointer to the published schema (served by the demo site at `/tokens.schema.json`, alongside `/tokens.json` — also exported as `remarque-tokens/tokens.schema.json`).
74
+
75
+ `tokens.json` is **conformant in spirit** with the [Design Tokens Community Group (DTCG)](https://www.designtokens.org/) format — every token carries `$value`/`$type` — with two **deliberate** structural divergences, documented in the generated file itself (`$extensions.remarque.dtcg`) so a future regeneration can't lose the rationale:
76
+
77
+ 1. **Color values are `oklch()` CSS strings**, not the DTCG structured color object (`{ colorSpace, components, alpha }`). Remarque's color pipeline (audit, drift-check, `remarque-theme`) is built entirely on parsing/emitting `oklch(L C H)` strings; adopting the structured form would mean carrying two color representations in parallel for no present benefit. **Gated on:** the DTCG color `$type` structured-value format ratifying.
78
+ 2. **Palette-tier tokens nest per-token `light`/`dark` groups**, each with its own `$value`, rather than a single `$value` plus a modes/resolver mechanism. The DTCG spec has no ratified multi-mode/theming primitive yet; this repo's own theming (`@media (prefers-color-scheme: dark)`, `[data-theme="dark"]`, the Palette Deck's `[data-palette]` scoping) predates any such draft. **Gated on:** the DTCG multi-mode / resolver draft ratifying.
79
+
80
+ Full DTCG conformance is gated on those two drafts ratifying — a named, checkable trigger, not "someday." Until then, do not "fix" `tokens.json` toward either unratified draft; the CSS is the source of truth, and both divergences are load-bearing for the tooling that already depends on this shape.
81
+
71
82
  ---
72
83
 
73
84
  ## Color Providers
@@ -706,7 +717,8 @@ for the value-by-value mapping).
706
717
  <h2 id="section-one">Section One</h2>
707
718
  <p>
708
719
  A claim worth a citation<a href="#note-1" id="note-1-ref"
709
- class="remarque-sidenote-ref" aria-describedby="note-1"></a>.
720
+ class="remarque-sidenote-ref" aria-label="Note 1"
721
+ aria-describedby="note-1"></a>.
710
722
  </p>
711
723
  <aside class="remarque-sidenote" id="note-1" role="note">
712
724
  The note text, in real DOM order right after the paragraph that
@@ -729,6 +741,13 @@ Requirements the CSS depends on:
729
741
  `.remarque-sidenote` after it; later citations of the same note use
730
742
  `.remarque-sidenote-ref--repeat` (styled identically, does not advance
731
743
  the counter) so the shared numbering doesn't skip.
744
+ - `aria-label="Note N"` on every `.remarque-sidenote-ref`, numbered in
745
+ the same order the CSS counter advances — the ref's visible digit is
746
+ counter-generated, so the anchor itself has no text content, and an
747
+ empty focusable link with no accessible name fails WCAG 4.1.2 / 2.4.4
748
+ (axe: `link-name`). Because both the label and the counter derive from
749
+ DOM order, they cannot drift apart. (Found in the flagship's migration,
750
+ williamzujkowski.github.io#380.)
732
751
  - `role="note"` on every `.remarque-sidenote` — `<aside>`'s implicit role
733
752
  is `complementary`, a landmark; an essay with several notes would
734
753
  otherwise scatter that many unlabeled landmark regions through the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@williamzujkowski/oklch-terminal-themes": "0.4.0",
36
+ "ajv": "^8.20.0",
36
37
  "culori": "^4.0.2"
37
38
  },
38
39
  "peerDependencies": {
@@ -64,6 +65,7 @@
64
65
  "default": "./tokens.json"
65
66
  },
66
67
  "./tokens.d.ts": "./tokens.d.ts",
68
+ "./tokens.schema.json": "./tokens.schema.json",
67
69
  "./prose": "./prose.css",
68
70
  "./prose.css": "./prose.css",
69
71
  "./print": "./print.css",
@@ -97,6 +99,7 @@
97
99
  "theme.css",
98
100
  "tokens.json",
99
101
  "tokens.d.ts",
102
+ "tokens.schema.json",
100
103
  "scripts/tokens-json.mjs",
101
104
  "prose.css",
102
105
  "print.css",
package/scripts/audit.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  /*
3
3
  * remarque audit — enforces REMARQUE.md's checklist mechanically.
4
4
  *
5
- * node scripts/audit.mjs [--palette tokens-palette.css] [--src <dir>]
5
+ * node scripts/audit.mjs [--palette tokens-palette.css] [--src <dir>] [--json]
6
6
  * npx remarque-audit --palette your-palette.css --src src
7
7
  *
8
8
  * Checks (exit 1 on any failure):
@@ -19,6 +19,11 @@
19
19
  * 2. FONT FLOOR — no font-size declaration below 0.8125rem / 13px in src.
20
20
  * 3. NO HARDCODED COLORS — no hex/rgb()/hsl() literals in src styles;
21
21
  * oklch() literals are allowed only in token files.
22
+ *
23
+ * --json: suppresses all human console output and emits ONE JSON document
24
+ * to stdout instead (exit codes unchanged) — for agents/tooling to parse
25
+ * instead of scraping colored stdout. Shape documented in AGENT_RULES.md
26
+ * ("Machine-Readable Output — remarque-audit --json").
22
27
  */
23
28
 
24
29
  import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
@@ -33,11 +38,35 @@ function argOf(flag, dflt) {
33
38
  }
34
39
  const PALETTE = argOf('--palette', 'tokens-palette.css');
35
40
  const SRC = argOf('--src', existsSync('site/src') ? 'site/src' : '.');
41
+ const JSON_MODE = args.includes('--json');
42
+
43
+ // The audit's OWN package version (this script's remarque-tokens, resolved
44
+ // via import.meta.url so it is correct whether run in this repo or as
45
+ // npx remarque-audit from a consumer's node_modules/remarque-tokens) —
46
+ // not the consumer project's package.json.
47
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
48
+ const PKG_VERSION = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version;
49
+
50
+ const report = {
51
+ version: PKG_VERSION,
52
+ palette: PALETTE,
53
+ src: SRC,
54
+ passed: false,
55
+ contrast: [],
56
+ gamut: [],
57
+ srcScans: { fontFloor: [], unverifiableFontSize: [], hardcodedColors: [], oklchLiteral: [] },
58
+ failures: [],
59
+ };
60
+
61
+ function log(...a) {
62
+ if (!JSON_MODE) console.log(...a);
63
+ }
36
64
 
37
65
  let failures = 0;
38
66
  function fail(msg) {
39
67
  failures++;
40
- console.error(` ✗ ${msg}`);
68
+ report.failures.push(msg);
69
+ if (!JSON_MODE) console.error(` ✗ ${msg}`);
41
70
  }
42
71
 
43
72
  if (!existsSync(PALETTE)) {
@@ -48,6 +77,9 @@ if (!existsSync(SRC)) {
48
77
  console.error(`src directory not found: ${SRC} (use --src <dir>)`);
49
78
  process.exit(1);
50
79
  }
80
+ // These two checks are fatal usage errors (exit 1) unrelated to the
81
+ // checked project's compliance, so they print unconditionally even in
82
+ // --json mode — there is no report to emit yet.
51
83
 
52
84
  /* ── OKLCH → sRGB → WCAG luminance ──────────────────────── */
53
85
 
@@ -139,11 +171,14 @@ const CHECKS = [
139
171
  ];
140
172
 
141
173
  for (const [themeName, decls] of [['light', lightDecls], ['dark', darkDecls]]) {
142
- console.log(`\n${themeName} theme (${PALETTE})`);
174
+ log(`\n${themeName} theme (${PALETTE})`);
143
175
  // gamut check on every color-valued token
144
176
  for (const name of Object.keys(decls)) {
145
177
  const r = resolveColor(decls, name);
146
- if (r.color && !inGamut(oklchToLinearSrgb(...r.color))) {
178
+ if (!r.color) continue; // unresolvable tokens are covered by the contrast checks' own error path
179
+ const ok = inGamut(oklchToLinearSrgb(...r.color));
180
+ report.gamut.push({ theme: themeName, token: name, value: `oklch(${r.color.join(' ')})`, ok });
181
+ if (!ok) {
147
182
  fail(`--${name} oklch(${r.color.join(' ')}) is outside sRGB gamut — browsers will clip it; author an in-gamut value`);
148
183
  }
149
184
  }
@@ -151,12 +186,16 @@ for (const [themeName, decls] of [['light', lightDecls], ['dark', darkDecls]]) {
151
186
  const fg = resolveColor(decls, fgName);
152
187
  const bg = resolveColor(decls, bgName);
153
188
  if (fg.err || bg.err) {
189
+ report.contrast.push({ theme: themeName, fg: fgName, bg: bgName, label, required: min, actual: null, ok: false, error: fg.err || bg.err });
154
190
  fail(`${fgName}/${bgName} (${label}): ${fg.err || bg.err}`);
155
191
  continue;
156
192
  }
157
193
  const r = ratio(fg.color, bg.color);
158
- if (r < min) fail(`${fgName}/${bgName} = ${r.toFixed(2)}:1 < ${min}:1 (${label})`);
159
- else console.log(` ✓ ${fgName}/${bgName} = ${r.toFixed(2)}:1 (${label})`);
194
+ const actual = Math.round(r * 100) / 100;
195
+ const ok = r >= min;
196
+ report.contrast.push({ theme: themeName, fg: fgName, bg: bgName, label, required: min, actual, ok });
197
+ if (!ok) fail(`${fgName}/${bgName} = ${r.toFixed(2)}:1 < ${min}:1 (${label})`);
198
+ else log(` ✓ ${fgName}/${bgName} = ${r.toFixed(2)}:1 (${label})`);
160
199
  }
161
200
  }
162
201
 
@@ -171,7 +210,7 @@ function* walk(dir) {
171
210
  }
172
211
  }
173
212
 
174
- console.log(`\nsource scans (${SRC})`);
213
+ log(`\nsource scans (${SRC})`);
175
214
  const FONT_FLOOR_REM = 0.8125, FONT_FLOOR_PX = 13;
176
215
  // tokens.astro is the token *reference page* — its job is displaying literal
177
216
  // values (issue #48 tracks generating it from a machine-readable source).
@@ -198,26 +237,36 @@ for (const file of walk(SRC)) {
198
237
  for (const m of line.matchAll(/font-size:\s*([\d.]+)(rem|px)/g)) {
199
238
  const v = parseFloat(m[1]);
200
239
  if ((m[2] === 'rem' && v < FONT_FLOOR_REM) || (m[2] === 'px' && v < FONT_FLOOR_PX)) {
240
+ report.srcScans.fontFloor.push({ file: rel, line: i + 1, size: v, unit: m[2] });
201
241
  fail(`${rel}:${i + 1} font-size ${m[1]}${m[2]} below the 13px floor`);
202
242
  }
203
243
  }
204
244
  if (!isTokenFile(rel) && /font-size:\s*(clamp\(|[\d.]+%)/.test(line)) {
245
+ report.srcScans.unverifiableFontSize.push({ file: rel, line: i + 1, snippet: line.trim().slice(0, 80) });
205
246
  fail(`${rel}:${i + 1} statically unverifiable font-size (clamp/%) — use var(--text-*) tokens: ${line.trim().slice(0, 80)}`);
206
247
  }
207
248
  // 3. hardcoded colors (hex / rgb / hsl anywhere; oklch outside token files).
208
249
  // Note: hex fills/strokes in inline SVG are flagged deliberately —
209
250
  // Remarque icons use currentColor, per the no-hardcoded-colors rule.
210
251
  if (/(#[0-9a-fA-F]{3,8}\b(?![0-9a-zA-Z]))|(\brgba?\()|(\bhsla?\()/.test(line) && !/xmlns|href|url\(#|\{#/.test(line)) {
252
+ report.srcScans.hardcodedColors.push({ file: rel, line: i + 1, snippet: line.trim().slice(0, 80) });
211
253
  fail(`${rel}:${i + 1} hardcoded hex/rgb/hsl color: ${line.trim().slice(0, 80)}`);
212
254
  }
213
255
  if (!isTokenFile(rel) && /\boklch\(/.test(line) && !line.includes('var(--')) {
256
+ report.srcScans.oklchLiteral.push({ file: rel, line: i + 1, snippet: line.trim().slice(0, 80) });
214
257
  fail(`${rel}:${i + 1} oklch() literal outside token files: ${line.trim().slice(0, 80)}`);
215
258
  }
216
259
  });
217
260
  }
218
261
 
219
- if (failures) {
262
+ report.passed = failures === 0;
263
+
264
+ if (JSON_MODE) {
265
+ console.log(JSON.stringify(report, null, 2));
266
+ } else if (failures) {
220
267
  console.error(`\naudit FAILED — ${failures} problem(s)\n`);
221
- process.exit(1);
268
+ } else {
269
+ console.log('\naudit passed ✓\n');
222
270
  }
223
- console.log('\naudit passed ✓\n');
271
+
272
+ if (failures) process.exit(1);
@@ -2,7 +2,7 @@
2
2
  /*
3
3
  * remarque-drift — token drift check for consumers of remarque-tokens.
4
4
  *
5
- * node scripts/drift-check.mjs --css-file <path> --package-dir <dir>
5
+ * node scripts/drift-check.mjs --css-file <path> --package-dir <dir> [--json]
6
6
  * npx remarque-drift --css-file src/styles/global.css --package-dir .
7
7
  *
8
8
  * Compares a consumer's stylesheet against the INSTALLED remarque-tokens
@@ -27,6 +27,11 @@
27
27
  *
28
28
  * Exit code: 1 iff there is at least one undocumented (FAIL) core-tier
29
29
  * mismatch. WARN and INFO never fail the build.
30
+ *
31
+ * --json: suppresses all human console output and emits ONE JSON document
32
+ * to stdout instead (exit codes unchanged) — records mirror the FAIL/
33
+ * WARN/INFO classification above. Shape documented in AGENT_RULES.md
34
+ * ("Machine-Readable Output — remarque-drift --json").
30
35
  */
31
36
 
32
37
  import { readFileSync, existsSync } from 'node:fs';
@@ -41,6 +46,7 @@ function argOf(flag, dflt) {
41
46
 
42
47
  const CSS_FILE = argOf('--css-file', null);
43
48
  const PACKAGE_DIR = resolve(argOf('--package-dir', '.'));
49
+ const JSON_MODE = args.includes('--json');
44
50
 
45
51
  if (!CSS_FILE) {
46
52
  console.error('usage: drift-check.mjs --css-file <path> --package-dir <dir>');
@@ -162,6 +168,27 @@ for (const [name, tok] of Object.entries(tokens.palette || {})) {
162
168
 
163
169
  /* ── Report ─────────────────────────────────────────────────────────── */
164
170
 
171
+ if (JSON_MODE) {
172
+ // Flatten the internal per-token fails/warns/infos (each may bundle a
173
+ // light+dark mismatch) into one record per theme mismatch — the FAIL/
174
+ // WARN/INFO records the human report prints one line per, above.
175
+ const fail = fails.flatMap((f) => f.mismatches.map((m) => ({ name: f.name, theme: m.theme, canonical: f.canonical, value: m.value })));
176
+ const warn = warns.flatMap((w) => w.mismatches.map((m) => ({ name: w.name, theme: m.theme, canonical: w.canonical, value: m.value, doc: w.doc })));
177
+ const info = infos.flatMap((i) => i.diffs.map((d) => ({ name: i.name, theme: d.theme, canonical: d.theme === 'light' ? i.canonicalLight : i.canonicalDark, value: d.value })));
178
+ console.log(JSON.stringify({
179
+ cssFile: CSS_FILE,
180
+ packageDir: PKG_ROOT,
181
+ installedVersion,
182
+ deviationDoc: deviationDocPath,
183
+ passed: fails.length === 0,
184
+ fail,
185
+ warn,
186
+ info,
187
+ summary: { fail: fails.length, warn: warns.length, info: infos.length },
188
+ }, null, 2));
189
+ process.exit(fails.length ? 1 : 0);
190
+ }
191
+
165
192
  console.log('remarque-drift — token drift check');
166
193
  console.log(` consumer stylesheet: ${CSS_FILE}`);
167
194
  console.log(` installed remarque-tokens: ${installedVersion} (${PKG_ROOT})`);
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * Generates tokens.json (W3C design-tokens flavored) and tokens.d.ts
4
- * (TypeScript literal-union types) FROM the CSS — tokens-core.css and
5
- * tokens-palette.css remain the single source of truth; both files
3
+ * Generates tokens.json (DTCG/W3C design-tokens flavored), tokens.d.ts
4
+ * (TypeScript literal-union types), and tokens.schema.json (JSON Schema
5
+ * draft 2020-12 for tokens.json) FROM the CSS tokens-core.css and
6
+ * tokens-palette.css remain the single source of truth; all three files
6
7
  * here are derived output for tooling, AI agents, and TS editors.
7
8
  *
8
- * node scripts/tokens-json.mjs # (re)write tokens.json + tokens.d.ts
9
- * node scripts/tokens-json.mjs --check # exit 1 if either is stale (CI)
9
+ * node scripts/tokens-json.mjs # (re)write tokens.json + tokens.d.ts + tokens.schema.json
10
+ * node scripts/tokens-json.mjs --check # exit 1 if any of the three is stale (CI)
10
11
  *
11
12
  * Multi-theme values use per-token light/dark groups with $value on each
12
- * (the W3C spec has no native theming; this follows its $extensions
13
- * escape hatch conservatively).
13
+ * (the DTCG/W3C spec has no native theming; this follows its $extensions
14
+ * escape hatch conservatively). tokens.json is CONFORMANT IN SPIRIT with
15
+ * the Design Tokens Community Group format ($value/$type on every token)
16
+ * but has two deliberate divergences — see the `dtcg` block below and
17
+ * REMARQUE.md's "DTCG Conformance" section for the full rationale and the
18
+ * named ratification triggers that would close each gap.
14
19
  */
15
20
 
16
21
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
@@ -37,13 +42,41 @@ function valueFor(type, value) {
37
42
  return value;
38
43
  }
39
44
 
45
+ // Published alongside tokens.json/tokens.schema.json by the demo site
46
+ // (site/scripts/copy-tokens-json.mjs) — issue #99's "shadcn schema-URL
47
+ // precedent". Bump the path only in lockstep with an actual site move.
48
+ const SCHEMA_URL = 'https://williamzujkowski.github.io/remarque/tokens.schema.json';
49
+
40
50
  const version = JSON.parse(readFileSync('package.json', 'utf8')).version;
41
51
  const coreDecls = declsOf(extractBlocks(readFileSync('tokens-core.css', 'utf8')), isLightRoot);
42
52
  const paletteBlocks = extractBlocks(readFileSync('tokens-palette.css', 'utf8'));
43
53
  const lightDecls = declsOf(paletteBlocks, isLightRoot);
44
54
  const darkOverrides = declsOf(paletteBlocks, isDarkBlock);
45
55
 
56
+ // DTCG conformance note (issue #99, ratified option ii): documented IN the
57
+ // generated artifact, not just in prose, so a future agent regenerating
58
+ // tokens.json can't lose it. Two deliberate divergences from the Design
59
+ // Tokens Community Group draft, each with a named ratification trigger —
60
+ // full conformance is gated on those drafts landing, not "someday soon."
61
+ const DTCG_NOTE = {
62
+ conformance: 'partial — $value/$type present on every token (conformant in spirit); two deliberate structural divergences below',
63
+ divergences: [
64
+ {
65
+ aspect: 'color-value-encoding',
66
+ detail: 'Color $value is an oklch() CSS string, not the DTCG structured color object ({ colorSpace, components, alpha }).',
67
+ gatedOn: 'DTCG color $type structured-value format ratifying',
68
+ },
69
+ {
70
+ aspect: 'multi-mode-theming',
71
+ detail: 'Palette-tier tokens nest per-token { light: {$value}, dark: {$value} } groups instead of a single $value plus a modes/resolver mechanism.',
72
+ gatedOn: 'DTCG multi-mode / resolver draft ratifying',
73
+ },
74
+ ],
75
+ note: 'Deliberate, not oversight — see REMARQUE.md "DTCG Conformance" for the argument. Do not "fix" these toward the current unratified drafts; re-derive from the CSS via scripts/tokens-json.mjs instead.',
76
+ };
77
+
46
78
  const out = {
79
+ $schema: SCHEMA_URL,
47
80
  $description: 'Remarque design tokens — GENERATED from tokens-core.css + tokens-palette.css by scripts/tokens-json.mjs. Do not edit; the CSS is the source of truth.',
48
81
  $extensions: {
49
82
  remarque: {
@@ -52,6 +85,7 @@ const out = {
52
85
  core: 'immutable identity — overriding forks the system',
53
86
  palette: 'sanctioned personalization surface — override freely, then run remarque-audit',
54
87
  },
88
+ dtcg: DTCG_NOTE,
55
89
  },
56
90
  },
57
91
  core: {},
@@ -171,6 +205,7 @@ export interface RemarquePaletteTokenEntry {
171
205
  * below applying, etc).
172
206
  */
173
207
  export interface RemarqueTokensFile {
208
+ readonly $schema: string;
174
209
  readonly $description: string;
175
210
  readonly $extensions: {
176
211
  readonly remarque: {
@@ -179,6 +214,16 @@ export interface RemarqueTokensFile {
179
214
  readonly core: string;
180
215
  readonly palette: string;
181
216
  };
217
+ /** DTCG conformance note (issue #99) — see REMARQUE.md "DTCG Conformance". */
218
+ readonly dtcg: {
219
+ readonly conformance: string;
220
+ readonly divergences: ReadonlyArray<{
221
+ readonly aspect: string;
222
+ readonly detail: string;
223
+ readonly gatedOn: string;
224
+ }>;
225
+ readonly note: string;
226
+ };
182
227
  };
183
228
  };
184
229
  readonly core: { readonly [K in RemarqueCoreToken]: RemarqueCoreTokenEntry };
@@ -212,19 +257,155 @@ declare module 'remarque-tokens/tokens.json' {
212
257
 
213
258
  const dts = renderDts(out, version);
214
259
 
260
+ /* ── tokens.schema.json ────────────────────────────────────────────────
261
+ * JSON Schema draft 2020-12 describing tokens.json's ACTUAL shape —
262
+ * generated (not hand-maintained) so it can never drift from what this
263
+ * script emits. Token *names* are open (patternProperties on the
264
+ * kebab-case grammar every generated name follows) rather than an
265
+ * enumerated list of the current token set, so adding/removing a token
266
+ * in the CSS doesn't require a schema edit — only its documented VALUE
267
+ * shape ($value/$type, the light/dark palette grouping, the $extensions
268
+ * escape hatches) is fixed.
269
+ */
270
+ function renderSchema() {
271
+ const kebabName = '^[a-z][a-z0-9-]*$';
272
+ const scalarValue = { oneOf: [{ type: 'string' }, { type: 'number' }] };
273
+ return {
274
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
275
+ $id: SCHEMA_URL,
276
+ title: 'Remarque design tokens (tokens.json)',
277
+ description:
278
+ 'JSON Schema for remarque-tokens\' generated tokens.json — GENERATED by scripts/tokens-json.mjs, do not hand-edit. ' +
279
+ 'Conformant in spirit with the Design Tokens Community Group format ($value/$type on every token), with two ' +
280
+ 'deliberate divergences (oklch()-string color values; per-token light/dark nesting) — see the tokens.json ' +
281
+ '$extensions.remarque.dtcg block and REMARQUE.md "DTCG Conformance" for the full rationale and ratification triggers.',
282
+ type: 'object',
283
+ additionalProperties: false,
284
+ required: ['$description', '$extensions', 'core', 'palette'],
285
+ properties: {
286
+ // No format: 'uri' keyword — ajv's strict mode rejects unknown
287
+ // formats without the separate ajv-formats package, and this
288
+ // project deliberately keeps the schema-validation devDependency
289
+ // surface to ajv alone (see scripts/test-types.mjs).
290
+ $schema: { type: 'string' },
291
+ $description: { type: 'string' },
292
+ $extensions: {
293
+ type: 'object',
294
+ additionalProperties: false,
295
+ required: ['remarque'],
296
+ properties: {
297
+ remarque: {
298
+ type: 'object',
299
+ additionalProperties: false,
300
+ required: ['version', 'tiers', 'dtcg'],
301
+ properties: {
302
+ version: { type: 'string' },
303
+ tiers: {
304
+ type: 'object',
305
+ additionalProperties: false,
306
+ required: ['core', 'palette'],
307
+ properties: { core: { type: 'string' }, palette: { type: 'string' } },
308
+ },
309
+ dtcg: {
310
+ type: 'object',
311
+ additionalProperties: false,
312
+ required: ['conformance', 'divergences', 'note'],
313
+ properties: {
314
+ conformance: { type: 'string' },
315
+ divergences: {
316
+ type: 'array',
317
+ items: {
318
+ type: 'object',
319
+ additionalProperties: false,
320
+ required: ['aspect', 'detail', 'gatedOn'],
321
+ properties: {
322
+ aspect: { type: 'string' },
323
+ detail: { type: 'string' },
324
+ gatedOn: { type: 'string' },
325
+ },
326
+ },
327
+ },
328
+ note: { type: 'string' },
329
+ },
330
+ },
331
+ },
332
+ },
333
+ },
334
+ },
335
+ core: {
336
+ type: 'object',
337
+ additionalProperties: false,
338
+ patternProperties: { [kebabName]: { $ref: '#/$defs/coreEntry' } },
339
+ },
340
+ palette: {
341
+ type: 'object',
342
+ additionalProperties: false,
343
+ patternProperties: { [kebabName]: { $ref: '#/$defs/paletteEntry' } },
344
+ },
345
+ },
346
+ $defs: {
347
+ tokenType: {
348
+ type: 'string',
349
+ enum: ['color', 'dimension', 'number', 'fontFamily', 'fontWeight', 'duration', 'string'],
350
+ },
351
+ coreEntry: {
352
+ type: 'object',
353
+ additionalProperties: false,
354
+ required: ['$value', '$type'],
355
+ properties: { $value: scalarValue, $type: { $ref: '#/$defs/tokenType' } },
356
+ },
357
+ paletteSideValue: {
358
+ type: 'object',
359
+ additionalProperties: false,
360
+ required: ['$value'],
361
+ properties: {
362
+ $value: scalarValue,
363
+ $extensions: {
364
+ type: 'object',
365
+ additionalProperties: false,
366
+ properties: {
367
+ remarque: {
368
+ type: 'object',
369
+ additionalProperties: false,
370
+ properties: { inheritedFromLight: { type: 'boolean' } },
371
+ },
372
+ },
373
+ },
374
+ },
375
+ },
376
+ paletteEntry: {
377
+ type: 'object',
378
+ additionalProperties: false,
379
+ required: ['$type', 'light', 'dark'],
380
+ properties: {
381
+ $type: { $ref: '#/$defs/tokenType' },
382
+ light: { $ref: '#/$defs/paletteSideValue' },
383
+ dark: { $ref: '#/$defs/paletteSideValue' },
384
+ },
385
+ },
386
+ },
387
+ };
388
+ }
389
+
390
+ const schema = JSON.stringify(renderSchema(), null, 2) + '\n';
391
+
215
392
  if (process.argv.includes('--check')) {
216
393
  const currentJson = existsSync('tokens.json') ? readFileSync('tokens.json', 'utf8') : '';
217
394
  const currentDts = existsSync('tokens.d.ts') ? readFileSync('tokens.d.ts', 'utf8') : '';
395
+ const currentSchema = existsSync('tokens.schema.json') ? readFileSync('tokens.schema.json', 'utf8') : '';
218
396
  const staleJson = currentJson !== json;
219
397
  const staleDts = currentDts !== dts;
220
- if (staleJson || staleDts) {
398
+ const staleSchema = currentSchema !== schema;
399
+ if (staleJson || staleDts || staleSchema) {
221
400
  if (staleJson) console.error('tokens.json is stale — run: node scripts/tokens-json.mjs');
222
401
  if (staleDts) console.error('tokens.d.ts is stale — run: node scripts/tokens-json.mjs');
402
+ if (staleSchema) console.error('tokens.schema.json is stale — run: node scripts/tokens-json.mjs');
223
403
  process.exit(1);
224
404
  }
225
- console.log('tokens.json and tokens.d.ts are fresh ✓');
405
+ console.log('tokens.json, tokens.d.ts, and tokens.schema.json are fresh ✓');
226
406
  } else {
227
407
  writeFileSync('tokens.json', json);
228
408
  writeFileSync('tokens.d.ts', dts);
229
- console.log(`tokens.json + tokens.d.ts written — ${Object.keys(out.core).length} core + ${Object.keys(out.palette).length} palette tokens (v${version})`);
409
+ writeFileSync('tokens.schema.json', schema);
410
+ console.log(`tokens.json + tokens.d.ts + tokens.schema.json written — ${Object.keys(out.core).length} core + ${Object.keys(out.palette).length} palette tokens (v${version})`);
230
411
  }
package/tokens.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Remarque design tokens — GENERATED from tokens.json by
3
- * scripts/tokens-json.mjs (v0.19.0). Do not edit — the CSS
3
+ * scripts/tokens-json.mjs (v0.20.0). Do not edit — the CSS
4
4
  * (tokens-core.css + tokens-palette.css) is the source of truth;
5
5
  * tokens.json is the intermediate machine-readable form this file is
6
6
  * generated from. Regenerate with: node scripts/tokens-json.mjs
@@ -152,6 +152,7 @@ export interface RemarquePaletteTokenEntry {
152
152
  * below applying, etc).
153
153
  */
154
154
  export interface RemarqueTokensFile {
155
+ readonly $schema: string;
155
156
  readonly $description: string;
156
157
  readonly $extensions: {
157
158
  readonly remarque: {
@@ -160,6 +161,16 @@ export interface RemarqueTokensFile {
160
161
  readonly core: string;
161
162
  readonly palette: string;
162
163
  };
164
+ /** DTCG conformance note (issue #99) — see REMARQUE.md "DTCG Conformance". */
165
+ readonly dtcg: {
166
+ readonly conformance: string;
167
+ readonly divergences: ReadonlyArray<{
168
+ readonly aspect: string;
169
+ readonly detail: string;
170
+ readonly gatedOn: string;
171
+ }>;
172
+ readonly note: string;
173
+ };
163
174
  };
164
175
  };
165
176
  readonly core: { readonly [K in RemarqueCoreToken]: RemarqueCoreTokenEntry };
package/tokens.json CHANGED
@@ -1,11 +1,28 @@
1
1
  {
2
+ "$schema": "https://williamzujkowski.github.io/remarque/tokens.schema.json",
2
3
  "$description": "Remarque design tokens — GENERATED from tokens-core.css + tokens-palette.css by scripts/tokens-json.mjs. Do not edit; the CSS is the source of truth.",
3
4
  "$extensions": {
4
5
  "remarque": {
5
- "version": "0.19.0",
6
+ "version": "0.20.0",
6
7
  "tiers": {
7
8
  "core": "immutable identity — overriding forks the system",
8
9
  "palette": "sanctioned personalization surface — override freely, then run remarque-audit"
10
+ },
11
+ "dtcg": {
12
+ "conformance": "partial — $value/$type present on every token (conformant in spirit); two deliberate structural divergences below",
13
+ "divergences": [
14
+ {
15
+ "aspect": "color-value-encoding",
16
+ "detail": "Color $value is an oklch() CSS string, not the DTCG structured color object ({ colorSpace, components, alpha }).",
17
+ "gatedOn": "DTCG color $type structured-value format ratifying"
18
+ },
19
+ {
20
+ "aspect": "multi-mode-theming",
21
+ "detail": "Palette-tier tokens nest per-token { light: {$value}, dark: {$value} } groups instead of a single $value plus a modes/resolver mechanism.",
22
+ "gatedOn": "DTCG multi-mode / resolver draft ratifying"
23
+ }
24
+ ],
25
+ "note": "Deliberate, not oversight — see REMARQUE.md \"DTCG Conformance\" for the argument. Do not \"fix\" these toward the current unratified drafts; re-derive from the CSS via scripts/tokens-json.mjs instead."
9
26
  }
10
27
  }
11
28
  },
@@ -0,0 +1,210 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://williamzujkowski.github.io/remarque/tokens.schema.json",
4
+ "title": "Remarque design tokens (tokens.json)",
5
+ "description": "JSON Schema for remarque-tokens' generated tokens.json — GENERATED by scripts/tokens-json.mjs, do not hand-edit. Conformant in spirit with the Design Tokens Community Group format ($value/$type on every token), with two deliberate divergences (oklch()-string color values; per-token light/dark nesting) — see the tokens.json $extensions.remarque.dtcg block and REMARQUE.md \"DTCG Conformance\" for the full rationale and ratification triggers.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "$description",
10
+ "$extensions",
11
+ "core",
12
+ "palette"
13
+ ],
14
+ "properties": {
15
+ "$schema": {
16
+ "type": "string"
17
+ },
18
+ "$description": {
19
+ "type": "string"
20
+ },
21
+ "$extensions": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": [
25
+ "remarque"
26
+ ],
27
+ "properties": {
28
+ "remarque": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": [
32
+ "version",
33
+ "tiers",
34
+ "dtcg"
35
+ ],
36
+ "properties": {
37
+ "version": {
38
+ "type": "string"
39
+ },
40
+ "tiers": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "required": [
44
+ "core",
45
+ "palette"
46
+ ],
47
+ "properties": {
48
+ "core": {
49
+ "type": "string"
50
+ },
51
+ "palette": {
52
+ "type": "string"
53
+ }
54
+ }
55
+ },
56
+ "dtcg": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": [
60
+ "conformance",
61
+ "divergences",
62
+ "note"
63
+ ],
64
+ "properties": {
65
+ "conformance": {
66
+ "type": "string"
67
+ },
68
+ "divergences": {
69
+ "type": "array",
70
+ "items": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": [
74
+ "aspect",
75
+ "detail",
76
+ "gatedOn"
77
+ ],
78
+ "properties": {
79
+ "aspect": {
80
+ "type": "string"
81
+ },
82
+ "detail": {
83
+ "type": "string"
84
+ },
85
+ "gatedOn": {
86
+ "type": "string"
87
+ }
88
+ }
89
+ }
90
+ },
91
+ "note": {
92
+ "type": "string"
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ },
100
+ "core": {
101
+ "type": "object",
102
+ "additionalProperties": false,
103
+ "patternProperties": {
104
+ "^[a-z][a-z0-9-]*$": {
105
+ "$ref": "#/$defs/coreEntry"
106
+ }
107
+ }
108
+ },
109
+ "palette": {
110
+ "type": "object",
111
+ "additionalProperties": false,
112
+ "patternProperties": {
113
+ "^[a-z][a-z0-9-]*$": {
114
+ "$ref": "#/$defs/paletteEntry"
115
+ }
116
+ }
117
+ }
118
+ },
119
+ "$defs": {
120
+ "tokenType": {
121
+ "type": "string",
122
+ "enum": [
123
+ "color",
124
+ "dimension",
125
+ "number",
126
+ "fontFamily",
127
+ "fontWeight",
128
+ "duration",
129
+ "string"
130
+ ]
131
+ },
132
+ "coreEntry": {
133
+ "type": "object",
134
+ "additionalProperties": false,
135
+ "required": [
136
+ "$value",
137
+ "$type"
138
+ ],
139
+ "properties": {
140
+ "$value": {
141
+ "oneOf": [
142
+ {
143
+ "type": "string"
144
+ },
145
+ {
146
+ "type": "number"
147
+ }
148
+ ]
149
+ },
150
+ "$type": {
151
+ "$ref": "#/$defs/tokenType"
152
+ }
153
+ }
154
+ },
155
+ "paletteSideValue": {
156
+ "type": "object",
157
+ "additionalProperties": false,
158
+ "required": [
159
+ "$value"
160
+ ],
161
+ "properties": {
162
+ "$value": {
163
+ "oneOf": [
164
+ {
165
+ "type": "string"
166
+ },
167
+ {
168
+ "type": "number"
169
+ }
170
+ ]
171
+ },
172
+ "$extensions": {
173
+ "type": "object",
174
+ "additionalProperties": false,
175
+ "properties": {
176
+ "remarque": {
177
+ "type": "object",
178
+ "additionalProperties": false,
179
+ "properties": {
180
+ "inheritedFromLight": {
181
+ "type": "boolean"
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ },
189
+ "paletteEntry": {
190
+ "type": "object",
191
+ "additionalProperties": false,
192
+ "required": [
193
+ "$type",
194
+ "light",
195
+ "dark"
196
+ ],
197
+ "properties": {
198
+ "$type": {
199
+ "$ref": "#/$defs/tokenType"
200
+ },
201
+ "light": {
202
+ "$ref": "#/$defs/paletteSideValue"
203
+ },
204
+ "dark": {
205
+ "$ref": "#/$defs/paletteSideValue"
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }