@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.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/adapters/codex/AGENTS.md +29 -0
- package/adapters/codex/codex-mcp.toml +4 -0
- package/adapters/codex/prompts/component-builder.md +272 -0
- package/adapters/codex/prompts/component-pipeline.md +81 -0
- package/adapters/codex/prompts/design-system-audit.md +122 -0
- package/adapters/codex/prompts/design-system-status.md +41 -0
- package/adapters/codex/prompts/figma-environment-setup.md +456 -0
- package/adapters/codex/prompts/icon-system-builder.md +268 -0
- package/adapters/codex/prompts/new-component.md +13 -0
- package/adapters/codex/prompts/repository-builder.md +163 -0
- package/adapters/codex/prompts/retrofit-planner.md +136 -0
- package/adapters/codex/prompts/start.md +22 -0
- package/adapters/codex/prompts/storybook-chromatic-builder.md +261 -0
- package/adapters/codex/prompts/sync-figma-tokens.md +22 -0
- package/adapters/codex/prompts/token-builder.md +361 -0
- package/adapters/codex/prompts/token-crosswalk-builder.md +120 -0
- package/adapters/codex/prompts/token-sheet-builder.md +162 -0
- package/adapters/codex/prompts/token-sync-layer.md +232 -0
- package/adapters/cursor/.cursor/commands/design-system-status.md +41 -0
- package/adapters/cursor/.cursor/commands/new-component.md +13 -0
- package/adapters/cursor/.cursor/commands/start.md +22 -0
- package/adapters/cursor/.cursor/commands/sync-figma-tokens.md +22 -0
- package/adapters/cursor/.cursor/mcp.json +14 -0
- package/adapters/cursor/.cursor/rules/component-builder.mdc +276 -0
- package/adapters/cursor/.cursor/rules/component-pipeline.mdc +85 -0
- package/adapters/cursor/.cursor/rules/design-system-audit.mdc +126 -0
- package/adapters/cursor/.cursor/rules/figma-environment-setup.mdc +460 -0
- package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +272 -0
- package/adapters/cursor/.cursor/rules/repository-builder.mdc +167 -0
- package/adapters/cursor/.cursor/rules/retrofit-planner.mdc +140 -0
- package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +265 -0
- package/adapters/cursor/.cursor/rules/token-builder.mdc +365 -0
- package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +124 -0
- package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +166 -0
- package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +236 -0
- package/adapters/generic/AGENTS.md +46 -0
- package/adapters/generic/commands/design-system-status.md +41 -0
- package/adapters/generic/commands/new-component.md +13 -0
- package/adapters/generic/commands/start.md +22 -0
- package/adapters/generic/commands/sync-figma-tokens.md +22 -0
- package/adapters/generic/skills/component-builder/SKILL.md +272 -0
- package/adapters/generic/skills/component-pipeline/SKILL.md +81 -0
- package/adapters/generic/skills/design-system-audit/SKILL.md +122 -0
- package/adapters/generic/skills/figma-environment-setup/SKILL.md +456 -0
- package/adapters/generic/skills/icon-system-builder/SKILL.md +268 -0
- package/adapters/generic/skills/repository-builder/SKILL.md +163 -0
- package/adapters/generic/skills/retrofit-planner/SKILL.md +136 -0
- package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +261 -0
- package/adapters/generic/skills/token-builder/SKILL.md +361 -0
- package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +120 -0
- package/adapters/generic/skills/token-sheet-builder/SKILL.md +162 -0
- package/adapters/generic/skills/token-sync-layer/SKILL.md +232 -0
- package/package.json +29 -0
- package/references/brainstorm-before-build.md +139 -0
- package/references/brownfield-retrofit.md +93 -0
- package/references/coding-level.md +70 -0
- package/references/crosswalk-schema.md +113 -0
- package/references/figma-component-standards.md +532 -0
- package/references/figma-publishing.md +125 -0
- package/references/figma-scripting.md +278 -0
- package/references/manifest-schema.md +341 -0
- package/references/scaling-up-handoff.md +61 -0
- package/references/sync-adapters.md +141 -0
- package/scripts/README.md +78 -0
- package/scripts/adapters/emit-codex.mjs +58 -0
- package/scripts/adapters/emit-cursor.mjs +26 -0
- package/scripts/adapters/emit-generic.mjs +41 -0
- package/scripts/adapters/generate.mjs +73 -0
- package/scripts/adapters/read-sources.mjs +54 -0
- package/scripts/adapters/translate.mjs +51 -0
- package/scripts/build-reverse-index.mjs +56 -0
- package/scripts/crosswalk.schema.json +33 -0
- package/scripts/grep-color-usage.mjs +143 -0
- package/scripts/guard-token-removal.mjs +95 -0
- package/scripts/install.mjs +143 -0
- package/scripts/lib/crosswalk.mjs +91 -0
- 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.
|