@radicool/throughline 0.19.0 → 0.21.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.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/adapters/codex/prompts/component-builder.md +29 -2
  3. package/adapters/codex/prompts/document-component.md +10 -1
  4. package/adapters/codex/prompts/icon-system-builder.md +4 -0
  5. package/adapters/codex/prompts/storybook-chromatic-builder.md +114 -7
  6. package/adapters/codex/prompts/token-builder.md +110 -3
  7. package/adapters/codex/prompts/token-crosswalk-builder.md +2 -0
  8. package/adapters/codex/prompts/token-sheet-builder.md +4 -0
  9. package/adapters/codex/prompts/token-sync-layer.md +140 -20
  10. package/adapters/cursor/.cursor/commands/document-component.md +10 -1
  11. package/adapters/cursor/.cursor/rules/component-builder.mdc +29 -2
  12. package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +4 -0
  13. package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +114 -7
  14. package/adapters/cursor/.cursor/rules/token-builder.mdc +110 -3
  15. package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +2 -0
  16. package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +4 -0
  17. package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +140 -20
  18. package/adapters/generic/commands/document-component.md +10 -1
  19. package/adapters/generic/skills/component-builder/SKILL.md +29 -2
  20. package/adapters/generic/skills/icon-system-builder/SKILL.md +4 -0
  21. package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +114 -7
  22. package/adapters/generic/skills/token-builder/SKILL.md +110 -3
  23. package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +2 -0
  24. package/adapters/generic/skills/token-sheet-builder/SKILL.md +4 -0
  25. package/adapters/generic/skills/token-sync-layer/SKILL.md +140 -20
  26. package/package.json +1 -1
  27. package/references/component-doc-schema.md +11 -1
  28. package/references/figma-component-standards.md +61 -2
  29. package/references/figma-scripting.md +12 -2
  30. package/references/manifest-schema.md +42 -2
  31. package/references/proof-bundle.md +312 -0
  32. package/references/sync-adapters.md +38 -53
  33. package/scripts/README.md +150 -14
  34. package/scripts/build-doc-card-builder.mjs +1 -1
  35. package/scripts/build-docs-digest.mjs +1 -0
  36. package/scripts/grep-color-usage.mjs +10 -30
  37. package/scripts/guard-token-removal.mjs +10 -26
  38. package/scripts/lib/component-states.mjs +56 -0
  39. package/scripts/lib/contrast.mjs +117 -0
  40. package/scripts/lib/dtcg.mjs +3 -2
  41. package/scripts/lib/proof.mjs +186 -0
  42. package/scripts/lib/source-scan.mjs +45 -0
  43. package/scripts/validate-adherence.mjs +1178 -0
  44. package/scripts/validate-token-output.mjs +468 -63
  45. package/scripts/verify-check.mjs +718 -0
@@ -105,6 +105,10 @@ component once its slice of the spec is complete enough to transcribe. **Keep th
105
105
  human checkpoint between components** — subagents run continuously within a
106
106
  component, but you pause for the human between them. If your host has no subagent
107
107
  dispatch, build and verify each component inline, sequentially, as before.
108
+ Inline, run the active-file preflight in
109
+ `.throughline/references/figma-scripting.md` before the first write and
110
+ again before replacing anything — the executor's file check does not come with
111
+ you.
108
112
 
109
113
  ## Step 3 — Build each component, bound to tokens/styles
110
114
 
@@ -167,7 +171,9 @@ deterministic naming):
167
171
  visual-validation loop (screenshot → fix any overlaps/misalignment →
168
172
  re-screenshot) **and its "Post-build audit (REQUIRED before handoff)"
169
173
  read-back checklist** (container type, auto layout, bound variables,
170
- deterministic names) before the checkpoint.
174
+ deterministic names) before the checkpoint. When you build inline, with no
175
+ `figma-executor`, that includes item 9's `focus-indicator` check: a miss stops
176
+ the build, as it would for the executor.
171
177
 
172
178
  Checkpoint after each component: show all variants, confirm before the next.
173
179
 
@@ -252,7 +258,10 @@ reference register, not this skill's guide voice.
252
258
  2. **Enrich from the archetype knowledge base.** Match the component to the nearest
253
259
  archetype in `.throughline/references/component-doc-archetypes.md` and
254
260
  seed `dos`, `donts`, `accessibility`, `whenToUse`, `whenNotToUse`. Provenance
255
- `best-practice` (or `w3c-apg` for the accessibility block).
261
+ `best-practice` (or `w3c-apg` for the accessibility block). Record the matched
262
+ archetype's id in the record's `archetype` field — one of `button`, `input`,
263
+ `choice`, `card`, `modal`, `badge`, `other` — so `verify:check` can read it
264
+ later rather than re-guessing from the name.
256
265
  3. **Specialize to `project.uiFramework`.** Align variant-meaning wording and the
257
266
  accessibility idiom to the target framework (the same field you read for variant
258
267
  vocabulary). Provenance `framework`.
@@ -347,6 +356,24 @@ any component built with the toggle + manual-swap fallback is listed in
347
356
  `components.instanceSwapUpgradePending`. Append `component-builder` to
348
357
  `completedSkills`.
349
358
 
