@radicool/throughline 0.24.0 → 0.26.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 (62) hide show
  1. package/README.md +8 -4
  2. package/adapters/codex/AGENTS.md +2 -0
  3. package/adapters/codex/prompts/check-figma.md +41 -0
  4. package/adapters/codex/prompts/component-builder.md +5 -3
  5. package/adapters/codex/prompts/compose-figma.md +59 -0
  6. package/adapters/codex/prompts/document-component.md +3 -2
  7. package/adapters/cursor/.cursor/commands/check-figma.md +41 -0
  8. package/adapters/cursor/.cursor/commands/compose-figma.md +59 -0
  9. package/adapters/cursor/.cursor/commands/document-component.md +3 -2
  10. package/adapters/cursor/.cursor/rules/component-builder.mdc +5 -3
  11. package/adapters/generic/AGENTS.md +2 -0
  12. package/adapters/generic/commands/check-figma.md +41 -0
  13. package/adapters/generic/commands/compose-figma.md +59 -0
  14. package/adapters/generic/commands/document-component.md +3 -2
  15. package/adapters/generic/skills/component-builder/SKILL.md +5 -3
  16. package/package.json +1 -1
  17. package/references/doc-card-builder.md +62 -15
  18. package/references/figma-adherence.md +463 -0
  19. package/references/figma-checks.md +106 -43
  20. package/references/figma-compose.md +896 -0
  21. package/scripts/README.md +8 -2
  22. package/scripts/build-doc-card-builder.mjs +16 -0
  23. package/scripts/build-figma-checks.mjs +208 -23
  24. package/scripts/doc-card-payload.mjs +115 -0
  25. package/scripts/install.mjs +7 -1
  26. package/scripts/lib/doc-card-plan.mjs +37 -0
  27. package/scripts/lib/doc-card-render.figma.js +9 -15
  28. package/scripts/lib/figma-adherence-read.figma.js +228 -0
  29. package/scripts/lib/figma-adherence.mjs +146 -0
  30. package/scripts/lib/figma-checks-read.figma.js +43 -43
  31. package/scripts/lib/figma-checks.mjs +16 -5
  32. package/scripts/lib/figma-compose.figma.js +326 -0
  33. package/scripts/lib/figma-compose.mjs +317 -0
  34. package/scripts/lib/figma-variables-read.figma.js +39 -0
  35. package/scripts/lib/fixtures/figma-adherence/clean.json +90 -0
  36. package/scripts/lib/fixtures/figma-adherence/detached-library.json +116 -0
  37. package/scripts/lib/fixtures/figma-adherence/ds01-gaps.json +84 -0
  38. package/scripts/lib/fixtures/figma-adherence/inert-all.json +45 -0
  39. package/scripts/lib/fixtures/figma-adherence/mixed-text.json +61 -0
  40. package/scripts/lib/fixtures/figma-adherence/nothing-scanned.json +49 -0
  41. package/scripts/lib/fixtures/figma-adherence/off-scale-gap.json +65 -0
  42. package/scripts/lib/fixtures/figma-adherence/override-raw.json +72 -0
  43. package/scripts/lib/fixtures/figma-adherence/padding-raw.json +74 -0
  44. package/scripts/lib/fixtures/figma-adherence/raw-colour-token.json +70 -0
  45. package/scripts/lib/fixtures/figma-adherence/root-radius.json +88 -0
  46. package/scripts/lib/fixtures/figma-adherence/skip-colour.json +86 -0
  47. package/scripts/lib/fixtures/figma-adherence/zygarden-override-bound.json +85 -0
  48. package/scripts/lib/fixtures/figma-adherence/zygarden-text-raw.json +208 -0
  49. package/scripts/lib/fixtures/figma-compose/casing-and-suffix.json +8 -0
  50. package/scripts/lib/fixtures/figma-compose/fill-in-unlaid-slot.json +7 -0
  51. package/scripts/lib/fixtures/figma-compose/hug-on-fixed-component.json +34 -0
  52. package/scripts/lib/fixtures/figma-compose/inventory.json +295 -0
  53. package/scripts/lib/fixtures/figma-compose/many-errors.json +8 -0
  54. package/scripts/lib/fixtures/figma-compose/missing-component.json +7 -0
  55. package/scripts/lib/fixtures/figma-compose/name-taken.json +6 -0
  56. package/scripts/lib/fixtures/figma-compose/padding-sides.json +23 -0
  57. package/scripts/lib/fixtures/figma-compose/radius-kind.json +27 -0
  58. package/scripts/lib/fixtures/figma-compose/raw-number.json +5 -0
  59. package/scripts/lib/fixtures/figma-compose/sign-in-ok.json +16 -0
  60. package/scripts/lib/fixtures/figma-compose/slot-ok.json +9 -0
  61. package/scripts/lib/fixtures/figma-compose/unknown-prop-and-option.json +7 -0
  62. package/scripts/lib/fixtures/figma-compose/wrong-kind.json +5 -0
package/README.md CHANGED
@@ -73,7 +73,7 @@ Everything above was created during a single working session and synced directly
73
73
 
74
74
  | | |
75
75
  |---|---|
