@radicool/throughline 0.12.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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/adapters/codex/AGENTS.md +29 -0
  4. package/adapters/codex/codex-mcp.toml +4 -0
  5. package/adapters/codex/prompts/component-builder.md +272 -0
  6. package/adapters/codex/prompts/component-pipeline.md +81 -0
  7. package/adapters/codex/prompts/design-system-audit.md +122 -0
  8. package/adapters/codex/prompts/design-system-status.md +41 -0
  9. package/adapters/codex/prompts/figma-environment-setup.md +456 -0
  10. package/adapters/codex/prompts/icon-system-builder.md +268 -0
  11. package/adapters/codex/prompts/new-component.md +13 -0
  12. package/adapters/codex/prompts/repository-builder.md +163 -0
  13. package/adapters/codex/prompts/retrofit-planner.md +136 -0
  14. package/adapters/codex/prompts/start.md +22 -0
  15. package/adapters/codex/prompts/storybook-chromatic-builder.md +261 -0
  16. package/adapters/codex/prompts/sync-figma-tokens.md +22 -0
  17. package/adapters/codex/prompts/token-builder.md +361 -0
  18. package/adapters/codex/prompts/token-crosswalk-builder.md +120 -0
  19. package/adapters/codex/prompts/token-sheet-builder.md +162 -0
  20. package/adapters/codex/prompts/token-sync-layer.md +232 -0
  21. package/adapters/cursor/.cursor/commands/design-system-status.md +41 -0
  22. package/adapters/cursor/.cursor/commands/new-component.md +13 -0
  23. package/adapters/cursor/.cursor/commands/start.md +22 -0
  24. package/adapters/cursor/.cursor/commands/sync-figma-tokens.md +22 -0
  25. package/adapters/cursor/.cursor/mcp.json +14 -0
  26. package/adapters/cursor/.cursor/rules/component-builder.mdc +276 -0
  27. package/adapters/cursor/.cursor/rules/component-pipeline.mdc +85 -0
  28. package/adapters/cursor/.cursor/rules/design-system-audit.mdc +126 -0
  29. package/adapters/cursor/.cursor/rules/figma-environment-setup.mdc +460 -0
  30. package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +272 -0
  31. package/adapters/cursor/.cursor/rules/repository-builder.mdc +167 -0
  32. package/adapters/cursor/.cursor/rules/retrofit-planner.mdc +140 -0
  33. package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +265 -0
  34. package/adapters/cursor/.cursor/rules/token-builder.mdc +365 -0
  35. package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +124 -0
  36. package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +166 -0
  37. package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +236 -0
  38. package/adapters/generic/AGENTS.md +46 -0
  39. package/adapters/generic/commands/design-system-status.md +41 -0
  40. package/adapters/generic/commands/new-component.md +13 -0
  41. package/adapters/generic/commands/start.md +22 -0
  42. package/adapters/generic/commands/sync-figma-tokens.md +22 -0
  43. package/adapters/generic/skills/component-builder/SKILL.md +272 -0
  44. package/adapters/generic/skills/component-pipeline/SKILL.md +81 -0
  45. package/adapters/generic/skills/design-system-audit/SKILL.md +122 -0
  46. package/adapters/generic/skills/figma-environment-setup/SKILL.md +456 -0
  47. package/adapters/generic/skills/icon-system-builder/SKILL.md +268 -0
  48. package/adapters/generic/skills/repository-builder/SKILL.md +163 -0
  49. package/adapters/generic/skills/retrofit-planner/SKILL.md +136 -0
  50. package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +261 -0
  51. package/adapters/generic/skills/token-builder/SKILL.md +361 -0
  52. package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +120 -0
  53. package/adapters/generic/skills/token-sheet-builder/SKILL.md +162 -0
  54. package/adapters/generic/skills/token-sync-layer/SKILL.md +232 -0
  55. package/package.json +29 -0
  56. package/references/brainstorm-before-build.md +139 -0
  57. package/references/brownfield-retrofit.md +93 -0
  58. package/references/coding-level.md +70 -0
  59. package/references/crosswalk-schema.md +113 -0
  60. package/references/figma-component-standards.md +532 -0
  61. package/references/figma-publishing.md +125 -0
  62. package/references/figma-scripting.md +278 -0
  63. package/references/manifest-schema.md +341 -0
  64. package/references/scaling-up-handoff.md +61 -0
  65. package/references/sync-adapters.md +141 -0
  66. package/scripts/README.md +78 -0
  67. package/scripts/adapters/emit-codex.mjs +58 -0
  68. package/scripts/adapters/emit-cursor.mjs +26 -0
  69. package/scripts/adapters/emit-generic.mjs +41 -0
  70. package/scripts/adapters/generate.mjs +73 -0
  71. package/scripts/adapters/read-sources.mjs +54 -0
  72. package/scripts/adapters/translate.mjs +51 -0
  73. package/scripts/build-reverse-index.mjs +56 -0
  74. package/scripts/crosswalk.schema.json +33 -0
  75. package/scripts/grep-color-usage.mjs +143 -0
  76. package/scripts/guard-token-removal.mjs +95 -0
  77. package/scripts/install.mjs +143 -0
  78. package/scripts/lib/crosswalk.mjs +91 -0
  79. package/scripts/validate-crosswalk.mjs +96 -0
