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