76
- | **A supported agent** | Required — [Claude Code](https://docs.claude.com/en/docs/claude-code) (native plugin), or **Cursor**, **Codex**, or any **AGENTS.md**-aware agent via `npx @radicool/throughline@0.24.0 init`. |
76
+ | **A supported agent** | Required — [Claude Code](https://docs.claude.com/en/docs/claude-code) (native plugin), or **Cursor**, **Codex**, or any **AGENTS.md**-aware agent via `npx @radicool/throughline@0.26.0 init`. |
77
77
  | **Figma** | Required, **desktop app** (the browser version causes connection errors). **Professional plan or higher recommended** — multi-mode variables (Light/Dark, brand themes) need it. |
78
78
  | **Figma access token** | Optional — only for the few Figma calls the Desktop Bridge can't make, like reading a published library. Without it, you work through the bridge alone. The setup skill walks you through it; your token stays yours and is never shared in chat. |
79
79
  | **GitHub** (or similar) | Optional — only when you're ready to graduate to a real remote repo with PRs and CI. |
@@ -107,6 +107,10 @@ Then start with:
107
107
 
108
108
  This is the reliable entry point — it runs environment setup first, ahead of anything else. (You can also just say *"let's set up my design system"*, but if you have other plugins installed that grab "let's build…" style phrases, the slash command guarantees ThroughLine takes the wheel.)
109
109
 
110
+ Already have screens or components in Figma? `/throughline:check-figma` checks them against the system without touching the file: it flags a component someone detached, and a colour, gap, padding or radius that's hardcoded when a token already has that value. It also runs the state, focus-ring and focus-contrast checks over the components already in your library.
111
+
112
+ Need a new screen? `/throughline:compose-figma` builds it in Figma from your own components and tokens, every value bound. If the screen needs something your system doesn't have, it says so instead of drawing a lookalike, and it runs the same check before handing the screen over.
113
+
110
114
  Update anytime with `/plugin marketplace update throughline-marketplace`.
111
115
 
112
116
  #### Cursor, Codex, or a generic AGENTS.md agent
@@ -114,9 +118,9 @@ Update anytime with `/plugin marketplace update throughline-marketplace`.
114
118
  Run the installer in your project — it stamps in the skills, the reference docs, the scripts, and the Figma MCP config for your tool:
115
119
 
116
120
  ```
117
- npx @radicool/throughline@0.24.0 init --target=cursor # → .cursor/rules + .cursor/mcp.json
118
- npx @radicool/throughline@0.24.0 init --target=codex # → prompts/ + AGENTS.md index + codex-mcp.toml
119
- npx @radicool/throughline@0.24.0 init --target=generic # → skills/ + AGENTS.md index
121
+ npx @radicool/throughline@0.26.0 init --target=cursor # → .cursor/rules + .cursor/mcp.json
122
+ npx @radicool/throughline@0.26.0 init --target=codex # → prompts/ + AGENTS.md index + codex-mcp.toml
123
+ npx @radicool/throughline@0.26.0 init --target=generic # → skills/ + AGENTS.md index
120
124
  ```
121
125
 
122
126
  It's safe to re-run (it merges `AGENTS.md` and `.cursor/mcp.json` non-destructively) and stages everything the skills read into `.throughline/`. Then open the `figma-environment-setup` skill/rule/prompt for your tool to begin. For Codex, add the printed `codex-mcp.toml` block to your Codex config to enable Figma access.
@@ -19,6 +19,8 @@ ThroughLine builds a design system end to end. Load the matching prompt for the
19
19
 
20
20
  ## ThroughLine commands
21
21
 
22
+ - `check-figma` — Check what's already in Figma against the design system, read-only — screens for detached components and raw values a token has, and existing component sets for their states, focus ring and focus contrast. → load `prompts/check-figma.md`.
23
+ - `compose-figma` — Compose a screen in Figma from the design system's own components, variables and text styles — every value bound, nothing invented, gated before it's handed over. → load `prompts/compose-figma.md`.
22
24
  - `design-system-status` — Show a plain-language summary of the current design system state — what's set up, what's not, and sensible next steps — read from design-system.json. → load `prompts/design-system-status.md`.
23
25
  - `document-component` — Author, refresh, or reconcile the usage documentation for one existing component — draft its canonical doc record from four sources, project it to Figma, the doc card, and code, and resolve any drift via a reviewable per-item choice. → load `prompts/document-component.md`.
24
26
  - `new-component` — Build a single new component end to end — in Figma, then sync any new tokens, then build its code component and stories — with a confirmation between each stage. → load `prompts/new-component.md`.
@@ -0,0 +1,41 @@
1
+ Check the Figma file against the design system without changing it. Use the
2
+ settings already in `design-system.json` (`figma.fileKey`, `project.uiFramework`,
3
+ `components.built`) rather than re-asking them. Scale explanation to
4
+ `user.codingLevel`.
5
+
6
+ 1. **Know the file.** Read `figma.fileKey` from `design-system.json`. If it's
7
+ absent, stop and ask which file to check.
8
+ 2. **Confirm the active file.** Run the preflight in
9
+ `.throughline/references/figma-scripting.md` ("confirm the *active*
10
+ file is your target") against that fileKey, and pin it with `figma_navigate`
11
+ (`lock: true`) for the run. Unpin when done.
12
+ 3. **Screens.** Unless the user named them, ask which page holds the screens, and
13
+ optionally which screens on it. Run the snippet in
14
+ `.throughline/references/figma-adherence.md` verbatim, with
15
+ `const TARGET = { page, screens, fileKey }` (the fileKey from step 1, for the
16
+ links) and `const SKIP = []` filled. The script is the rule; don't re-derive its
17
+ checks.
18
+ 4. **Existing component sets.** Ask which page holds the component sets, so only
19
+ that page is loaded. Ask which colour variables are the page surfaces the
20
+ components sit on, offering the file's likeliest default page surface (read the
21
+ names with `figma_get_variables`), or let the user skip focus contrast. For each
22
+ name in `components.built`, run
23
+ `.throughline/references/figma-checks.md` with `MODE = 'existing'` and
24
+ `EXPECT = { name, page, uiFramework, surfaces }` (`uiFramework` from
25
+ `project.uiFramework`). A name with no set on that page is reported as missing,
26
+ not as a failure of the others. When `focus-indicator` fails on a wrapped control
27
+ (an Input with a label, a Checkbox whose box is an inner frame), look at one focus
28
+ variant's layers before reporting it: if the ring is on an inner layer, run that
29
+ set again with `controlLayer` naming that layer, and say in the report that you
30
+ did.
31
+ 5. **Report.** The screen gate gives one pass or fail for the page: list its
32
+ failures grouped by screen (the path's second segment), each with its detail and
33
+ link, then the advisories, the counts and any skipped rules. Then pass or fail
34
+ per component set, with whether focus contrast ran and against which surfaces. Say plainly what the
35
+ gate can't see: a frame drawn from scratch to look like a component, variables
36
+ that live only in a published library, and focus states drawn as an INSIDE
37
+ stroke.
38
+
39
+ Never edit the file to make a check pass, and never edit a token. A failure is
40
+ a finding for the user; fixing it is a separate, deliberate step.
41
+
@@ -327,9 +327,11 @@ reference register, not this skill's guide voice.
327
327
  always last.
328
328
  - **Doc card body.** Render the card's `Usage` band with the canonical builder
329
329
  snippet in `.throughline/references/doc-card-builder.md` (via
330
- `figma_execute` with an explicit `timeout`, one card per call): fill the
331
- RECORD/CANONICAL_FP slots, resolve the nine required semantic variables and
332
- the `Body/Default` text style per that file's call contract, run it, and
330
+ `figma_execute` with an explicit `timeout`, one card per call): assemble the
331
+ call with `node .throughline/scripts/doc-card-payload.mjs <record> <cardNodeId>`
332
+ (it fills the RECORD/CANONICAL_FP slots and resolves the nine semantic
333
+ variables and `Body/Default` per that file's call contract), run it, check
334
+ `summary.renderHash` against the `expectedRenderHash` it printed, and
333
335
  verify the returned summary (`rowsRendered`, `blocksCreated`, `cardWidth`) —
334
336
  not a screenshot. The builder creates the `Doc Fingerprint` node itself and
335
337
  is the only thing that may build the usage body — never hand-assemble it.
@@ -0,0 +1,59 @@
1
+ Compose the screen the user asked for in Figma, from what the file already has.
2
+ Use the settings already in `design-system.json` (`figma.fileKey`) rather than
3
+ re-asking them. Scale explanation to `user.codingLevel`.
4
+
5
+ 1. **Know the file.** Read `figma.fileKey` from `design-system.json`. If it's
6
+ absent, stop and ask which file to compose in.
7
+ 2. **Confirm the active file.** Run the preflight in
8
+ `.throughline/references/figma-scripting.md` ("confirm the *active*
9
+ file is your target") against that fileKey, and pin it with `figma_navigate`
10
+ (`lock: true`) for the run. Unpin when done.
11
+ 3. **Read what exists.** Unless the user named them, ask which page holds the
12
+ component sets and which page the screen goes on (it's created if missing). Run
13
+ the snippet in `.throughline/references/figma-compose.md` verbatim, called
14
+ as its "How to call it" section says (`MODE` and `SPEC` declared above it,
15
+ `return await runCompose(MODE, SPEC);` below it), with `MODE = 'inventory'` and
16
+ `SPEC = { componentsPage }`. If the plan will set an instance-swap property (an
17
+ icon, say), ask which page holds the swap targets and run `inventory` again with
18
+ `iconsPage`.
19
+ 4. **Plan.** Write the screen description (its shape is the `DESCRIPTION` comment
20
+ in the snippet, at the top of the resolver) using only names from the
21
+ inventory's summary: component sets and their properties and options, variables
22
+ by kind, text styles, modes. Leave `sizing` off an instance unless it should
23
+ fill; `hug` works only on a set whose summary says `autoLayout: true`. Frames
24
+ carry layout only (auto layout, gap, padding, fill, stroke, radius); everything
25
+ the user sees is a component instance or styled text. When the request needs a
26
+ component the inventory doesn't have, say so before building and ask: leave it
27
+ out, use a named existing component instead, or stop. Never draw a lookalike
28
+ from frames.
29
+ 5. **Build.** Run the snippet with `MODE = 'build'` and
30
+ `SPEC = { componentsPage, iconsPage, description, fileKey }`. If it returns
31
+ errors, nothing was written: fix the description (a `missing-component` error
32
+ goes back to the user as in step 4) and build again. A `name-taken` error on
33
+ `<screen>` means a screen a person made already has that name: ask for another
34
+ name. On `WIP: <screen>` it means an unmarked frame holds the working name: ask
35
+ the user to rename or remove it. If the call throws instead, the build rolled
36
+ itself back and nothing was written: stop, and report the error as a composer
37
+ bug rather than reworking the screen around it.
38
+ 6. **Gate.** Run the screen gate in
39
+ `.throughline/references/figma-adherence.md` verbatim with
40
+ `TARGET = { page, screens: ['WIP: <screen>'], fileKey }` and `SKIP = []`. If it
41
+ fails, stop: report its failures as a composer bug, with their links, and leave
42
+ the `WIP:` frame for inspection. Don't change the description to make it pass.
43
+ Remove that frame with `MODE = 'discard'` and `SPEC = { page, screen }` only
44
+ when the user asks.
45
+ 7. **Finalize.** Run the snippet with `MODE = 'finalize'` and
46
+ `SPEC = { page, screen, fileKey }`. It replaces an earlier screen of that name
47
+ only if throughline composed it.
48
+ 8. **Review.** Dispatch the `reviewer` agent in Figma-visual mode with the screen's
49
+ node id (from finalize), the fileKey, the project's working directory (where
50
+ `design-system.json` is) and the user's request as its spec, unless the host
51
+ can't dispatch agents, in which case screenshot the screen yourself and say so.
52
+ 9. **Report.** The screen's link, whether it replaced an earlier one, the gate's
53
+ counts, the reviewer's verdict, and every gap: each component that was asked
54
+ for and doesn't exist, with the nearest names the script offered, and anything
55
+ left out of the request because of it.
56
+
57
+ Never edit a component, a variable or a text style, and never write a raw value.
58
+ If the screen can't be built from what the system has, the gap is the finding.
59
+
@@ -24,8 +24,9 @@ Ask which component to document (e.g. "Button"), then:
24
24
  2. **Project it.** Write `design-system/docs/components/<Name>.doc.json`, set the
25
25
  Figma component `description`, rebuild the doc card's `Usage` band with the
26
26
  canonical builder (`.throughline/references/doc-card-builder.md` —
27
- verify its returned summary and stamp `surfaces.docCard.{src,render,renderer}`
28
- from it), and (if the repo/code side exists) render MDX/JSDoc and run
27
+ assemble the call with `node .throughline/scripts/doc-card-payload.mjs`,
28
+ check the returned `renderHash` against the one it printed, verify the
29
+ returned summary and stamp `surfaces.docCard.{src,render,renderer}` from it), and (if the repo/code side exists) render MDX/JSDoc and run
29
30
  `docs:digest` per the `storybook-chromatic-builder` render step.
30
31
  Before the Figma writes, run the active-file preflight in
31
32
  `.throughline/references/figma-scripting.md` — this command writes
@@ -0,0 +1,41 @@
1
+ Check the Figma file against the design system without changing it. Use the
2
+ settings already in `design-system.json` (`figma.fileKey`, `project.uiFramework`,
3
+ `components.built`) rather than re-asking them. Scale explanation to
4
+ `user.codingLevel`.
5
+
6
+ 1. **Know the file.** Read `figma.fileKey` from `design-system.json`. If it's
7
+ absent, stop and ask which file to check.
8
+ 2. **Confirm the active file.** Run the preflight in
9
+ `.throughline/references/figma-scripting.md` ("confirm the *active*
10
+ file is your target") against that fileKey, and pin it with `figma_navigate`
11
+ (`lock: true`) for the run. Unpin when done.
12
+ 3. **Screens.** Unless the user named them, ask which page holds the screens, and
13
+ optionally which screens on it. Run the snippet in
14
+ `.throughline/references/figma-adherence.md` verbatim, with
15
+ `const TARGET = { page, screens, fileKey }` (the fileKey from step 1, for the
16
+ links) and `const SKIP = []` filled. The script is the rule; don't re-derive its
17
+ checks.
18
+ 4. **Existing component sets.** Ask which page holds the component sets, so only
19
+ that page is loaded. Ask which colour variables are the page surfaces the
20
+ components sit on, offering the file's likeliest default page surface (read the
21
+ names with `figma_get_variables`), or let the user skip focus contrast. For each
22
+ name in `components.built`, run
23
+ `.throughline/references/figma-checks.md` with `MODE = 'existing'` and
24
+ `EXPECT = { name, page, uiFramework, surfaces }` (`uiFramework` from
25
+ `project.uiFramework`). A name with no set on that page is reported as missing,
26
+ not as a failure of the others. When `focus-indicator` fails on a wrapped control
27
+ (an Input with a label, a Checkbox whose box is an inner frame), look at one focus
28
+ variant's layers before reporting it: if the ring is on an inner layer, run that
29
+ set again with `controlLayer` naming that layer, and say in the report that you
30
+ did.
31
+ 5. **Report.** The screen gate gives one pass or fail for the page: list its
32
+ failures grouped by screen (the path's second segment), each with its detail and
33
+ link, then the advisories, the counts and any skipped rules. Then pass or fail
34
+ per component set, with whether focus contrast ran and against which surfaces. Say plainly what the
35
+ gate can't see: a frame drawn from scratch to look like a component, variables
36
+ that live only in a published library, and focus states drawn as an INSIDE
37
+ stroke.
38
+
39
+ Never edit the file to make a check pass, and never edit a token. A failure is
40
+ a finding for the user; fixing it is a separate, deliberate step.
41
+
@@ -0,0 +1,59 @@
1
+ Compose the screen the user asked for in Figma, from what the file already has.
2
+ Use the settings already in `design-system.json` (`figma.fileKey`) rather than
3
+ re-asking them. Scale explanation to `user.codingLevel`.
4
+
5
+ 1. **Know the file.** Read `figma.fileKey` from `design-system.json`. If it's
6
+ absent, stop and ask which file to compose in.
7
+ 2. **Confirm the active file.** Run the preflight in
8
+ `.throughline/references/figma-scripting.md` ("confirm the *active*
9
+ file is your target") against that fileKey, and pin it with `figma_navigate`
10
+ (`lock: true`) for the run. Unpin when done.
11
+ 3. **Read what exists.** Unless the user named them, ask which page holds the
12
+ component sets and which page the screen goes on (it's created if missing). Run
13
+ the snippet in `.throughline/references/figma-compose.md` verbatim, called
14
+ as its "How to call it" section says (`MODE` and `SPEC` declared above it,
15
+ `return await runCompose(MODE, SPEC);` below it), with `MODE = 'inventory'` and
16
+ `SPEC = { componentsPage }`. If the plan will set an instance-swap property (an
17
+ icon, say), ask which page holds the swap targets and run `inventory` again with
18
+ `iconsPage`.
19
+ 4. **Plan.** Write the screen description (its shape is the `DESCRIPTION` comment
20
+ in the snippet, at the top of the resolver) using only names from the
21
+ inventory's summary: component sets and their properties and options, variables
22
+ by kind, text styles, modes. Leave `sizing` off an instance unless it should
23
+ fill; `hug` works only on a set whose summary says `autoLayout: true`. Frames
24
+ carry layout only (auto layout, gap, padding, fill, stroke, radius); everything
25
+ the user sees is a component instance or styled text. When the request needs a
26
+ component the inventory doesn't have, say so before building and ask: leave it
27
+ out, use a named existing component instead, or stop. Never draw a lookalike
28
+ from frames.
29
+ 5. **Build.** Run the snippet with `MODE = 'build'` and
30
+ `SPEC = { componentsPage, iconsPage, description, fileKey }`. If it returns
31
+ errors, nothing was written: fix the description (a `missing-component` error
32
+ goes back to the user as in step 4) and build again. A `name-taken` error on
33
+ `<screen>` means a screen a person made already has that name: ask for another
34
+ name. On `WIP: <screen>` it means an unmarked frame holds the working name: ask
35
+ the user to rename or remove it. If the call throws instead, the build rolled
36
+ itself back and nothing was written: stop, and report the error as a composer
37
+ bug rather than reworking the screen around it.
38
+ 6. **Gate.** Run the screen gate in
39
+ `.throughline/references/figma-adherence.md` verbatim with
40
+ `TARGET = { page, screens: ['WIP: <screen>'], fileKey }` and `SKIP = []`. If it
41
+ fails, stop: report its failures as a composer bug, with their links, and leave
42
+ the `WIP:` frame for inspection. Don't change the description to make it pass.
43
+ Remove that frame with `MODE = 'discard'` and `SPEC = { page, screen }` only
44
+ when the user asks.
45
+ 7. **Finalize.** Run the snippet with `MODE = 'finalize'` and
46
+ `SPEC = { page, screen, fileKey }`. It replaces an earlier screen of that name
47
+ only if throughline composed it.
48
+ 8. **Review.** Dispatch the `reviewer` agent in Figma-visual mode with the screen's
49
+ node id (from finalize), the fileKey, the project's working directory (where
50
+ `design-system.json` is) and the user's request as its spec, unless the host
51
+ can't dispatch agents, in which case screenshot the screen yourself and say so.
52
+ 9. **Report.** The screen's link, whether it replaced an earlier one, the gate's
53
+ counts, the reviewer's verdict, and every gap: each component that was asked
54
+ for and doesn't exist, with the nearest names the script offered, and anything
55
+ left out of the request because of it.
56
+
57
+ Never edit a component, a variable or a text style, and never write a raw value.
58
+ If the screen can't be built from what the system has, the gap is the finding.
59
+
@@ -24,8 +24,9 @@ Ask which component to document (e.g. "Button"), then:
24
24
  2. **Project it.** Write `design-system/docs/components/<Name>.doc.json`, set the
25
25
  Figma component `description`, rebuild the doc card's `Usage` band with the
26
26
  canonical builder (`.throughline/references/doc-card-builder.md` —
27
- verify its returned summary and stamp `surfaces.docCard.{src,render,renderer}`
28
- from it), and (if the repo/code side exists) render MDX/JSDoc and run
27
+ assemble the call with `node .throughline/scripts/doc-card-payload.mjs`,
28
+ check the returned `renderHash` against the one it printed, verify the
29
+ returned summary and stamp `surfaces.docCard.{src,render,renderer}` from it), and (if the repo/code side exists) render MDX/JSDoc and run
29
30
  `docs:digest` per the `storybook-chromatic-builder` render step.
30
31
  Before the Figma writes, run the active-file preflight in
31
32
  `.throughline/references/figma-scripting.md` — this command writes
@@ -331,9 +331,11 @@ reference register, not this skill's guide voice.
331
331
  always last.
332
332
  - **Doc card body.** Render the card's `Usage` band with the canonical builder
333
333
  snippet in `.throughline/references/doc-card-builder.md` (via
334
- `figma_execute` with an explicit `timeout`, one card per call): fill the
335
- RECORD/CANONICAL_FP slots, resolve the nine required semantic variables and
336
- the `Body/Default` text style per that file's call contract, run it, and
334
+ `figma_execute` with an explicit `timeout`, one card per call): assemble the
335
+ call with `node .throughline/scripts/doc-card-payload.mjs <record> <cardNodeId>`
336
+ (it fills the RECORD/CANONICAL_FP slots and resolves the nine semantic
337
+ variables and `Body/Default` per that file's call contract), run it, check
338
+ `summary.renderHash` against the `expectedRenderHash` it printed, and
337
339
  verify the returned summary (`rowsRendered`, `blocksCreated`, `cardWidth`) —
338
340
  not a screenshot. The builder creates the `Doc Fingerprint` node itself and
339
341
  is the only thing that may build the usage body — never hand-assemble it.
@@ -19,6 +19,8 @@ ThroughLine builds a design system end to end. Read the matching skill file for
19
19
 
20
20
  ## ThroughLine commands
21
21
 
22
+ - `check-figma` — Check what's already in Figma against the design system, read-only — screens for detached components and raw values a token has, and existing component sets for their states, focus ring and focus contrast. → read `commands/check-figma.md`.
23
+ - `compose-figma` — Compose a screen in Figma from the design system's own components, variables and text styles — every value bound, nothing invented, gated before it's handed over. → read `commands/compose-figma.md`.
22
24
  - `design-system-status` — Show a plain-language summary of the current design system state — what's set up, what's not, and sensible next steps — read from design-system.json. → read `commands/design-system-status.md`.
23
25
  - `document-component` — Author, refresh, or reconcile the usage documentation for one existing component — draft its canonical doc record from four sources, project it to Figma, the doc card, and code, and resolve any drift via a reviewable per-item choice. → read `commands/document-component.md`.
24
26
  - `new-component` — Build a single new component end to end — in Figma, then sync any new tokens, then build its code component and stories — with a confirmation between each stage. → read `commands/new-component.md`.
@@ -0,0 +1,41 @@
1
+ Check the Figma file against the design system without changing it. Use the
2
+ settings already in `design-system.json` (`figma.fileKey`, `project.uiFramework`,
3
+ `components.built`) rather than re-asking them. Scale explanation to
4
+ `user.codingLevel`.
5
+
6
+ 1. **Know the file.** Read `figma.fileKey` from `design-system.json`. If it's
7
+ absent, stop and ask which file to check.
8
+ 2. **Confirm the active file.** Run the preflight in
9
+ `.throughline/references/figma-scripting.md` ("confirm the *active*
10
+ file is your target") against that fileKey, and pin it with `figma_navigate`
11
+ (`lock: true`) for the run. Unpin when done.
12
+ 3. **Screens.** Unless the user named them, ask which page holds the screens, and
13
+ optionally which screens on it. Run the snippet in
14
+ `.throughline/references/figma-adherence.md` verbatim, with
15
+ `const TARGET = { page, screens, fileKey }` (the fileKey from step 1, for the
16
+ links) and `const SKIP = []` filled. The script is the rule; don't re-derive its
17
+ checks.
18
+ 4. **Existing component sets.** Ask which page holds the component sets, so only
19
+ that page is loaded. Ask which colour variables are the page surfaces the
20
+ components sit on, offering the file's likeliest default page surface (read the
21
+ names with `figma_get_variables`), or let the user skip focus contrast. For each
22
+ name in `components.built`, run
23
+ `.throughline/references/figma-checks.md` with `MODE = 'existing'` and
24
+ `EXPECT = { name, page, uiFramework, surfaces }` (`uiFramework` from
25
+ `project.uiFramework`). A name with no set on that page is reported as missing,
26
+ not as a failure of the others. When `focus-indicator` fails on a wrapped control
27
+ (an Input with a label, a Checkbox whose box is an inner frame), look at one focus
28
+ variant's layers before reporting it: if the ring is on an inner layer, run that
29
+ set again with `controlLayer` naming that layer, and say in the report that you
30
+ did.
31
+ 5. **Report.** The screen gate gives one pass or fail for the page: list its
32
+ failures grouped by screen (the path's second segment), each with its detail and
33
+ link, then the advisories, the counts and any skipped rules. Then pass or fail
34
+ per component set, with whether focus contrast ran and against which surfaces. Say plainly what the
35
+ gate can't see: a frame drawn from scratch to look like a component, variables
36
+ that live only in a published library, and focus states drawn as an INSIDE
37
+ stroke.
38
+
39
+ Never edit the file to make a check pass, and never edit a token. A failure is
40
+ a finding for the user; fixing it is a separate, deliberate step.
41
+
@@ -0,0 +1,59 @@
1
+ Compose the screen the user asked for in Figma, from what the file already has.
2
+ Use the settings already in `design-system.json` (`figma.fileKey`) rather than
3
+ re-asking them. Scale explanation to `user.codingLevel`.
4
+
5
+ 1. **Know the file.** Read `figma.fileKey` from `design-system.json`. If it's
6
+ absent, stop and ask which file to compose in.
7
+ 2. **Confirm the active file.** Run the preflight in
8
+ `.throughline/references/figma-scripting.md` ("confirm the *active*
9
+ file is your target") against that fileKey, and pin it with `figma_navigate`
10
+ (`lock: true`) for the run. Unpin when done.
11
+ 3. **Read what exists.** Unless the user named them, ask which page holds the
12
+ component sets and which page the screen goes on (it's created if missing). Run
13
+ the snippet in `.throughline/references/figma-compose.md` verbatim, called
14
+ as its "How to call it" section says (`MODE` and `SPEC` declared above it,
15
+ `return await runCompose(MODE, SPEC);` below it), with `MODE = 'inventory'` and
16
+ `SPEC = { componentsPage }`. If the plan will set an instance-swap property (an
17
+ icon, say), ask which page holds the swap targets and run `inventory` again with
18
+ `iconsPage`.
19
+ 4. **Plan.** Write the screen description (its shape is the `DESCRIPTION` comment
20
+ in the snippet, at the top of the resolver) using only names from the
21
+ inventory's summary: component sets and their properties and options, variables
22
+ by kind, text styles, modes. Leave `sizing` off an instance unless it should
23
+ fill; `hug` works only on a set whose summary says `autoLayout: true`. Frames
24
+ carry layout only (auto layout, gap, padding, fill, stroke, radius); everything
25
+ the user sees is a component instance or styled text. When the request needs a
26
+ component the inventory doesn't have, say so before building and ask: leave it
27
+ out, use a named existing component instead, or stop. Never draw a lookalike
28
+ from frames.
29
+ 5. **Build.** Run the snippet with `MODE = 'build'` and
30
+ `SPEC = { componentsPage, iconsPage, description, fileKey }`. If it returns
31
+ errors, nothing was written: fix the description (a `missing-component` error
32
+ goes back to the user as in step 4) and build again. A `name-taken` error on
33
+ `<screen>` means a screen a person made already has that name: ask for another
34
+ name. On `WIP: <screen>` it means an unmarked frame holds the working name: ask
35
+ the user to rename or remove it. If the call throws instead, the build rolled
36
+ itself back and nothing was written: stop, and report the error as a composer
37
+ bug rather than reworking the screen around it.
38
+ 6. **Gate.** Run the screen gate in
39
+ `.throughline/references/figma-adherence.md` verbatim with
40
+ `TARGET = { page, screens: ['WIP: <screen>'], fileKey }` and `SKIP = []`. If it
41
+ fails, stop: report its failures as a composer bug, with their links, and leave
42
+ the `WIP:` frame for inspection. Don't change the description to make it pass.
43
+ Remove that frame with `MODE = 'discard'` and `SPEC = { page, screen }` only
44
+ when the user asks.
45
+ 7. **Finalize.** Run the snippet with `MODE = 'finalize'` and
46
+ `SPEC = { page, screen, fileKey }`. It replaces an earlier screen of that name
47
+ only if throughline composed it.
48
+ 8. **Review.** Dispatch the `reviewer` agent in Figma-visual mode with the screen's
49
+ node id (from finalize), the fileKey, the project's working directory (where
50
+ `design-system.json` is) and the user's request as its spec, unless the host
51
+ can't dispatch agents, in which case screenshot the screen yourself and say so.
52
+ 9. **Report.** The screen's link, whether it replaced an earlier one, the gate's
53
+ counts, the reviewer's verdict, and every gap: each component that was asked
54
+ for and doesn't exist, with the nearest names the script offered, and anything
55
+ left out of the request because of it.
56
+
57
+ Never edit a component, a variable or a text style, and never write a raw value.
58
+ If the screen can't be built from what the system has, the gap is the finding.
59
+
@@ -24,8 +24,9 @@ Ask which component to document (e.g. "Button"), then:
24
24
  2. **Project it.** Write `design-system/docs/components/<Name>.doc.json`, set the
25
25
  Figma component `description`, rebuild the doc card's `Usage` band with the
26
26
  canonical builder (`.throughline/references/doc-card-builder.md` —
27
- verify its returned summary and stamp `surfaces.docCard.{src,render,renderer}`
28
- from it), and (if the repo/code side exists) render MDX/JSDoc and run
27
+ assemble the call with `node .throughline/scripts/doc-card-payload.mjs`,
28
+ check the returned `renderHash` against the one it printed, verify the
29
+ returned summary and stamp `surfaces.docCard.{src,render,renderer}` from it), and (if the repo/code side exists) render MDX/JSDoc and run
29
30
  `docs:digest` per the `storybook-chromatic-builder` render step.
30
31
  Before the Figma writes, run the active-file preflight in
31
32
  `.throughline/references/figma-scripting.md` — this command writes
@@ -327,9 +327,11 @@ reference register, not this skill's guide voice.
327
327
  always last.
328
328
  - **Doc card body.** Render the card's `Usage` band with the canonical builder
329
329
  snippet in `.throughline/references/doc-card-builder.md` (via
330
- `figma_execute` with an explicit `timeout`, one card per call): fill the
331
- RECORD/CANONICAL_FP slots, resolve the nine required semantic variables and
332
- the `Body/Default` text style per that file's call contract, run it, and
330
+ `figma_execute` with an explicit `timeout`, one card per call): assemble the
331
+ call with `node .throughline/scripts/doc-card-payload.mjs <record> <cardNodeId>`
332
+ (it fills the RECORD/CANONICAL_FP slots and resolves the nine semantic
333
+ variables and `Body/Default` per that file's call contract), run it, check
334
+ `summary.renderHash` against the `expectedRenderHash` it printed, and
333
335
  verify the returned summary (`rowsRendered`, `blocksCreated`, `cardWidth`) —
334
336
  not a screenshot. The builder creates the `Doc Fingerprint` node itself and
335
337
  is the only thing that may build the usage body — never hand-assemble it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radicool/throughline",
3
- "version": "0.24.0",
3
+ "version": "0.26.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": {
@@ -14,6 +14,18 @@ it reads the specimen and never writes it. The status chip keeps its own owner
14
14
 
15
15
  ## How to call it
16
16
 
17
+ Steps 1–4 are one command once `figma.docCardVariables` is recorded:
18
+
19
+ ```sh
20
+ node scripts/doc-card-payload.mjs design-system/docs/components/<Name>.doc.json <cardNodeId> --out <scratch>/payload.js
21
+ ```
22
+
23
+ It writes the complete `figma_execute` body — record, fingerprint, the nine
24
+ role lookups by recorded name, `Body/Default`, the snippet and the call — and
25
+ prints the `expectedRenderHash` to stderr. Pass the file's contents as the
26
+ `code` unchanged, then do steps 5–6. The steps below are what it does, and
27
+ what to do by hand when the mapping isn't recorded yet.
28
+
17
29
  1. Load the record and compute its canonical fingerprint in Node
18
30
  (`canonicalFingerprint` in `scripts/lib/doc-record.mjs`).
19
31
  2. Read `figma.docCardVariables` from `design-system.json`.
@@ -82,6 +94,10 @@ const summary = await renderDocCard({ card, record: RECORD, vars, bodyTextStyle
82
94
  `cardWidth` — not from a screenshot, then stamp the manifest from it:
83
95
  `surfaces.docCard = { src: summary.fingerprint, render: summary.renderHash,
84
96
  renderer: summary.rendererVersion }`. Never re-read the card to stamp.
97
+ With the payload script, first check `summary.renderHash` equals the
98
+ `expectedRenderHash` it printed. A mismatch means the payload changed on
99
+ the way in, or `Body/Default` isn't the size the script assumed — the
100
+ summary's `bodyFontSize` says which; re-run with `--font-size`.
85
101
 
86
102
  ## The snippet
87
103
 
@@ -188,20 +204,11 @@ function planDocCard(record, bodyTextStyle) {
188
204
  };
189
205
  }
190
206
 
191
- // Figma plugin-API renderer for the doc-card Usage band. This file is NOT a
192
- // Node module — build-doc-card-builder.mjs concatenates it after the inlined
193
- // planner (doc-card-plan.mjs) into references/doc-card-builder.md, and the
194
- // result runs inside figma_execute (dynamic-page mode). Constraints honored
195
- // here (see references/figma-scripting.md): async APIs only, style/font set
196
- // BEFORE .characters, fonts loaded up front, resize() sizing modes re-asserted,
197
- // bound paints seeded light-gray (never pure black).
198
- //
199
- // In-scope globals when assembled: planDocCard, DOC_CARD_RENDERER_VERSION
200
- // (inlined planner) and the caller-filled slots RECORD, CANONICAL_FP.
201
-
202
- // 32-bit FNV-1a — the render hash. Only ever compared against itself (the
203
- // manifest's surfaces.docCard.render), so it does not need to match the
204
- // sha256-based canonical fingerprint, which cannot run in the Figma sandbox.
207
+ // 32-bit FNV-1a — the render hash: fnv1a(JSON.stringify(plan)). Only ever
208
+ // compared against itself (the manifest's surfaces.docCard.render), so it does
209
+ // not need to match the sha256-based canonical fingerprint, which cannot run in
210
+ // the Figma sandbox. Lives here, not in the renderer, so Node can compute the
211
+ // hash a render should return (doc-card-payload.mjs) with the same code.
205
212
  function fnv1a(str) {
206
213
  let h = 0x811c9dc5;
207
214
  for (let i = 0; i < str.length; i++) {
@@ -211,6 +218,40 @@ function fnv1a(str) {
211
218
  return h.toString(16).padStart(8, '0');
212
219
  }
213
220
 
221
+ // When a header's title row has no node named `Status Pill`/`Status`, name the
222
+ // node that most likely IS the chip so the error can say what to rename. Never
223
+ // used to pick a node to write — the renderer still refuses to guess. A
224
+ // candidate is a FRAME below the title row whose only child is a TEXT node
225
+ // (the chip shape: a pill frame around its label). Exactly one candidate or
226
+ // no hint. Reads only type/name/id/characters/children, so it runs on Figma
227
+ // nodes and on plain test objects alike.
228
+ function statusChipHint(titleRow) {
229
+ if (!titleRow || titleRow.type !== 'FRAME') return null;
230
+ const found = [];
231
+ const walk = (node) => {
232
+ for (const child of node.children || []) {
233
+ if (child.type === 'FRAME' && child.children && child.children.length === 1
234
+ && child.children[0].type === 'TEXT') {
235
+ found.push({ id: child.id, name: child.name, label: child.children[0].characters });
236
+ }
237
+ walk(child);
238
+ }
239
+ };
240
+ walk(titleRow);
241
+ return found.length === 1 ? found[0] : null;
242
+ }
243
+
244
+ // Figma plugin-API renderer for the doc-card Usage band. This file is NOT a
245
+ // Node module — build-doc-card-builder.mjs concatenates it after the inlined
246
+ // planner (doc-card-plan.mjs) into references/doc-card-builder.md, and the
247
+ // result runs inside figma_execute (dynamic-page mode). Constraints honored
248
+ // here (see references/figma-scripting.md): async APIs only, style/font set
249
+ // BEFORE .characters, fonts loaded up front, resize() sizing modes re-asserted,
250
+ // bound paints seeded light-gray (never pure black).
251
+ //
252
+ // In-scope globals when assembled: planDocCard, DOC_CARD_RENDERER_VERSION,
253
+ // fnv1a, statusChipHint (inlined planner) and the caller-filled slots RECORD, CANONICAL_FP.
254
+
214
255
  const REQUIRED_VARS = [
215
256
  'textDefault', 'textMuted', 'tonePositive', 'toneNegative', 'border',
216
257
  'spacePadding', 'spaceRowGap', 'spaceBlockGap', 'spaceItemGap',
@@ -341,7 +382,13 @@ async function renderDocCard({ card, record, vars, bodyTextStyle }) {
341
382
  const descAmbiguous = !named && descCandidates.length > 1;
342
383
 
343
384
  const missingAnchors = [];
344
- if (!hasStatusAnchor) missingAnchors.push('status (title row must contain a descendant named "Status Pill" or "Status")');
385
+ if (!hasStatusAnchor) {
386
+ const hint = statusChipHint(titleRow);
387
+ missingAnchors.push('status (title row must contain a descendant named "Status Pill" or "Status"'
388
+ + (hint ? ' — frame ' + hint.id + ' "' + hint.name + '" holding "' + hint.label
389
+ + '" looks like the status chip; rename it "Status Pill" and re-run' : '')
390
+ + ')');
391
+ }
345
392
  if (!headerDescCandidate) {
346
393
  missingAnchors.push(descAmbiguous
347
394
  ? 'description (found ' + descCandidates.length + ' candidate TEXT children, cannot tell which is the description — name the right one "Header Description" by hand and re-run)'