359
+ **Record the stage entry, one call per component that actually finished:**
360
+ `node .throughline/scripts/verify-check.mjs --record --stage
361
+ component-builder --subject <Name> --entry <tmp>.json`. Build `<tmp>.json` with
362
+ `at` (an ISO timestamp you supply), `changed` (what was built), the attested checks `structural-read-back`,
363
+ `state-baseline` and `figma-name` from what the `figma-executor` returned (or
364
+ what your inline read-back found), `focus-indicator` when that check ran and
365
+ passed (not when it was skipped),
366
+ `review` when a reviewer ran, `screenshot` when one was captured, and
367
+ `advancedBecause`. The entry shape is
368
+ `.throughline/references/proof-bundle.md` — don't restate it here. Write
369
+ an entry only for a component that actually finished; a `BLOCKED` component gets
370
+ none. Do this after adding the component to `components.built` above — by the
371
+ time this loop runs, the whole run's components are already there, which is what
372
+ lets this stage's first `--record` capture the full adoption batch; each record
373
+ then removes its own subject from the captured `exempt` list, so running the
374
+ loop over every finished component (not a one-shot write) is what un-grandfathers
375
+ the rest of the batch.
376
+
350
377
  **Upgrade pass:** if `components.instanceSwapUpgradePending` is non-empty, re-resolve
351
378
  publish state (detect-or-ask per `.throughline/references/figma-publishing.md`);
352
379
  only when it is **confirmed published** (`figma.libraryPublished` true via a verified
@@ -180,6 +180,10 @@ needed here. Route per `.throughline/references/agent-routing.md`. If
180
180
  your host has no subagent dispatch, script the SVG-to-component pass inline,
181
181
  sequentially, with the user in the loop. Either way, follow
182
182
  `.throughline/references/figma-scripting.md`.
183
+ Inline, run the active-file preflight in
184
+ `.throughline/references/figma-scripting.md` before the first write and
185
+ again before replacing anything — the executor's file check does not come with
186
+ you.
183
187
 
184
188
  ## Step 3 — Normalize: page, naming, sizing, variants
185
189
 
@@ -27,9 +27,18 @@ system). Wire it to consume `packages/tokens` output so stories render with the
27
27
  real design tokens (import the generated CSS/theme). Checkpoint: confirm
28
28
  Storybook runs and shows the token-themed canvas.
29
29
 
30
- Install the documentation scripts alongside the token scripts: copy the five
31
- files and register the three npm scripts listed under **Documentation scripts —
32
- install as a set** in `.throughline/scripts/README.md`. That table is
30
+ Install the documentation scripts alongside the token scripts: copy the twelve
31
+ files and register the five npm scripts listed under **Documentation scripts —
32
+ install as a set** in `.throughline/scripts/README.md`. `adherence:check`
33
+ needs the repo's own values substituted in — the UI package's specifier for
34
+ `--package`, and paths for `--root`/`--system` that match this repo's layout;
35
+ registering it with the table's placeholders leaves a script that cannot run.
36
+ `verify:check` needs the same treatment for its `--root` and `--tokens`, and
37
+ registering it without `--tokens` is worse than leaving a placeholder: the script
38
+ runs, and skips both `orphan-token` and `color-contrast` on every run. Pass **one
39
+ `--tokens` flag per mode file** — `color-contrast` checks each file as its own
40
+ mode, so a multi-mode system registered with a single `--tokens` silently checks
41
+ only one of them. That table is
33
42
  the single source of truth for what a consuming repo gets; do not restate the
34
43
  list here.
35
44
 
@@ -244,13 +253,83 @@ step and/or a Turbo task). It compares every surface against its record and exit
244
253
  non-zero on drift; Figma surfaces report `edit-unverified` (checked live in a Figma
245
254
  session). Run `docs:check` once here and confirm it passes before handing off.
246
255
 