@@ -0,0 +1,278 @@
1
+ # Figma scripting via `figma_execute`
2
+
3
+ Shared gotchas for **any** skill that writes to Figma through the Console MCP's
4
+ `figma_execute` (component-builder, icon-system-builder, token-builder,
5
+ token-sheet-builder). The bridge runs in Figma's **`dynamic-page`** document mode
6
+ and executes batched JS. Several behaviors below have caused silent, hard-to-
7
+ screenshot corruption — read this before authoring `figma_execute` scripts, and
8
+ keep the read-backs in your post-build audit.
9
+
10
+ ## Preflight: one *live* bridge instance per file (concurrent-write corruption)
11
+
12
+ Before any write, call **`figma_get_status`** and inspect `otherInstances`.
13
+ Concurrent writes from two **live** Desktop Bridge instances connected to the same
14
+ `fileKey` collide and produce **truncated parent frames and orphaned node
15
+ fragments** at negative coordinates — damage a screenshot won't reveal. So a second
16
+ *live* instance is a hard stop.
17
+
18
+ **But do not hard-block on *stale* entries (bug B4).** Users routinely hit a wall
19
+ where `otherInstances` lists ports they never opened — phantom/stale connections
20
+ left by a plugin reload, a file switch, or an MCP reconnect that spawned a new port
21
+ without reaping the old one. Telling them to "close the other instance" is useless
22
+ when they never opened one. Distinguish the two cases before blocking:
23
+
24
+ 1. **Verify liveness, don't assume it.** Treat an `otherInstances` entry as
25
+ *suspected stale* until confirmed live. Attempt a `figma_reconnect` (or re-read
26
+ `figma_get_status`) — stale ports typically drop out after a reconnect — or use a
27
+ liveness/heartbeat signal if the MCP exposes one.
28
+ 2. **Only the genuinely-live count blocks.** If exactly one live instance remains
29
+ after reaping stale entries, proceed. Only block when **two or more** instances
30
+ are confirmed live.
31
+ 3. **If you must block, be actionable.** Name the exact ports, state which are
32
+ suspected stale vs. live, and give a concrete clear path (run `figma_reconnect`,
33
+ reload the bridge plugin, or restart the MCP client) — never a bare "shut down
34
+ the others." If only stale entries remain and they won't clear, say so plainly
35
+ and let the user proceed rather than dead-ending them.
36
+
37
+ This is the bridge-side application of the read-discipline principle (B4) in
38
+ `${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md`: don't assert "another
39
+ instance is active" without confirming it's actually live.
40
+
41
+ ## Read discipline: never report "empty" without a verified read (B1/B2)
42
+
43
+ Before reporting that a file has no variables, no text styles, or no effect styles,
44
+ you MUST have run an explicit read **for that specific class** that returned empty —
45
+ after the file is fully loaded. Two real bugs came from violating this:
46
+
47
+ - **B1** — a first read returned `0` variables on a fully-populated file (stale/early
48
+ read) and was reported as fact. **Fix:** `await figma.loadAllPagesAsync()` before
49
+ counting; treat a `0` on first read as suspect and re-read before reporting; prefer
50
+ the dedicated `figma_get_variables` tool (handles `dynamic-page`, resolves aliases).
51
+ - **B2** — "no text styles" was asserted because no text *variables* were found —
52
+ styles were never read. **Fix:** variables and styles are different surfaces. Read
53
+ each independently: variables (`figma_get_variables`), text styles
54
+ (`figma_get_text_styles`), effect/paint styles (`figma_get_styles`). Report "none"
55
+ only for the class whose own read came back empty.
56
+
57
+ An unexpectedly-empty result is a possible read error, not ground truth. See the
58
+ full principle in `${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md`.
59
+
60
+ - **Read-after-write: pass `refreshCache: true` on the verifying read.** A specific
61
+ trigger of the false-empty class — immediately after a write in the same session,
62
+ `figma_get_variables` (and similar reads) can return a **stale cached empty** with an
63
+ old timestamp. Creating 25 semantic variables and reading them straight back returned
64
+ `variables: []`; the same read with `refreshCache: true` returned all 25. After **any**
65
+ variable/style write, pass `refreshCache: true` on the read that confirms it.
66
+
67
+ ## `dynamic-page` mode: use the async APIs — reads **and** writes
68
+
69
+ Synchronous document-wide getters *and several setters* throw under
70
+ `dynamic-page` (the error reads `Cannot call with documentAccess: dynamic-page.
71
+ Use figma.<x>Async instead.`). Use the async variants and `await` them:
72
+
73
+ - **Node lookup:** `await figma.getNodeByIdAsync(id)` (**not** `figma.getNodeById(id)`).
74
+ This is the most common one in write-back scripts (finding a doc card / `Status`
75
+ chip by id) — default every node lookup to the async form.
76
+ - **Reads:** `getLocalVariableCollectionsAsync`, `getVariableByIdAsync`,
77
+ `getVariablesByCollectionAsync`, `getStyleByIdAsync`, `loadAllPagesAsync`.
78
+ - **Writes / setters:** `setCurrentPageAsync(page)` (**not** `figma.currentPage =`),
79
+ `setTextStyleIdAsync(id)` (**not** `node.textStyleId =`), and likewise
80
+ `setFillStyleIdAsync`, `setStrokeStyleIdAsync`, `setEffectStyleIdAsync`.
81
+
82
+ Generate scripts with the async forms from the start — retrofitting after a sync
83
+ setter throws mid-build is how partial writes happen. For a simple verification
84
+ read, prefer the dedicated `figma_get_variables` tool (it handles `dynamic-page`
85
+ correctly and resolves aliases with `resolveAliases: true`) over a hand-written
86
+ script.
87
+
88
+ ## Set the text style *before* writing `.characters` (font-load order)
89
+
90
+ `figma.createText()` starts every node as **Inter Regular** — so writing
91
+ `.characters` first throws `Cannot write to node with unloaded font "Inter Regular"`
92
+ even when you only loaded your brand fonts (e.g. Bricolage Grotesque + DM Mono).
93
+ Applying the text style switches the node to a loaded family, so it must come **first**.
94
+ This is the same call surface as the `setTextStyleIdAsync` (dynamic-page) note above —
95
+ both bite in the same place.
96
+
97
+ ```js
98
+ const t = figma.createText();
99
+ await t.setTextStyleIdAsync(style.id); // switches font to the loaded family FIRST
100
+ t.characters = str; // now safe — no Inter load needed
101
+ ```
102
+
103
+ ## Binding an effect resets its geometry — re-assert `spread`/`radius`/`offset` after
104
+
105
+ `figma.variables.setBoundVariableForEffect(effect, 'color', v)` returns a **new**
106
+ effect object that drops every non-color field back to defaults — `spread → 0`,
107
+ `radius → 0`, `offset → {0,0}`. A focus ring built as a drop-shadow (offset 0, blur 0,
108
+ **spread 3**) therefore renders **invisible** after you bind its color: a read-back
109
+ shows `spread: 0` even though the literal set `spread: 3`. Re-assert the geometry
110
+ fields **after** binding, then assign:
111
+
112
+ ```js
113
+ let eff = {type:'DROP_SHADOW', color:{r:0,g:0,b:0,a:1}, offset:{x:0,y:0}, radius:0, spread:3, visible:true, blendMode:'NORMAL'};
114
+ eff = figma.variables.setBoundVariableForEffect(eff, 'color', ringVar);
115
+ eff = {...eff, spread:3, radius:0, offset:{x:0,y:0}}; // REQUIRED — bind wiped these
116
+ node.effects = [eff];
117
+ ```
118
+
119
+ ## A drop-shadow casts only from **opaque pixels** — transparent frames get no ring
120
+
121
+ Unlike CSS `box-shadow` (which draws from the border-box), a Figma `DROP_SHADOW` is
122
+ computed from the node's rendered **alpha**. A no-fill frame has nothing to cast, so a
123
+ shadow-based focus ring (`0 0 0 3px`) shows on filled controls but is **completely
124
+ absent** on transparent variants (outline / ghost / link). **`clipsContent` does NOT
125
+ fix this** — it changes child clipping, not casting geometry (the commonly-cited
126
+ "clip to make the shadow follow the radius" trick does nothing here). Verify by
127
+ temporarily setting the ring to solid red: only filled frames will show it.
128
+
129
+ **Pattern for a universal ring:**
130
+ - **Filled control** → a drop-shadow effect (clean, no extra node).
131
+ - **Transparent control** → an **absolutely-positioned ring child**: a `RECTANGLE`
132
+ with `layoutPositioning = "ABSOLUTE"`, `strokeAlign = "OUTSIDE"`, stroke weight = ring
133
+ width, sized to the parent with `STRETCH` constraints, parent `clipsContent = false`.
134
+ A **child, not a wrapper** — it doesn't inflate layout and coexists with an existing
135
+ border.
136
+
137
+ Both map to the same `box-shadow: 0 0 0 3px var(--ring)` in code. See the build-side
138
+ rules in `${CLAUDE_PLUGIN_ROOT}/references/figma-component-standards.md` ("State
139
+ handling").
140
+
141
+ ## Give a bound paint a sensible placeholder color, and read the bind back
142
+
143
+ A paint whose color you intend to bind can briefly render its **literal** color if the
144
+ bind is late or doesn't stick — and a pure-black `{0,0,0}` placeholder then reads as an
145
+ accidental **dark-mode** panel (seen once on a component-set background bound to
146
+ `bg/default`; re-running the identical bind fixed it). Two safeguards:
147
+
148
+ - Seed the paint with the token's **approximate value**, not pure black, so a
149
+ failed/late bind degrades gracefully.
150
+ - **Read back `fills[0].boundVariables.color`** on container / component-set fills in
151
+ the post-build audit — a screenshot can't tell a stuck bind from a placeholder.
152
+
153
+ ## `resize()` locks the *opposite* auto-layout axis to FIXED
154
+
155
+ Calling `node.resize(w, h)` on an auto-layout frame to set one dimension silently
156
+ flips the **other** axis' sizing mode to `FIXED`, pinning it. The classic symptom
157
+ is an auto-layout frame that **collapses to ~10px** on the pinned axis with content
158
+ overlapping or clipped (seen on color grids, variant cells, whole component sets).
159
+ A screenshot may not show it — only a sizing-mode/height read-back does.
160
+
161
+ **Pattern — prefer the layout-sizing setters; if you must `resize()`, re-assert
162
+ the modes immediately after:**
163
+
164
+ ```js
165
+ // Preferred: express hug/fill intent directly (no axis gets pinned)
166
+ frame.layoutSizingHorizontal = "FILL" // or "HUG" / "FIXED"
167
+ frame.layoutSizingVertical = "HUG"
168
+
169
+ // If resize() is unavoidable, restore the intended modes right after:
170
+ frame.resize(width, frame.height)
171
+ frame.primaryAxisSizingMode = "AUTO" // your intended mode
172
+ frame.counterAxisSizingMode = "AUTO"
173
+ ```
174
+
175
+ Read back `primaryAxisSizingMode` / `counterAxisSizingMode` (or
176
+ `layoutSizing*`) in the post-build audit — a collapsed axis is otherwise invisible
177
+ until handoff.
178
+
179
+ **The axis mapping is inverted for VERTICAL frames — this is the part that bites.**
180
+ For a **VERTICAL** auto-layout frame, `primaryAxisSizingMode` controls **height** and
181
+ `counterAxisSizingMode` controls **width** (the *opposite* of a HORIZONTAL frame, where
182
+ primary = width). It's very easy to think "fixed width, hug height" and set the axes
183
+ backwards, after which `resize()` pins the wrong dimension — the real symptoms were an
184
+ Input variant collapsing to 10px tall (placeholders overlapping), a Card's *height*
185
+ locking to 10px, and a parent layout frame's height pinned to a stale value.
186
+
187
+ This is exactly why the **`layoutSizingHorizontal` / `layoutSizingVertical`** setters
188
+ are safer: they name the dimension directly, so there's no primary/counter axis to get
189
+ backwards.
190
+
191
+ ```js
192
+ // VERTICAL frame, want fixed width + hug height:
193
+ frame.layoutMode = "VERTICAL";
194
+ frame.layoutSizingHorizontal = "FIXED"; // width — unambiguous
195
+ frame.layoutSizingVertical = "HUG"; // height — unambiguous
196
+ // Equivalent via axis modes (easy to invert): counter = width, primary = height
197
+ // frame.counterAxisSizingMode = "FIXED"; frame.primaryAxisSizingMode = "AUTO";
198
+ ```
199
+
200
+ ## Pass an explicit `timeout` for batch / multi-node writes
201
+
202
+ `figma_execute`'s default timeout (~5000ms) is too low for scripts that touch
203
+ **many nodes in sequence** — especially anything that `await`s `loadFontAsync`
204
+ per node (text edits, doc-card status write-backs). The call **times out mid-run
205
+ with no partial-success signal**, leaving a half-applied write. For any batch
206
+ operation, pass an explicit `timeout` sized to the work — a good rule is
207
+ **`node_count * 3000` ms** (e.g. a 9-card status write-back → `timeout: 30000`).
208
+ Load fonts once and reuse where possible rather than re-loading per node.
209
+
210
+ **But `figma_execute` is effectively capped at ~30s regardless of the `timeout` you
211
+ pass** — a bigger number is not honored past that ceiling. So `node_count * 3000` is
212
+ only a guide *up to* the cap; a build that genuinely needs more than ~30s must be
213
+ **chunked into multiple `figma_execute` calls**, not handed a larger timeout. Design
214
+ large builds for the cap from the start — the 108-variant Button was built as **3× 36-
215
+ variant passes**. (This is the same ceiling behind the WRAP-chunking rule below.)
216
+
217
+ ## Large wrapped auto-layout (`layoutWrap = "WRAP"`) is expensive — chunk it
218
+
219
+ Building a large grid as a **single `layoutWrap = "WRAP"` frame in one
220
+ `figma_execute` call** can exceed the execute budget (~30s) and roll back the
221
+ *entire* call. WRAP relayout cost grows disproportionately with cell count. For
222
+ large grids (e.g. a 30+ cell color or variant grid), build **manual rows** —
223
+ nested horizontal auto-layout frames inside a vertical parent — instead of one
224
+ WRAP frame, or split the build across **multiple `figma_execute` calls**. The
225
+ identical content that times out as one WRAP frame builds fine as manual rows.
226
+
227
+ ## Binding-survival audit: count variable bindings before and after a rename
228
+
229
+ A brownfield retrofit renames variables **in place** to preserve their Figma IDs
230
+ (guardrail 3 in `${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md` — a
231
+ delete-and-recreate unbinds every consumer). The only way to *prove* a rename kept
232
+ its bindings is to count consuming bindings before and after. Run this read with
233
+ the dedicated tooling where possible (`figma_get_variables`), or via `figma_execute`
234
+ when you need the raw consumer count.
235
+
236
+ A variable's bindings are not enumerable directly, so count **consumers**: nodes and
237
+ styles whose bound properties reference each variable id. The robust, `dynamic-page`-safe
238
+ approach is to snapshot the total consumer count across the file before the rename,
239
+ rename in place, then re-snapshot and assert equality.
240
+
241
+ ```js
242
+ // dynamic-page safe: load everything, then walk consumers counting variable refs.
243
+ await figma.loadAllPagesAsync();
244
+
245
+ function countBoundVariableRefs(node, tally) {
246
+ const bv = node.boundVariables;
247
+ if (bv) {
248
+ for (const key of Object.keys(bv)) {
249
+ const entry = bv[key];
250
+ const refs = Array.isArray(entry) ? entry : [entry];
251
+ for (const r of refs) {
252
+ if (r && r.id) tally[r.id] = (tally[r.id] || 0) + 1;
253
+ }
254
+ }
255
+ }
256
+ if ('children' in node) {
257
+ for (const child of node.children) countBoundVariableRefs(child, tally);
258
+ }
259
+ return tally;
260
+ }
261
+
262
+ const tally = {};
263
+ for (const page of figma.root.children) countBoundVariableRefs(page, tally);
264
+ const totalBindings = Object.values(tally).reduce((a, b) => a + b, 0);
265
+ // Report totalBindings (and tally per id) BEFORE the rename; re-run AFTER and
266
+ // assert the total is unchanged. A drop means a binding was severed — STOP and
267
+ // investigate (almost always a delete-and-recreate slipped in).
268
+ ```
269
+
270
+ - **Pass an explicit `timeout`** (this walks every node — size it per the batch-timeout
271
+ rule above; a large file needs tens of seconds).
272
+ - **Style bindings count too.** Text/effect/paint styles can bind variables; include a
273
+ pass over `getLocalTextStylesAsync()` / `getLocalPaintStylesAsync()` /
274
+ `getLocalEffectStylesAsync()` and their `boundVariables` if the file uses style-level
275
+ bindings.
276
+ - **This is the number `design-system-audit` records** as
277
+ `audit.figmaInventory.bindings`, and the before/after gate the `token-builder`
278
+ brownfield branch runs around every rename.
@@ -0,0 +1,341 @@
1
+ # The `design-system.json` manifest
2
+
3
+ This file is the single source of truth for **what has been set up** in a user's
4
+ design system. It lives at the root of the user's working directory. It does NOT
5
+ store token values (Figma owns those) — it stores decisions, progression state,
6
+ and pointers.
7
+
8
+ Every skill's first action is to read this file (creating it with defaults if
9
+ absent). Every skill's last action is to update the fields it owns and report
10
+ what changed. Gating decisions are made by reading this file: if a prerequisite
11
+ field is unset, the skill **offers** to run the prerequisite skill rather than
12
+ bailing or running silently.
13
+
14
+ ## Schema (schemaVersion 4)
15
+
16
+ ```json
17
+ {
18
+ "schemaVersion": 4,
19
+ "user": {
20
+ "codingLevel": "new"
21
+ },
22
+ "project": {
23
+ "uiFramework": null
24
+ },
25
+ "workspace": {
26
+ "name": "my-design-system",
27
+ "localPath": ".",
28
+ "stage": "folder",
29
+ "origin": null,
30
+ "detectedLayers": {
31
+ "monorepo": null,
32
+ "storybook": null,
33
+ "tokens": null,
34
+ "syncLayer": null
35
+ }
36
+ },
37
+ "figma": {
38
+ "mechanism": null,
39
+ "fileKey": null,
40
+ "connected": false,
41
+ "lastVerified": null,
42
+ "coverPageBuilt": false,
43
+ "canPublish": null,
44
+ "libraryPublished": false,
45
+ "publishedAt": null
46
+ },
47
+ "tokens": {
48
+ "intakeMode": null,
49
+ "tiers": 2,
50
+ "primitivesBuilt": false,
51
+ "semanticBuilt": false,
52
+ "stylesBuilt": false,
53
+ "collections": [],
54
+ "styleGroups": [],
55
+ "lastSync": null
56
+ },
57
+ "sheets": { "built": false },
58
+ "icons": {
59
+ "library": null,
60
+ "version": null,
61
+ "built": false,
62
+ "packageInstalled": false,
63
+ "subset": []
64
+ },
65
+ "components": {
66
+ "built": [],
67
+ "meta": {},
68
+ "instanceSwapUpgradePending": []
69
+ },
70
+ "repo": {
71
+ "stage": "none",
72
+ "packageManager": "pnpm",
73
+ "monorepo": "turborepo",
74
+ "remote": null
75
+ },
76
+ "sync": {
77
+ "platforms": [],
78
+ "customAdapters": [],
79
+ "lastRun": null
80
+ },
81
+ "storybook": {
82
+ "initialized": false,
83
+ "chromatic": false,
84
+ "codeConnect": false
85
+ },
86
+ "audit": {
87
+ "ranAt": null,
88
+ "codeSurface": null,
89
+ "figmaInventory": null,
90
+ "percentSemantic": null
91
+ },
92
+ "tokenCrosswalk": {
93
+ "path": null,
94
+ "statusCounts": null,
95
+ "validatorPassing": null
96
+ },
97
+ "retrofit": {
98
+ "phase": null,
99
+ "startedAt": null,
100
+ "completedAt": null,
101
+ "journalScaffolded": false
102
+ },
103
+ "completedSkills": []
104
+ }
105
+ ```
106
+
107
+ ## Field reference
108
+
109
+ ### `user`
110
+ - `codingLevel` — `"new"`, `"some"`, or `"comfortable"`. Governs how much
111
+ code/git/terminal concepts are explained in the code-touching skills. Set in
112
+ `figma-environment-setup` via concrete anchoring questions; changeable
113
+ anytime. See `${CLAUDE_PLUGIN_ROOT}/references/coding-level.md`. Changes explanation only, never
114
+ capability. Default `"new"` (over-explaining is recoverable; under-explaining
115
+ strands beginners).
116
+
117
+ ### `project`
118
+ - `uiFramework` — the target UI framework (e.g. `"shadcn"`, `"mui"`,
119
+ `"vanilla"`, or a generated target). Captured **lazily at first relevance** —
120
+ by whichever of `component-builder` or `token-sync-layer` runs first — and read
121
+ by both, so it's asked once. It does **not** affect tokens (framework-neutral)
122
+ and does **not** affect Figma component *structure* (also neutral); it informs
123
+ component **variant vocabulary and naming** so the Figma component API lines up
124
+ with the framework's code component API. For multi-framework targets, use a
125
+ neutral vocabulary and let each adapter map it.
126
+
127
+ ### `workspace`
128
+ - `name` — human-friendly project name the user picked.
129
+ - `localPath` — path to the working directory, relative to the manifest. Almost
130
+ always `"."` since the manifest lives in the working dir.
131
+ - `stage` — the folder→git→github progression. One of:
132
+ - `"folder"` — a plain local folder, no version control. The default. The
133
+ entire Figma authoring phase (skills 0–4) is fully usable in this stage.
134
+ - `"local-git"` — `git init` has been run; local history exists, no remote.
135
+ - `"github"` — connected to a GitHub remote; PRs and CI are possible.
136
+
137
+ Advancing stages is the job of the repository-builder skill (5). Downstream
138
+ skills that need a later stage (e.g. token-sync wants at least `local-git`)
139
+ read this field and offer to advance it.
140
+
141
+ - `origin` — how the user's project was configured at intake time. Set **once** by
142
+ `figma-environment-setup` Step 0 and never overwritten by any downstream skill.
143
+ Values:
144
+ - `"greenfield"` — empty or newly created folder; no repo or tooling detected
145
+ - `"existing-repo"` — `package.json` present but no monorepo config; user will need
146
+ to convert to monorepo before the code phase
147
+ - `"existing-monorepo"` — `detectedLayers.monorepo` is `true` (see detection criteria there); code
148
+ phase skills should adapt rather than scaffold from scratch
149
+ - `"unknown"` — scan was inconclusive; treat conservatively (prompt the user)
150
+ - `null` — intake has not yet run (default)
151
+
152
+ - `detectedLayers` — snapshot of tooling found in the working directory at intake time.
153
+ Written by `figma-environment-setup` Step 0, read by downstream skills to adapt
154
+ behavior. `null` = not yet scanned. `false` = scanned, not found. `true` = found.
155
+ `detectedLayers` records pre-existing tooling found before any skill ran; it does not replace the canonical per-skill flags (`storybook.initialized`, `repo.monorepo`, etc.) which track whether this project's skills have set those layers up.
156
+ - `monorepo` — both `turbo.json` and `pnpm-workspace.yaml` are present
157
+ - `storybook` — `.storybook/` directory is present
158
+ - `tokens` — `tokens.json` or a `tokens/` directory is present
159
+ - `syncLayer` — `style-dictionary.config.js` or `*.style-dictionary.js` files present at the project root or within immediate subdirectories
160
+
161
+ ### `figma`
162
+ - `mechanism` — which write mechanism is active. One of `"console-mcp"`
163
+ (default, recommended) or `"official-plugin"` (lower-setup fallback). Set by
164
+ skill 0. The adapter layer in every Figma skill reads this to know which tool
165
+ names to use.
166
+ - `fileKey` — the Figma file key the system writes into (extracted from the
167
+ file URL). Lets skills target the right file without re-asking.
168
+ - `connected` — whether setup completed successfully at least once. Note this is
169
+ a record of *setup completion*, not live connection state — connection is
170
+ verified live each run (see skill 0's liveness check).
171
+ - `lastVerified` — ISO timestamp of the last successful liveness check.
172
+ - `coverPageBuilt` — whether the branded **Cover** page has been generated in the
173
+ file (set by `token-sheet-builder`, *not* `figma-environment-setup` — the Cover
174
+ is built after tokens and styles exist so it can be on-brand). The plugin cannot
175
+ set the file thumbnail via the API, so "set as thumbnail" stays a one-time manual
176
+ user step.
177
+ - `canPublish` — whether the user can publish a Figma **team library** (requires
178
+ a paid plan, Professional+). `true` / `false` / `null` (unknown / not yet
179
+ asked). Asked once and recorded; gates the typed instance-swap dropdown path.
180
+ See `${CLAUDE_PLUGIN_ROOT}/references/figma-publishing.md`.
181
+ - `libraryPublished` — whether the user has published the file as a library at
182
+ least once (so local component keys resolve for `INSTANCE_SWAP`). User-driven
183
+ and manual; the plugin verifies, never publishes. **Treat the default `false`
184
+ as _unverified_, not "definitely not published" (bug B3): before asserting a
185
+ library is unpublished, attempt detection (`figma_get_library_components` /
186
+ `figma_get_library_variables`); if detection is inconclusive, ask the user once
187
+ and record the answer here. Never silently assume unpublished — see the read
188
+ discipline in `${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md`.**
189
+ - `publishedAt` — ISO timestamp the user last confirmed a publish, for "you may
190
+ need to re-publish after adding components" messaging.
191
+
192
+ ### `tokens`
193
+ - `intakeMode` — how the user started: `"generative"` (seed expanded by AI),
194
+ `"descriptive"` (from aesthetic direction), `"import"` (existing set
195
+ organized), or `"retrofit"` (a mature codebase **and** a populated Figma file
196
+ reconciled onto tokens — the brownfield path; differs from `"import"` by having
197
+ existing code bindings to inspect and converge). Recorded so later runs know how
198
+ the system was built.
199
+ - `tiers` — `2` (primitive + semantic, default) or `3` (adds a component tier,
200
+ multi-brand opt-in only).
201
+ - `primitivesBuilt` / `semanticBuilt` / `stylesBuilt` — phase completion flags.
202
+ The primitive→semantic checkpoint seam depends on the first two; `stylesBuilt`
203
+ covers the text/effect/grid styles phase.
204
+ - `collections` — names of the Figma variable collections created. With the
205
+ per-category structure this is several names per tier, e.g.
206
+ `["_Color/Primitive", "Spacing/Primitive", "_Typography/Primitive",
207
+ "_Radius/Primitive", "_Border/Primitive", "Color/Semantic", "Spacing/Semantic",
208
+ "Typography/Semantic", "Radius/Semantic", "Border/Semantic"]`.
209
+ - `styleGroups` — names of Figma style groups created (e.g.
210
+ `["Text", "Elevation"]`). Styles are distinct from variables.
211
+ - `lastSync` — ISO timestamp tokens were last extracted to code (set by skill 6,
212
+ not by the token builder).
213
+
214
+ ### `sheets`
215
+ - `built` — whether the visual "Foundations" page has been generated. Swatches
216
+ live-bind to variables; labels/descriptions are snapshots refreshed by
217
+ re-running the token-sheet-builder.
218
+
219
+ ### `icons`
220
+ - `library` — `"lucide"`, `"tabler"`, `"phosphor"`, `"material"`, or `"custom"`.
221
+ (`lucide`/`tabler`/`phosphor` share the official-SVG-fetch mechanism.)
222
+ - `version` — the library version the Figma mirror was built from (e.g. Lucide
223
+ `"0.424.0"`). The drift check compares this against the installed npm package
224
+ so the Figma icons and code icons stay the same generation. `null` for custom.
225
+ - `built` — whether the Figma Icons page has been populated.
226
+ - `packageInstalled` — whether the code-side npm package (`lucide-react`,
227
+ `@tabler/icons-react`, `@phosphor-icons/react`, `@mui/icons-material`) has been
228
+ installed. Library icons reach code via install
229
+ + name mapping, never by generating component code; custom icons reach code via
230
+ the sync layer's SVGR pipeline instead.
231
+ - `subset` — the curated list of icon names imported (most projects need a
232
+ subset, not the full library). Grows by re-running icon-system-builder.
233
+
234
+ ### `components`
235
+ - `built` — array of component names created in Figma (e.g. `["Button",
236
+ "Input"]`). Each component's spec, including icon-slot contracts, is recorded
237
+ for the code side (via Code Connect when available, else the repo component
238
+ spec).
239
+ - `meta` — object keyed by component name holding the values stamped onto each
240
+ component's documentation artboard (see
241
+ `${CLAUDE_PLUGIN_ROOT}/references/figma-component-standards.md`): `{ "Button":
242
+ { "status": "stable", "updatedAt": "<ISO>" } }`. `status` is one of
243
+ `"draft"` / `"beta"` / `"stable"` / `"deprecated"`. **Lifecycle:** a component
244
+ is created at `"draft"` by `component-builder` (Figma exists, no code yet) and
245
+ promoted to `"stable"` by `storybook-chromatic-builder` when its code + stories
246
+ are built and approved. That promotion also writes the new chip color +
247
+ last-updated date back into the Figma doc card (see "Promoting a component's
248
+ status" in `${CLAUDE_PLUGIN_ROOT}/references/figma-component-standards.md`) — so
249
+ the manifest and the artboard never disagree. Re-running a component refreshes
250
+ its `updatedAt`. Keep `built` (names) as the source of truth for "exists";
251
+ `meta` is supplementary doc metadata.
252
+ - `instanceSwapUpgradePending` — array of component names whose icon/component
253
+ slots were built with the **toggle + manual-swap fallback** because the
254
+ library wasn't published yet, so the typed `INSTANCE_SWAP` dropdown is still
255
+ owed. A later run (after the user publishes) reads this, adds the typed
256
+ dropdowns, and clears the entry. See
257
+ `${CLAUDE_PLUGIN_ROOT}/references/figma-publishing.md`.
258
+
259
+ ### `repo`
260
+ - `stage` — mirrors `workspace.stage` for the repo concern; kept here so repo
261
+ tooling reads one place. Keep in sync with `workspace.stage`.
262
+ - `packageManager` / `monorepo` — defaults `pnpm` / `turborepo`. Recorded so
263
+ later skills don't re-ask.
264
+ - `remote` — the GitHub remote URL once `stage` is `"github"`, else `null`.
265
+
266
+ ### `sync`
267
+ - `platforms` — array of adapter targets configured, e.g.
268
+ `["shadcn", "ios-swift"]`. Curated adapters map to vetted presets; any other
269
+ name is a generated (Tier 2) adapter.
270
+ - `customAdapters` — names of validated generated adapters saved to
271
+ `packages/tokens/adapters/` for reuse, so future syncs don't regenerate them.
272
+ - `lastRun` — ISO timestamp the sync command last ran.
273
+
274
+ ### `storybook`
275
+ - `initialized` / `chromatic` / `codeConnect` — setup flags. `codeConnect` stays
276
+ `false` when the user's Figma plan doesn't support it (the storybook skill
277
+ degrades gracefully).
278
+
279
+ ### `audit`
280
+ Populated by the `design-system-audit` skill (brownfield front door). `null` in
281
+ every field until that skill runs. Records the measured state of a pre-existing
282
+ system so the retrofit can be right-sized.
283
+ - `ranAt` — ISO timestamp the audit last ran.
284
+ - `codeSurface` — object of counts sizing the code-side retrofit, e.g.
285
+ `{ "scssColorVars": 692, "tailwindColorClasses": 230, "jsColorsUsages": 74,
286
+ "rawHexRgba": 143, "svgFills": 430 }`. Keys vary by what the repo actually uses.
287
+ - `figmaInventory` — object snapshot of the existing Figma file from explicit
288
+ per-class reads, e.g. `{ "variables": 0, "bindings": 0, "textStyles": 0,
289
+ "effectStyles": 0, "modes": [] }`. Each count comes from a verified read, never
290
+ an assumption (see `${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md`).
291
+ - `percentSemantic` — integer 0–100: how much of the existing system is already
292
+ semantic. The single number that decides rename+cleanup vs. rewrite — see
293
+ `${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md` (safe sequence, `audit`
294
+ phase) for how it's used.
295
+
296
+ ### `tokenCrosswalk`
297
+ Populated by the `token-crosswalk-builder` skill. Points at the backbone artifact
298
+ that maps new token ↔ old Figma token ↔ code identifier.
299
+ - `path` — repo-relative path to the crosswalk file (e.g. `"tokens/crosswalk.json"`),
300
+ or `null` if not yet built. The manifest stores the pointer, not the map.
301
+ - `statusCounts` — object counting crosswalk rows by `status`, e.g.
302
+ `{ "aligned": 12, "renamed": 151, "driftFix": 2, "added": 42, "mappedNearest": 3 }`.
303
+ - `validatorPassing` — `true`/`false`/`null`: whether the last crosswalk validator
304
+ run passed (`resolved value == new value` for every row).
305
+
306
+ ### `retrofit`
307
+ Populated by the `retrofit-planner` orchestrator. Tracks where a multi-phase
308
+ retrofit stands so a later session can resume.
309
+ - `phase` — one of `"audit"`, `"refine"`, `"rebind"`, `"sync"`, `"baseline"`,
310
+ `"code"`, `"cleanup"`, `"done"`, or `null` (no retrofit in progress). Phases run
311
+ in that order; see the safe sequence in
312
+ `${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md`.
313
+ - `startedAt` / `completedAt` — ISO timestamps bounding the retrofit.
314
+ - `journalScaffolded` — whether the `docs/design-system/` decision journal has been
315
+ created for this retrofit (offered default-on by `retrofit-planner`).
316
+
317
+ ### `completedSkills`
318
+ - Append-only list of skill identifiers that have run to completion at least
319
+ once. Useful for the `/design-system-status` command and for friendly
320
+ "you've already done X" messaging. Never used as the *sole* gate — always
321
+ pair with the specific field (e.g. check `tokens.primitivesBuilt`, not just
322
+ presence in this list).
323
+
324
+ ## Rules for skills touching the manifest
325
+
326
+ 1. **Read first, create if absent.** If the file doesn't exist, create it with
327
+ the defaults above. Never assume it exists.
328
+ 2. **Migrate forward on `schemaVersion` mismatch.** If you read a lower
329
+ `schemaVersion` than you expect, add missing fields with defaults and bump
330
+ the version. Never delete fields you don't recognize (forward-compat).
331
+ 3. **Only write the fields you own.** The token builder doesn't touch
332
+ `storybook`; the storybook skill doesn't touch `tokens.primitivesBuilt`.
333
+ 4. **The manifest is human-readable on purpose.** A designer should be able to
334
+ open it (or run `/design-system-status`) and understand the state of their
335
+ system. Keep it tidy; don't dump opaque blobs into it.
336
+ 5. **Never store secrets.** No Figma tokens, no Chromatic tokens, no
337
+ credentials of any kind. The manifest is committed to the repo once
338
+ `workspace.stage` advances — treat it as public.
339
+ 6. **`workspace.origin` is immutable after intake.** Written once by
340
+ `figma-environment-setup` Step 0 and must not be overwritten by any downstream
341
+ skill. Skills read it to adapt behavior — they do not modify it.