256
+ Ensure `verify:check` is wired alongside it, with the repo's real `--tokens`
257
+ paths — not the registered placeholder, and one flag per mode file — so
258
+ `orphan-token` and `color-contrast` actually run instead of reporting as skipped.
259
+ All four rules (`orphan-token`, `state-incomplete`, `name-drift`,
260
+ `color-contrast`) go live now; if the orphan rule misfires on this repo,
261
+ `--skip orphan-token` is the documented answer, and `--skip color-contrast` is
262
+ the same answer on the same terms for a system whose colour roles are named
263
+ differently. Both print on the report's
264
+ `skipped:` line. Dropping `--tokens` is not an equivalent fix — that switches the
265
+ rules off silently instead of visibly. **Run `verify:check` once here, before
266
+ Step 6 promotes anything to `stable`, and confirm it passes before handing
267
+ off — running it here rather than after Step 6 is deliberate, not an
268
+ oversight**: this run's components are still `draft`, so
269
+ `storybook-chromatic-builder` owes them no entry yet and the gate passes; Step 6
270
+ is what promotes them to `stable`, and Step 7 is what records their entries.
271
+ Moving this call after Step 6, or moving Step 7's recording ahead of it, would
272
+ leave the components `stable`, owed an entry, and without one.
273
+
274
+ **A contrast failure the user already accepted.** `token-builder` stops on a pair
275
+ below 4.5:1, and a user may explicitly accept it anyway. That acceptance lives in
276
+ `design-system/proof/token-builder.json` under the repo root, on the `system`
277
+ entry's `contrast-baseline` check, as `accepted: [{ fg, bg, mode }]`. Read that
278
+ file — it may not exist, in which case nothing is accepted and any failure stops
279
+ this step as usual. Then compare **this run's live failures** against it: take
280
+ the `[color-contrast]` lines off the report you just ran, and match each one's
281
+ `fg` and `bg` — the canonical `CONTRAST_PAIRS` paths, identical on both sides and
282
+ needing no translation — against each `accepted` entry's `fg`/`bg`, on the same
283
+ normalized fold (lowercase, alphanumerics only). Then:
284
+
285
+ - **every live failure is an accepted role pair** → register the `verify:check`
286
+ script in `package.json` with `--skip color-contrast`, and say in one line which
287
+ pair was accepted, that contrast is therefore **not checked in CI at all** while
288
+ that flag is there, and that deleting the flag is what re-enables it;
289
+ - **no live failures** → register the script **without** the flag. The pair was
290
+ fixed, or never applied to this repo, and switching the rule off would hide a
291
+ future one;
292
+ - **any live failure whose roles are not in `accepted`** → stop, exactly as this
293
+ step already does.
294
+
295
+ **The live run is the condition, not the presence of the field.** `accepted`
296
+ persists until `token-builder` is re-run, which nothing asks for — so keying off
297
+ the field alone would switch CI's contrast rule off for a pair that now passes,
298
+ while telling the user the flag is what re-enables it.
299
+
300
+ **Matching on the roles, and ignoring the mode, is deliberate here.** `--skip` is
301
+ rule-wide: there is no way to skip contrast in Dark and keep it in Light, so a
302
+ per-mode test would decide nothing a role test does not. This skill also *cannot*
303
+ translate a mode name — the recorded `mode` is the Figma name, and this skill
304
+ neither read the Figma modes nor wrote the DTCG files. Only `token-sync-layer`,
305
+ which did both, matches on the mode. The cost, stated rather than hidden:
306
+ whenever an accepted pair's roles also fail in a mode the user did not accept,
307
+ now or later, the roles still match and CI stays skipped. The sync is the
308
+ backstop — it subtracts per mode, so it stops on that unaccepted mode, and every
309
+ token change goes through it.
310
+
311
+ **That backstop only holds on a tree the sync finished on.** A sync that stopped
312
+ leaves both mode files written and the failure standing, and this skill running
313
+ over that tree would see two failures, match both on roles, and register the flag
314
+ over one nobody accepted. Do not wire the script on a tree whose last sync
315
+ stopped: the pipeline stopped there too, and the fix belongs in Figma before the
316
+ storying stage runs at all.
317
+
318
+ This skill is where the registered script is created, so it is the only place
319
+ that can apply this consequence — `token-builder` runs at folder stage, before
320
+ any repo, and cannot edit a `package.json` that does not exist.
321
+
247
322
  ## Step 6 — Finalize component status (Figma write-back)
248
323
 
249
324
  A component built and storied here is now **done** — but its Figma doc card was
250
325
  stamped `draft` by component-builder and won't change on its own. Once the code
251
326
  component renders and its stories build (and the user has approved the result),
252
327
  **promote each finalized component to `stable`** so the design system tells the
253
- truth in both places.
328
+ truth in both places. **This promotion is what makes a component owe a
329
+ `storybook-chromatic-builder` proof entry** — the stage's lifecycle gate reads
330
+ `components.meta[name].status`, and a component still at `draft` owes it
331
+ nothing. Step 7's recording only follows this step for that reason: reorder them
332
+ and a component would be `stable`, owed an entry, and recorded before it exists.
254
333
 
255
334
  **Confirm the write-back once, up front.** Before touching Figma, state the batched
256
335
  change in one line and get a yes — *"I'll update the N doc cards in Figma: flip the
@@ -263,15 +342,25 @@ For every component you finalized in this run, follow the **"Promoting a
263
342
  component's status (write-back on finalize)"** routine in
264
343
  `.throughline/references/figma-component-standards.md` (which also covers
265
344
  the `figma_execute` scripting gotchas — `getNodeByIdAsync` and an explicit
266
- `timeout` for the multi-card write):
345
+ `timeout` for the multi-card write). This write runs inline, so run the
346
+ active-file preflight in `.throughline/references/figma-scripting.md`
347
+ before it:
267
348
 
268
349
  - Set `components.meta[name].status` = `"stable"` and refresh
269
- `components.meta[name].updatedAt` to today.
350
+ `components.meta[name].updatedAt` to today — for every component this pass
351
+ promotes, including ones this run didn't build. **No verification gate reads
352
+ `updatedAt`.** A promotion run moves the timestamp of components other stages
353
+ own, so `proof-missing` scopes itself with the stage's captured `exempt` list
354
+ instead of a date comparison.
270
355
  - When promoting status (e.g. draft → stable), also set `status` + `updatedAt` in
271
356
  the component's `.doc.json`, recompute its fingerprint, re-run `docs:digest`, and
272
357
  re-render the affected surfaces so `docs:check` stays green.
273
358
  - If Figma is connected (per `figma.mechanism`), open the component's doc card and
274
- update the `Status Label` text to `stable`, re-bind the `Status` chip fill to
359
+ locate its status chip under either header shape, as the routine describes: a
360
+ `Status` chip with a `Status Label` text node on to-spec cards, a `Status Pill`
361
+ with one text node on legacy cards. If neither is there, don't guess — tell the
362
+ user that card still shows the old status. Otherwise set the label text to
363
+ `stable`, re-bind the chip fill to
275
364
  the **success** semantic color variable (mode-aware, not a hardcoded hex), then
276
365
  re-run the canonical doc-card builder against the same card to refresh the
277
366
  header date from the `record.updatedAt` already set above (it locates the date
@@ -292,6 +381,24 @@ Set `storybook.initialized` = `true`, `storybook.chromatic` accordingly,
292
381
  Step 6.) Note the ongoing loop: new components flow through the
293
382
  component-pipeline orchestrator; token changes flow through `/sync-figma-tokens`.
294
383
 
384
+ **Record the stage entry, one call per component finalized this run:**
385
+ `node .throughline/scripts/verify-check.mjs --record --stage
386
+ storybook-chromatic-builder --subject <Name> --entry <tmp>.json`. Build
387
+ `<tmp>.json` with `at` (an ISO timestamp you supply), the attested checks `build` (the story build outcome) and
388
+ `review`, `advancedBecause`, and **the two component-scoped derived results,
389
+ `state-incomplete` and `name-drift`, read off the `verify:check` report Step
390
+ 5.5 already ran** — recorded with `method: "derived"`. This is one of two stages
391
+ that record derived results, and it qualifies for the same reason the other does:
392
+ it has already run the gate before recording, so it holds the result rather than
393
+ asserting it (`.throughline/references/proof-bundle.md` names both); the
394
+ entry shape is
395
+ `.throughline/references/proof-bundle.md` — don't restate it here. As
396
+ in `component-builder`, this run's components are already in
397
+ `components.built` by the time this loop runs, so this stage's first `--record`
398
+ captures the whole batch into `exempt`; looping over every finalized component
399
+ here, rather than writing once, is what removes each one from that list instead
400
+ of leaving the rest of the batch grandfathered.
401
+
295
402
  ## Brownfield: baseline before retrofit + the verification triad
296
403
 
297
404
  On a **retrofit** (`tokens.intakeMode: "retrofit"`), the order of operations and the
@@ -66,6 +66,10 @@ human between them. Route per
66
66
  `.throughline/references/agent-routing.md`; never parallelize Figma
67
67
  work. If your host has no subagent dispatch, build and verify each tier inline,
68
68
  sequentially, as the steps below describe.
69
+ Inline, run the active-file preflight in
70
+ `.throughline/references/figma-scripting.md` before the first write and
71
+ again before replacing anything — the executor's file check does not come with
72
+ you.
69
73
 
70
74
  ## Step 1 — Brainstorm the structure (before building anything)
71
75
 
@@ -301,9 +305,84 @@ and later Device (`Spacing`/`Typography`). That's the cost of independent axes.
301
305
  Verify the aliases resolve — use `figma_get_variables` (filtered to the new
302
306
  semantic collection, `resolveAliases: true`) to confirm semantic tokens point at
303
307
  primitives, not literals; if you read via a `figma_execute` script instead, use
304
- the async APIs (see the dynamic-page note in Prerequisites). Then checkpoint:
305
- show the semantic layer and demonstrate the cascade if useful ("change `gray/50`
306
- and `bg/default` follows").
308
+ the async APIs (see the dynamic-page note in Prerequisites).
309
+
310
+ **Then assert contrast, before the checkpoint.** Using that same
311
+ `figma_get_variables` read (filtered to `Color/Semantic`, `resolveAliases: true`),
312
+ convert Figma's 0–1 RGBA channels to hex and **run the shipped module rather than
313
+ doing the arithmetic in the transcript**: shape the per-mode values as
314
+ `[{ mode, values: { "<dotted token path>": "#rrggbb" } }]` and pass them to a
315
+ single `node` invocation that imports `CONTRAST_PAIRS`, `contrastRatio`,
316
+ `composite` and `AA_NORMAL_TEXT` from
317
+ `.throughline/scripts/lib/contrast.mjs` and prints one line per pair per
318
+ mode:
319
+
320
+ node --input-type=module -e "<import the module, read the JSON from
321
+ process.argv[1], loop CONTRAST_PAIRS × modes, print mode/fg/bg/ratio>" '<the JSON>'
322
+
323
+ The module ships precisely so the maths has one home: read the table and call the
324
+ function, and restate neither. The pairs are the semantic roles the system
325
+ already promises (`text/onEmphasis` on `bg/emphasis`, `text/primary` on
326
+ `bg/default`, each status text on its own status background); a role this system
327
+ does not have is not asserted.
328
+
329
+ **The keys are dotted token paths, not Figma variable names.** Figma calls the
330
+ role `text/onEmphasis` in the `Color/Semantic` collection; `CONTRAST_PAIRS`
331
+ spells it `color.text.onEmphasis`, and no normalized fold bridges the two —
332
+ `colorsemantictextonemphasis` is not `colortextonemphasis`. Apply the **Name
333
+ mapping** rule the sync already defines
334
+ (`.throughline/skills/token-sync-layer/SKILL.md`): the category drives
335
+ the top-level group and the tier is dropped, so `Color/Semantic` +
336
+ `text/onEmphasis` → `color.text.onEmphasis`. Both halves of this check have to
337
+ agree on the spelling, or the Figma-time half asserts pairs the CLI half never
338
+ sees, and the two never disagree because they never meet.
339
+
340
+ **A failing pair stops the tier.** Do not proceed to the checkpoint or to Step 4
341
+ with a pair below 4.5:1. Report it in guide voice
342
+ (`.throughline/references/guide-voice.md`): name the two roles, the
343
+ mode, the measured ratio and the required one, then give **one** recommended fix
344
+ — re-point that mode's alias at a primitive with more separation, naming a
345
+ specific step in the ramp that clears — rather than a menu of options. This is
346
+ the "modes can't be built with poor colour contrast" promise, and it is the
347
+ cheapest place in the whole system to catch it: the mode is being defined right
348
+ now, and nothing is built on it yet.
349
+
350
+ **If the user explicitly accepts a failing pair**, continue, and say plainly in
351
+ the same breath what it costs downstream — describing the mechanism that actually
352
+ exists, not a registration that cannot reach the sync:
353
+
354
+ 1. The acceptance is recorded in this stage's entry (`contrast-baseline` at
355
+ `result: "fail"`, `advancedBecause` naming it, and the pair itself listed in
356
+ `accepted`), and `token-sync-layer` subtracts exactly that pair from its own
357
+ gate run — so **the next token sync is not blocked** by it, while any *other*
358
+ failing pair, including one introduced later, still stops the sync.
359
+ 2. The repo's own registered `verify:check` — the one
360
+ `storybook-chromatic-builder` wires into CI later — derives the same ratio off
361
+ the DTCG source and would fail there. That skill runs the gate itself when it
362
+ wires the script, and if every failure it sees is a pair recorded here as
363
+ accepted, it registers the script with `--skip color-contrast` and says so at
364
+ the time; if the pair has since been fixed, it registers the script without the
365
+ flag. Nothing is owed to a script that does not exist yet, and nothing is
366
+ switched off on the strength of a record alone.
367
+
368
+ Say both, in that order, and then say how each one ends, because they do not end
369
+ the same way. Fixing the pair in Figma **retires the sync's side by itself**: the
370
+ failure stops appearing, the recorded triple matches nothing, and the sync goes
371
+ back to normal on its own — with one expected line on that first sync, where the
372
+ previously recorded `fail` disagrees with a rerun that now passes. That line does
373
+ not stop anything, and the same run re-records the pass. The registered
374
+ `--skip color-contrast` does **not** heal: it is a line in the repo's
375
+ `package.json` and stays until someone removes it, which is the one thing this
376
+ override leaves behind. Say that plainly rather than letting them find it — a
377
+ user who fixes the pair months later and still sees contrast skipped in CI has
378
+ been told the gate is live when it is not. Say the scope too: `--skip` is
379
+ rule-wide, so while that flag is registered CI checks **no** pair, including one
380
+ introduced after the acceptance. The sync is what catches that one, since every
381
+ token change goes through it — but the user should know CI is not the thing
382
+ standing guard meanwhile.
383
+
384
+ Then checkpoint: show the semantic layer and demonstrate the cascade if useful
385
+ ("change `gray/50` and `bg/default` follows").
307
386
 
308
387
  Update the manifest: `tokens.semanticBuilt` = `true`, add every semantic
309
388
  collection to `tokens.collections`.
@@ -343,6 +422,34 @@ Update the manifest: set `tokens.stylesBuilt` = `true` and record which style
343
422
  groups were created in `tokens.styleGroups`. Append `token-builder` to
344
423
  `completedSkills`.
345
424
 
425
+ **Record the stage entry:** `node .throughline/scripts/verify-check.mjs
426
+ --record --stage token-builder --entry <tmp>.json` (subject defaults to
427
+ `"system"`; this stage is not per-component). Build `<tmp>.json` with `at` (an ISO
428
+ timestamp you supply), `changed` summarizing the collections and modes created, the attested check
429
+ `contrast-baseline` carrying `result` and a one-line `evidence` naming the
430
+ tightest ratio per mode (`"Light 5.2:1, Dark 4.9:1 — tightest is
431
+ text/onEmphasis on bg/emphasis"`), and `advancedBecause`.
432
+
433
+ When the user accepted a failing pair, the check's `result` is `"fail"`,
434
+ `advancedBecause` names the acceptance, and the check carries
435
+ `accepted: [{ fg, bg, mode }]` — one entry per accepted pair, `fg` and `bg` as the
436
+ dotted token paths `CONTRAST_PAIRS` uses, and `mode` as **the Figma mode name this
437
+ skill just read** (`Light`, `Dark` — the names in the collection it built), not a
438
+ file name. At this point in the pipeline no DTCG file exists to name a mode
439
+ after: the sync writes those later and chooses their names, so a filename here
440
+ would be a guess that never matches. The sync translates this name into its own
441
+ file label before matching, and recording what was actually observed is what
442
+ makes that translation possible. **Write the pair structurally as well as in
443
+ prose** — `advancedBecause` is for whoever reads the bundle later, and `accepted`
444
+ is what `token-sync-layer` matches against, so an acceptance recorded only in
445
+ prose would stop the next sync exactly as if it had never been given. Accept only
446
+ the pairs the user actually accepted; that list is the scope of the override.
447
+
448
+ The entry shape is `.throughline/references/proof-bundle.md` — do not
449
+ restate it here. Write the entry only when the tier actually completed: a run that
450
+ stopped on a failing pair and was not resumed records nothing, the same rule
451
+ `component-builder` follows for a `BLOCKED` component.
452
+
346
453
  ## Step 5 — Hand off
347
454
 
348
455
  Tell the user what's unlocked and offer natural next steps without forcing them:
@@ -67,6 +67,8 @@ Copy these from `.throughline/scripts/` into the user's repo **verbatim**
67
67
  - `lib/crosswalk.mjs` → `packages/tokens/scripts/lib/crosswalk.mjs`
68
68
  - `lib/dtcg.mjs` → `packages/tokens/scripts/lib/dtcg.mjs` (required by
69
69
  `validate-crosswalk.mjs` — copying the validator without it breaks the gate at import)
70
+ - `lib/source-scan.mjs` → `packages/tokens/scripts/lib/source-scan.mjs` (required by
71
+ `guard-token-removal.mjs` — copying the guard without it breaks it at import)
70
72
  - `validate-crosswalk.mjs` → `packages/tokens/scripts/validate-crosswalk.mjs`
71
73
  - `build-reverse-index.mjs` → `packages/tokens/scripts/build-reverse-index.mjs`
72
74
  - `guard-token-removal.mjs` → `packages/tokens/scripts/guard-token-removal.mjs`
@@ -46,6 +46,10 @@ variables via `figma_get_variables`, not just a screenshot), gated by a
46
46
  `.throughline/references/agent-routing.md`; never parallelize Figma
47
47
  work. If your host has no subagent dispatch, build and verify the page inline,
48
48
  sequentially, as the steps below describe.
49
+ Inline, run the active-file preflight in
50
+ `.throughline/references/figma-scripting.md` before the first write and
51
+ again before replacing anything — the executor's file check does not come with
52
+ you.
49
53
 
50
54
  ## Step 1 — Brainstorm the layout (lightly)
51
55
 
@@ -99,9 +99,17 @@ Three rules for the multi-collection structure:
99
99
  and token-sync-layer are a **matched pair** on this — neither is correct alone.
100
100
 
101
101
  This DTCG JSON is the canonical intermediate. Write it to a known location in
102
- `packages/tokens/` (e.g. `packages/tokens/dtcg/tokens.json`). Do not trust the
103
- MCP's built-in CSS/Tailwind exporters for final output — route through Style
104
- Dictionary so the adapter system governs the result.
102
+ `packages/tokens/`, **one file per mode**, because every gate downstream reads a
103
+ file as a mode. A single-mode system writes `packages/tokens/dtcg/tokens.json`; a
104
+ light/dark system writes `packages/tokens/dtcg/primitives.json`,
105
+ `packages/tokens/dtcg/semantic.light.json` and
106
+ `packages/tokens/dtcg/semantic.dark.json` — the ramps once, then each mode's
107
+ semantic aliases in its own file. Nesting the modes as groups inside a single
108
+ file (`color.light.*` / `color.dark.*`) is the layout `color-contrast` abstains
109
+ on, and writing a multi-mode system to one `tokens.json` is what leaves the
110
+ registered `verify:check` checking a single mode and reporting that it checked
111
+ one. Do not trust the MCP's built-in CSS/Tailwind exporters for final output —
112
+ route through Style Dictionary so the adapter system governs the result.
105
113
 
106
114
  ### Brownfield transforms (learned the hard way)
107
115
 
@@ -182,12 +190,12 @@ list. See `.throughline/references/native-adapter-config.md`.
182
190
  **Execution model — subagent dispatch with model routing.** Generating each
183
191
  platform's output is independent and verifiable. If your host supports subagent
184
192
  dispatch, dispatch **one `code-executor` per adapter** — each produces its
185
- platform's files and verifies them (for web: the config builds, the expected
186
- files appear, references resolve; for native: `tokens:validate-output` passes —
187
- "the config builds" is not verification, it is the condition under which all
188
- four known native failure modes ship silently) — then a **`reviewer`**
189
- to check each before combining. Choose each subagent's model from its role tier
190
- per `.throughline/references/agent-routing.md` (`code-executor` → fast,
193
+ platform's files and verifies them (for web and native alike:
194
+ `tokens:validate-output` passes — a clean build is not verification, it is the
195
+ condition under which every known failure mode, web or native, ships silently)
196
+ — then a **`reviewer`** to check each before combining. Choose each subagent's
197
+ model from its role tier per
198
+ `.throughline/references/agent-routing.md` (`code-executor` → fast,
191
199
  `reviewer` → balanced), and only dispatch once each adapter's spec is complete
192
200
  enough to transcribe. If your host has no subagent dispatch, generate and verify
193
201
  each adapter inline instead. For a single platform, run inline either way. This
@@ -201,23 +209,34 @@ as **build artifacts** — regenerated every sync, never hand-edited. Wire
201
209
  `packages/tokens/package.json` to export them so the UI package, Storybook, and
202
210
  any future app consume them.
203
211
 
204
- **Install the native token toolkit — all four files, as a set.** Copy
205
- `.throughline/scripts/validate-token-output.mjs` into
206
- `packages/tokens/scripts/`, and
207
- `.throughline/scripts/lib/dtcg.mjs`,
208
- `.throughline/scripts/lib/native-literal.mjs`, and
209
- `.throughline/scripts/lib/sd-native.mjs` into
210
- `packages/tokens/scripts/lib/`. `sd-native.mjs` and the validator both import
211
- `dtcg.mjs` and `native-literal.mjs`, so copying any of them without the others
212
- breaks at import. Then register the gate so it stays live on every future sync:
212
+ **Install the token toolkit — all four files, as a set, for web and native
213
+ targets alike.** Copy these from `.throughline/scripts/` into the
214
+ user's repo verbatim:
215
+
216
+ - `validate-token-output.mjs` → `packages/tokens/scripts/validate-token-output.mjs`
217
+ - `lib/dtcg.mjs` → `packages/tokens/scripts/lib/dtcg.mjs`
218
+ - `lib/native-literal.mjs` → `packages/tokens/scripts/lib/native-literal.mjs`
219
+ - `lib/sd-native.mjs` → `packages/tokens/scripts/lib/sd-native.mjs`
220
+
221
+ `sd-native.mjs` and the validator both import `dtcg.mjs` and
222
+ `native-literal.mjs`, so copying any of them without the others breaks at
223
+ import. Then register the gate so it stays live on every future sync:
213
224
 
214
225
  ```json
215
226
  "tokens:validate-output": "node scripts/validate-token-output.mjs --min-match 1"
216
227
  ```
217
228
 
218
229
  Invoke it once per native output file, passing the same `--source` list that
219
- file's build used. `--min-match 1` is what makes it a gate: the flag defaults to
220
- `0.5`, so without it a 60% match rate exits `0`.
230
+ file's build used. For web output (`shadcn`, `tailwind`, `vanilla-css`), invoke
231
+ it once per mode block instead: name the block with `--block` and pass the
232
+ sources that block was built from. Pass the generated **CSS** file, the one that
233
+ declares the custom properties. For `tailwind` that is the CSS the theme config
234
+ points at, never the config itself: the gate reads custom properties, and a JS
235
+ config has none, so it would match nothing and fail. A MUI theme, or a Tailwind
236
+ build whose only output is a JS config, isn't checked yet (#127). The
237
+ full contract is in `.throughline/scripts/README.md`. `--min-match 1`
238
+ is what makes it a gate: the flag defaults to `0.5`, so without it a 60% match
239
+ rate exits `0`.
221
240
 
222
241
  ## Step 4.5 — Icon code sync (install check + custom SVGR)
223
242
 
@@ -252,6 +271,69 @@ color.surface.default?`). Surface these prominently. Also surface plain
252
271
  deletions (consumers will break — that's intentional, but the human should see
253
272
  it).
254
273
 
274
+ ### Check contrast before it ships
275
+
276
+ Run the gate once against the DTCG sources this sync just wrote — **always**, with
277
+ no accepted-pair exception. The paragraphs below filter the result, never the run:
278
+
279
+ ```
280
+ node .throughline/scripts/verify-check.mjs --root <repo root> \
281
+ --tokens <each DTCG source, one flag per mode file> \
282
+ --skip orphan-token --skip state-incomplete --skip name-drift
283
+ ```
284
+
285
+ **One `--tokens` flag per mode file** — that is what makes each pair get checked
286
+ in every mode. Passing a single file of a multi-mode system checks one mode, and
287
+ says so on the report's `contrast:` line.
288
+
289
+ **Why those three rules are skipped, so nobody removes the flags.** A sync that
290
+ adds a token is adding one nothing in the repo names yet, which is
291
+ `orphan-token`'s definition of an orphan — running it here would fail nearly
292
+ every sync that does its job. `state-incomplete` and `name-drift` are about doc
293
+ records and code surfaces this skill does not touch. All three stay live in
294
+ `storybook-chromatic-builder`'s run of the same gate.
295
+
296
+ **Honour an accepted pair, and only an accepted one.** After the gate has run,
297
+ read `design-system/proof/token-builder.json` under the same `--root` (it may not
298
+ exist, in which case there is nothing to honour). Collect the `accepted` array
299
+ from its `system` entry's `contrast-baseline` check, then drop from this run's
300
+ `color-contrast` failures every one whose `(fg, bg, mode)` matches an entry in it,
301
+ comparing `fg` and `bg` on the same normalized fold the rule matches roles with
302
+ (lowercase, alphanumerics only). **Translate `mode` before comparing it:** the
303
+ recorded value is the Figma mode name `token-builder` saw (`Dark`), and the gate
304
+ labels a mode by its DTCG file's basename (`semantic.dark`). This skill is the one
305
+ that knows both — it read the Figma modes and it wrote the files — so map each
306
+ Figma mode to the file it generated for that mode, and compare against that label.
307
+ Untranslated, the triple can never match and the override path silently does not
308
+ exist. Match the triple exactly, and subtract nothing else.
309
+
310
+ **What this reads.** The gate has no machine-readable output, so this operates on
311
+ `formatReport`'s failure lines — `<fg> on <bg> is <ratio>:1 in <mode> …` — which
312
+ carry all three fields in fixed positions. That is machine-formatted output with a
313
+ stated shape, not prose. Do **not** treat `result: "fail"` on its own as the
314
+ condition, and do **not** pattern-match `advancedBecause`: the first switches off a
315
+ whole rule on the strength of one pair, and the second reads prose written for a
316
+ human as though it were a test.
317
+
318
+ **Then judge what is left.** Nothing remaining → proceed, and say in one line which
319
+ pair was accepted and is not being re-litigated, both in the sync's message and in
320
+ the PR body. That line is where the override stays visible, and it is the only
321
+ place, since a subtracted failure is by definition not on the report. Anything
322
+ remaining → stop, exactly as if there had been no acceptance: a pair the user never
323
+ saw, in any mode, is the case this rule exists for, and an acceptance given for one
324
+ pair must not carry another one through. An `accepted` list that matches nothing —
325
+ because the pair was fixed, or was never recorded properly — subtracts nothing and
326
+ the sync behaves as though there were no acceptance at all, which is both the
327
+ fail-safe direction and how an acceptance retires itself once the pair clears.
328
+
329
+ **A `color-contrast` failure stops the sync.** Do not open the PR. Report the
330
+ failing pair and mode in guide voice
331
+ (`.throughline/references/guide-voice.md`), and say the fix is in Figma —
332
+ re-point that mode's alias — not in the generated output, which is a build
333
+ artifact. **Any other failure in the report does not stop the sync**: surface it to
334
+ the user and summarize it in the PR body, but the token sync owns the token tier,
335
+ and a component's missing proof entry is not its to block on.
336
+
255
337
  ## Step 6 — Land it as a reviewable PR (never silent)
256
338
 
257
339
  Never overwrite token files silently. The output of a sync is a **pull request**
@@ -274,6 +356,44 @@ TurboSnap section in `storybook-chromatic-builder`.
274
356
  Update the manifest: `sync.lastRun` (timestamp), `tokens.lastSync`, confirm
275
357
  `sync.platforms`. Append `token-sync-layer` to `completedSkills`.
276
358
 
359
+ Alongside that, record the stage entry: `node
360
+ .throughline/scripts/verify-check.mjs --record --stage
361
+ token-sync-layer --entry <tmp>.json` (subject defaults to `"system"`, since this
362
+ stage isn't per-component). Build `<tmp>.json` with `at` (an ISO timestamp you supply), `changed` summarizing tokens
363
+ added, changed, deleted and probable renames, and the attested
364
+ `tokens:validate-output` result as a check, plus `advancedBecause`. The entry
365
+ shape is `.throughline/references/proof-bundle.md` — don't restate it
366
+ here.
367
+
368
+ The entry also carries the **derived** check `color-contrast`, its result read off
369
+ the Step 5 report, with `evidence` naming the modes checked and the tightest
370
+ ratio. Record it whenever the report's `contrast:` line compared at least one
371
+ pair. When it compared none (the report says `contrast-rule-inert`, or the run
372
+ passed `--skip color-contrast`) there is no result to record: omit the check, and
373
+ say why in `advancedBecause`. A `pass` written there would describe a comparison
374
+ that never happened, and the gate could never contradict it.
375
+
376
+ **When a failure was subtracted, record `result: "fail"` — what the gate actually
377
+ reported** — with `advancedBecause` naming the accepted pair as the reason the sync
378
+ proceeded anyway, and `evidence` saying which pair was subtracted. Do not record
379
+ `pass` on the grounds that nothing remained after subtraction: the gate does not
380
+ know about subtraction, the pair still fails on disk, and the next sync's own gate
381
+ run recomputes it, so a recorded `pass` would raise `proof-contradicted` on every
382
+ later sync report. A stored result that disagrees with the file it claims to
383
+ describe is an attestation wearing a derived label.
384
+
385
+ **Expect one `proof-contradicted` line on the first sync after the user fixes the
386
+ pair**, and say why when you relay it: the stored `fail` is now disagreed with by a
387
+ rerun that passes, which `checkProof` reports in both directions. It does not stop
388
+ the sync, and this step re-records the pass on that same run, so it clears itself
389
+ in one pass. Relaying an expected, self-healing line as a problem is how a user is
390
+ taught to distrust the gate.
391
+
392
+ **The ordering is the point.** The gate runs in Step 5 and the record is written
393
+ here in Step 6, and it is that order which makes recording a derived result honest:
394
+ this stage has the gate's output in hand rather than asserting it, which is the one
395
+ and only reason a stage may record `method: "derived"`.
396
+
277
397
  ## Step 7 — Install the `/sync-figma-tokens` command
278
398
 
279
399
  Set up the reusable command (the plugin ships it in `commands/`) so the user can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radicool/throughline",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Build a complete design system end to end — author in Figma, sync tokens to code, generate Storybook. Usable from Claude Code, Cursor, Codex, or any AGENTS.md agent.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@ deterministically. JSON is equally machine-legible for AI consumers.
31
31
  "type": { "primary": "Highest-emphasis action…", "secondary": "…", "ghost": "…" },
32
32
  "size": { "sm": "…", "md": "…", "lg": "…" }
33
33
  },
34
- "states": { "hover": "…", "focus": "…", "disabled": "…", "loading": "…" },
34
+ "states": { "hover": "…", "focus": "…", "active": "…", "disabled": "…", "loading": "…" },
35
35
  "dos": ["Lead with a verb", "One primary button per view"],
36
36
  "donts": ["Don't use for navigation", "Don't stack >2 primaries"],
37
37
  "accessibility": {
@@ -40,6 +40,7 @@ deterministically. JSON is equally machine-legible for AI consumers.
40
40
  "notes": ["Icon-only buttons need an aria-label"]
41
41
  },
42
42
  "tokensUsed": ["color.bg.primary", "spacing.sm", "radius.md"],
43
+ "archetype": "button",
43
44
  "status": "stable",
44
45
  "updatedAt": "2026-07-14",
45
46
  "provenance": {
@@ -56,6 +57,15 @@ deterministically. JSON is equally machine-legible for AI consumers.
56
57
  - **Optional content:** `whenToUse`, `whenNotToUse`, `variants`, `states`, `dos`,
57
58
  `donts`, `accessibility`, `tokensUsed`.
58
59
  - **Lifecycle:** `status` (`draft`|`beta`|`stable`|`deprecated`), `updatedAt` (ISO date).
60
+ - **`archetype`** — optional classification metadata: one of `button`, `input`,
61
+ `choice`, `card`, `modal`, `badge`, `other`, written by the authoring pipeline
62
+ that already matches a component to its nearest archetype. **Excluded from the
63
+ fingerprint**, like `provenance`, because it is classification rather than
64
+ projected content — so adding it changes no existing fingerprint and forces no
65
+ re-render. `verify:check` reads it to decide which baseline interaction states a
66
+ component owes; with it absent the gate falls back to an exact name match, and
67
+ reports `archetype-unknown` when neither resolves. See
68
+ `${CLAUDE_PLUGIN_ROOT}/references/proof-bundle.md`.
59
69
  - **`provenance`** — per-block author source, one of `imported`, `ai-inferred`,
60
70
  `best-practice`, `w3c-apg`, `framework`, `user`, or a `+`-joined combination
61
71
  (e.g. `best-practice+user`). Regeneration **re-infers** a block whose