canicode 0.9.1 → 0.10.1

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.
@@ -0,0 +1,413 @@
1
+ ---
2
+ name: canicode-roundtrip
3
+ description: Analyze Figma design, fix gotchas via Plugin API, re-analyze, then implement — true design-to-code roundtrip
4
+ disable-model-invocation: false
5
+ ---
6
+
7
+ # CanICode Roundtrip — True Design-to-Code Roundtrip
8
+
9
+ Orchestrate the full design-to-code roundtrip: analyze a Figma design for readiness, collect gotcha answers for problem areas, **apply fixes directly to the Figma design** via `use_figma`, re-analyze to verify the design improved, then generate code. The design itself gets better — the next analysis passes without gotchas.
10
+
11
+ ## Prerequisites
12
+
13
+ - **Figma MCP server** installed (provides `get_design_context`, `get_screenshot`, `use_figma`, and other Figma tools) — REQUIRED, there is no CLI fallback for `use_figma`
14
+ - **canicode MCP server** (preferred): `claude mcp add canicode -e FIGMA_TOKEN=figd_xxx -- npx -y -p canicode canicode-mcp`
15
+ - **Without canicode MCP** (fallback): Steps 1 (analyze) and 3 (gotcha-survey) shell out to `npx canicode <command> --json` — same JSON shape as the MCP tools. Step 4 (apply to Figma) still requires Figma MCP `use_figma`.
16
+ - **FIGMA_TOKEN** configured for live Figma URLs
17
+ - **Figma Full seat + file edit permission** (required for `use_figma` to modify the design)
18
+
19
+ ## Workflow
20
+
21
+ ### Step 0: Verify Figma MCP tools are loaded
22
+
23
+ Before Step 1, verify that `use_figma` is callable in **this** session — not merely listed in `.mcp.json`. Newly registered MCP servers (e.g. via `claude mcp add -s project -t http figma https://mcp.figma.com/mcp`) require a Claude Code restart to load their tools; reading `.mcp.json` is not a substitute for checking the live tool list you have access to right now.
24
+
25
+ If `use_figma` is unavailable in the current session, **Do NOT proceed to Step 1**. Steps 1 (analyze) and 3 (gotcha-survey) spend real Figma API calls and 5–15 minutes of human survey time before Step 4 would otherwise discover `use_figma` is missing. Halt immediately and tell the user:
26
+
27
+ 1. Confirm `.mcp.json` registers the Figma MCP entry (e.g. `figma` under `mcpServers`).
28
+ 2. Restart Claude Code so the newly registered tools load.
29
+ 3. Re-invoke `/canicode-roundtrip <url>`.
30
+
31
+ See the Edge Case **No Figma MCP server** below for the one-way fallback when Figma MCP genuinely cannot be installed — the precheck above is for the common "installed but not restarted" case, not a replacement for that fallback.
32
+
33
+ ### Step 1: Analyze the design
34
+
35
+ If the `analyze` MCP tool is available, call it with the user's Figma URL:
36
+
37
+ ```
38
+ analyze({ input: "<figma-url>" })
39
+ ```
40
+
41
+ **Without canicode MCP** — shell out to the CLI (same JSON shape):
42
+
43
+ ```bash
44
+ npx canicode analyze "<figma-url>" --json
45
+ ```
46
+
47
+ The response includes:
48
+ - `scores.overall.grade`: design grade (S, A+, A, B+, B, C+, C, D, F)
49
+ - `isReadyForCodeGen`: boolean gate for gotcha skip
50
+ - `issues`: array of design issues found
51
+ - `summary`: human-readable analysis summary
52
+
53
+ Show the user a brief summary:
54
+
55
+ ```
56
+ Design grade: **{grade}** ({percentage}%) — {issueCount} issues found.
57
+ ```
58
+
59
+ ### Step 2: Gate — check if gotchas are needed
60
+
61
+ If `isReadyForCodeGen` is `true` (grade S, A+, or A):
62
+ - Tell the user: "This design scored **{grade}** — ready for code generation with no gotchas needed."
63
+ - Skip directly to **Step 6**.
64
+
65
+ If `isReadyForCodeGen` is `false` (grade B+ or below):
66
+ - Tell the user: "This design scored **{grade}** — running gotcha survey to identify implementation pitfalls."
67
+ - Proceed to **Step 3**.
68
+
69
+ ### Step 3: Run gotcha survey and collect answers
70
+
71
+ If the `gotcha-survey` MCP tool is available, call it:
72
+
73
+ ```
74
+ gotcha-survey({ input: "<figma-url>" })
75
+ ```
76
+
77
+ **Without canicode MCP** — shell out to the CLI (same JSON shape):
78
+
79
+ ```bash
80
+ npx canicode gotcha-survey "<figma-url>" --json
81
+ ```
82
+
83
+ If `questions` is empty, skip to **Step 6**.
84
+
85
+ For each question in the `questions` array, present it to the user one at a time.
86
+
87
+ Build the message from the question fields. **If `question.instanceContext` is present**, prepend one line before the question body:
88
+
89
+ ```
90
+ _Instance note: This layer is inside an instance. Layout and size fixes may need to be applied on source component **{sourceComponentName or sourceComponentId or "unknown"}** (definition node `sourceNodeId`) and propagate to all instances — you will be asked to confirm before any definition-level write._
91
+ ```
92
+
93
+ Then the standard block:
94
+
95
+ ```
96
+ **[{severity}] {ruleId}** — node: {nodeName}
97
+
98
+ {question}
99
+
100
+ > Hint: {hint}
101
+ > Example: {example}
102
+ ```
103
+
104
+ Wait for the user's answer before moving to the next question. The user may:
105
+ - Answer the question directly
106
+ - Say "skip" to skip a question
107
+ - Say "n/a" if the question is not applicable
108
+
109
+ After all questions are answered, **upsert this design's gotcha section** into `.claude/skills/canicode-gotchas/SKILL.md` in the user's project. Read the existing file, then either replace the section whose `Design key` matches this run (same Figma URL → fileKey+nodeId) or append a new numbered section under `# Collected Gotchas`. Never modify anything above the `# Collected Gotchas` heading — the region above it (frontmatter + workflow prose) is the skill loader contract installed by `canicode init`. See the `/canicode-gotchas` skill's "Upsert the gotcha section" step (Step 4) for the exact section format and matching rule.
110
+
111
+ Then proceed to **Step 4** to apply answers to the Figma design.
112
+
113
+ ### Step 4: Apply gotcha answers to Figma design
114
+
115
+ Extract the `fileKey` from the Figma URL (format: `figma.com/design/:fileKey/...`).
116
+
117
+ For each answered gotcha (skip questions answered with "skip" or "n/a"), branch on the pre-computed `question.applyStrategy`. The routing table, target properties, and instance-child resolution are resolved server-side by `canicode` — do NOT re-derive them from the rule id.
118
+
119
+ Use the **`nodeId` from the answered question**. When `question.isInstanceChild` is `true`, treat layout and size-constraint changes as **high impact**: applying them on the source definition affects **every instance** of that component in the file. Ask for explicit user confirmation before writing to the definition node.
120
+
121
+ #### Input shape from canicode
122
+
123
+ Every gotcha-survey question (and every entry in `analyzeResult.issues[]`) carries these pre-computed fields:
124
+
125
+ | Field | Type | Meaning |
126
+ |-------|------|---------|
127
+ | `applyStrategy` | `"property-mod"` \| `"structural-mod"` \| `"annotation"` \| `"auto-fix"` | Which strategy branch to enter (A/B/C/D). |
128
+ | `targetProperty` | `string` \| `string[]` \| (absent) | Figma Plugin-API property to write. Array when multiple properties move together (e.g. `no-auto-layout` → `["layoutMode", "itemSpacing"]`). Absent for structural/annotation rules. |
129
+ | `annotationProperties` | `Array<{ type: string }>` \| (absent) | Pre-computed Dev Mode annotation `properties` hint for the ruleId (+ subType). Pass directly to `upsertCanicodeAnnotation`. Absent when the rule has no mapping. See the annotation matrix below for the enum + node-type filtering (enforced by the helper's retry path). |
130
+ | `suggestedName` | `string` \| (absent) | Naming rules only — pre-capitalized value to write to `node.name` (e.g. `"Hover"`). |
131
+ | `isInstanceChild` | `boolean` | Whether the `nodeId` targets a node inside an INSTANCE subtree. |
132
+ | `sourceChildId` | `string` \| (absent) | Definition node id inside the source component. Use directly with `figma.getNodeByIdAsync`. |
133
+ | `instanceContext` | object \| (absent) | Survey questions only. `{ parentInstanceNodeId, sourceNodeId, sourceComponentId?, sourceComponentName? }` for the Step 3 user-facing note. |
134
+
135
+ #### Instance-child property overridability (Plugin API)
136
+
137
+ Most production nodes sit under `INSTANCE` subtrees. `canicode` flags these via `question.isInstanceChild` and, when resolvable, surfaces the definition node id as `question.sourceChildId` plus extra metadata on `question.instanceContext`. You do not need to parse node ids.
138
+
139
+ Matrix below is confirmed by Experiment 08 ([#290](https://github.com/let-sunny/canicode/issues/290)) probes on shallow + deep instance-child FRAMEs in the Simple Design System fixture. `✅` = raw-value write accepted, `❌` = throws *"cannot be overridden in an instance"*, `⚠️` = no error but value silently unchanged (must detect with before/after compare).
140
+
141
+ | Property | Raw-value write on instance child | Variable binding | Notes |
142
+ |----------|----------------------------------|------------------|-------|
143
+ | `node.name` | ✅ | — | Prefer scene node first. |
144
+ | `annotations` | ✅ | — | Good fallback when another property cannot be set. |
145
+ | `itemSpacing`, `paddingTop/Right/Bottom/Left` | ✅ | ✅ | |
146
+ | `primaryAxisAlignItems`, `counterAxisAlignItems`, `layoutAlign` | ✅ | — | |
147
+ | `cornerRadius`, `opacity` | ✅ | ✅ | |
148
+ | `fills`, `strokes` (raw color) | ✅ | ✅ via `setBoundVariableForPaint(paint, "color", v)` | |
149
+ | `layoutSizingHorizontal` / `layoutSizingVertical` | ✅ | — | |
150
+ | `layoutMode` | ⚠️ on some nodes | — | Some instance children silently ignore the write (no throw, no change). |
151
+ | **`minWidth`, `maxWidth`, `minHeight`, `maxHeight`** | ❌ on many nodes | **✅** | **Variable binding bypasses the override restriction** — prefer binding when the answer names a token. Raw values route to the definition node after confirmation. |
152
+ | `fontSize`, `lineHeight`, `letterSpacing`, `paragraphSpacing` (TEXT) | ✅ | ✅ | |
153
+ | `characters` (TEXT) | ✅ | ✅ STRING variable | |
154
+
155
+ #### Annotation `properties` matrix
156
+
157
+ Experiment 09 ([#290 follow-up](https://github.com/let-sunny/canicode/issues/290)) re-measured the full 33-value enum on a scene FRAME (`3077:9894`) and scene TEXT (`3077:9963`) in the Simple Design System fixture. The key finding: **the gate is node-type, not scene-vs-instance**. FRAMEs reject `fills`/`cornerRadius`/`opacity`/`maxWidth`/`effects` regardless of context. Instance children additionally lose `minWidth`/`minHeight`/`alignItems` on FRAMEs — these are instance-override restrictions layered on top.
158
+
159
+ Each row below covers the full 33-value enum (`width`, `height`, `maxWidth`, `minWidth`, `maxHeight`, `minHeight`, `fills`, `strokes`, `effects`, `strokeWeight`, `cornerRadius`, `textStyleId`, `textAlignHorizontal`, `fontFamily`, `fontStyle`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `itemSpacing`, `padding`, `layoutMode`, `alignItems`, `opacity`, `mainComponent`, plus 8 grid props `gridRowGap`/`gridColumnGap`/`gridRowCount`/`gridColumnCount`/`gridRowAnchorIndex`/`gridColumnAnchorIndex`/`gridRowSpan`/`gridColumnSpan`):
160
+
161
+ | Node type | Accepted (scene) | Additionally rejected on instance child | Rejected in all contexts |
162
+ |-----------|------------------|-----------------------------------------|--------------------------|
163
+ | FRAME | `width`, `height`, `minWidth`, `minHeight`, `itemSpacing`, `padding`, `layoutMode`, `alignItems` | `minWidth`, `minHeight`, `alignItems` | `maxWidth`, `maxHeight`, `fills`, `strokes`, `effects`, `strokeWeight`, `cornerRadius`, `opacity`, `mainComponent`, all 8 text props, all 8 grid props |
164
+ | TEXT | `width`, `height`, `fills`, `textStyleId`, `fontFamily`, `fontStyle`, `fontSize`, `fontWeight`, `lineHeight` | not re-measured — Experiment 08 only probed `strokes`/`opacity`/`cornerRadius`/`effects`/`layoutMode`/`itemSpacing`/`padding` on instance-child TEXT, and all were rejected there too | `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `strokes`, `effects`, `strokeWeight`, `cornerRadius`, `opacity`, `textAlignHorizontal`, `letterSpacing`, `itemSpacing`, `padding`, `layoutMode`, `alignItems`, `mainComponent`, all 8 grid props |
165
+
166
+ `upsertCanicodeAnnotation` wraps the write in `try/catch`: if `properties` fails node-type validation it retries without them, so the markdown body always survives. You can pass `properties` speculatively.
167
+
168
+ > **Note:** This policy has shipped per ADR-012 (resolves [#295](https://github.com/let-sunny/canicode/issues/295)): **scene write by default; definition write is opt-in** behind `allowDefinitionWrite`. The bundled helper and the prose below match — reading one without the other is safe.
169
+
170
+ **Write policy (ordered tiers):**
171
+
172
+ The helper walks the tiers in order; variable binding is an alternative writeFn shape available at tiers 1 and 2 that bypasses the instance-child override gate (Experiment 08) — it is *not* a separate ordering position between the tiers.
173
+
174
+ 1. **Scene (instance) node** — `await figma.getNodeByIdAsync(question.nodeId)` and apply the write inside `try/catch`. If the answer names a design-system token (`{ variable: "name" }`), the helper calls `setBoundVariable` / `setBoundVariableForPaint` first and that binding bypasses the override gate — otherwise it performs a raw-value write. Success → done (local change only). Mark result with ✅.
175
+ 2. **Definition (source) node — opt-in only** — Runs only when the orchestrator passes `allowDefinitionWrite: true` on the helper context (after a batch-level confirmation naming the source component AND the propagation set). When the flag is off (the ADR-012 default), a recognized instance-override failure (override-error or silent-ignore) short-circuits here and routes directly to tier 3 — the definition node is never touched. When the flag is on, the helper loads `question.sourceChildId` (or walks `getMainComponentAsync()` if needed) and writes using the same bind-if-token-else-raw shape as tier 1; changes propagate to **every non-overridden instance** in the file (Experiment 10). Mark result with 🌐.
176
+ 3. **Annotation fallback — default path** — Under the ADR-012 default this is where override-errors and silent-ignores land: the helper annotates the **scene** node with markdown that names the source component as the recommended write target and notes that the instance kept its current value to avoid unintended fan-out. When `allowDefinitionWrite` is on, this tier also catches any definition-tier throw (e.g. Experiment 10 external-library read-only case, `mainComponent.remote === true` / *"Cannot write to internal and read-only node"*, and the `mainComponent === null` branch where `getMainComponentAsync()` resolves with no definition to name — see Experiment 11 / ADR-011). Either way, mark result with 📝.
177
+
178
+ **Confirmation is a batch-level concern — and only needed when opting in.** A `use_figma` call runs one JavaScript batch and cannot pause mid-batch for user input. Under the ADR-012 default (`allowDefinitionWrite: false`), no propagation happens, so no confirmation is required — override-errors annotate and move on. The orchestrator sets `allowDefinitionWrite: true` only after enumerating the likely propagation set to the user up-front and collecting **one confirmation for the whole batch** that names the source component(s) and the affected instance set. When describing impact, note that the write reaches every **non-overridden** instance — any instance with a local override for the same property keeps its override. The helper below never prompts — it assumes that if the flag is on, confirmation already happened.
179
+
180
+ **Shared helpers (bundled)** — the deterministic helpers live in TypeScript at `src/core/roundtrip/*.ts` and are bundled to a single IIFE at `.claude/skills/canicode-roundtrip/helpers.js`. `use_figma` only accepts a self-contained JS string, so the source of truth is TypeScript (with vitest coverage) and the bundle is the delivery artifact.
181
+
182
+ **Usage in a roundtrip session:**
183
+
184
+ 1. Read `.claude/skills/canicode-roundtrip/helpers.js` once at the start of Step 4.
185
+ 2. Prepend its contents verbatim at the top of every `use_figma` batch body — it registers a single global `CanICodeRoundtrip`.
186
+ 3. Reference exposed globals as `CanICodeRoundtrip.*`:
187
+ - `stripAnnotations(annotations)` — normalizes the D1 label/labelMarkdown mutex on readback.
188
+ - `ensureCanicodeCategories()` — returns `{ gotcha, autoFix, fallback }` category id map (D4); idempotent, safe to call at the top of every batch.
189
+ - `upsertCanicodeAnnotation(node, { ruleId, markdown, categoryId, properties })` — idempotent annotation upsert. Handles D1 mutex, D2 in-place replace by ruleId prefix, and the D3 `properties` node-type retry.
190
+ - `applyWithInstanceFallback(question, writeFn, { categories, allowDefinitionWrite, telemetry })` — three-tier write policy with silent-ignore detection. `allowDefinitionWrite` defaults to `false` per ADR-012 — override-errors and silent-ignores annotate the scene naming the source component instead of writing the definition. Set `true` only after a batch-level confirmation. `telemetry` is an optional `(event, props) => void` callback fired when a definition write is skipped (wiring point for future Node-side opt-in usage data). The `writeFn` may return `false` to signal "write accepted but value unchanged" so the helper can route to the next tier.
191
+ - `applyPropertyMod(question, answerValue, { categories, allowDefinitionWrite, telemetry })` — Strategy A entry point. Branches on `targetProperty` (single vs array) and answer shape (scalar, per-property object, `{ variable: "name" }` binding). Uses `setBoundVariableForPaint` for `fills` / `strokes` and `setBoundVariable` for scalar fields. Passes the full context through to `applyWithInstanceFallback`.
192
+ - `resolveVariableByName(name)` — local-variable exact-name lookup; returns `null` for remote library variables not imported into this file.
193
+
194
+ Keep each `writeFn` small so a throw does not abort unrelated writes. Experiment 08 findings informed every branch in the bundled helpers, and the batch-level confirmation contract still applies *when opting in*: if the orchestrator passes `allowDefinitionWrite: true`, it must have already collected one confirmation covering every potential definition write in the batch. Under the default, no confirmation is needed — the helper annotates the scene instead of propagating.
195
+
196
+ Wrap every property write in `CanICodeRoundtrip.applyWithInstanceFallback(question, async (target) => { ... }, { categories })` so failed or silently-ignored instance overrides route to the scene annotation (or, when the user has opted in, to the definition tier) instead of silently aborting the batch.
197
+
198
+ #### Strategy A: Property Modification — apply directly
199
+
200
+ Rules with `applyStrategy === "property-mod"`. Call the bundled helper — it branches on `question.targetProperty` (single vs array) and on each value type (scalar, multi-property object, `{ variable: "token-name" }` binding) automatically. Paint properties (`fills`, `strokes`) are bound with `setBoundVariableForPaint` per the Plugin API contract; scalar fields use `setBoundVariable`.
201
+
202
+ ```javascript
203
+ await CanICodeRoundtrip.applyPropertyMod(question, answerValue, { categories });
204
+ ```
205
+
206
+ Answer shape guide (LLM judgment — the user's answer is prose; parse accordingly):
207
+ - **`non-semantic-name`**: string — the new node name.
208
+ - **`irregular-spacing`**: number for gap (subType `gap`), or `{ paddingTop, paddingRight, paddingBottom, paddingLeft }` for padding.
209
+ - **`fixed-size-in-auto-layout`**: `"FILL"` \| `"HUG"` \| `"FIXED"` — applied to each axis listed in `targetProperty`.
210
+ - **`missing-size-constraint`**: partial `{ minWidth, maxWidth }` — include only the keys the answer supplied.
211
+ - **`no-auto-layout`**: `{ layoutMode, itemSpacing }`; optionally extend with padding/alignment from the answer.
212
+
213
+ **Variable binding** — whenever the answer names a design-system token (e.g. the user says the width should be `mobile-width`, the gap should be `space-m`, the color should be `Brand/Primary`), shape the value as `{ variable: "token-name" }` instead of a raw scalar. The helper calls `setBoundVariable` which **bypasses instance-child override restrictions**, so `minWidth`/`maxWidth`/color fields that raw writes cannot touch on an instance child will bind successfully. Mix shapes per-property — e.g. `{ minWidth: { variable: "mobile-width" }, maxWidth: 1440 }`.
214
+
215
+ The name must match **the variable's `name` field exactly** — including any slash path in the name (e.g. `"Brand/Primary"` matches only when the variable is literally named that way). Resolution is scoped to variables that `figma.variables.getLocalVariablesAsync()` returns: locally defined ones plus library variables that have already been imported into this file. If the token lives only in an unimported remote library, the binding step returns `null` and `applyPropertyMod` either falls through to a raw scalar (when the answer provided a `fallback` value) or records the miss — expose this as an annotation via the fallback category so the designer can import the variable and retry.
216
+
217
+ #### Strategy B: Structural Modification — confirm with user first
218
+
219
+ Rules with `applyStrategy === "structural-mod"`. Show the proposed change and **ask for user confirmation** before applying.
220
+
221
+ **`non-layout-container`** — Convert Group/Section to Auto Layout frame:
222
+ - Prompt: "I'll convert **{nodeName}** to an Auto Layout frame with {direction} layout and {spacing}px gap. Proceed?"
223
+ - If confirmed: `applyPropertyMod(question, { layoutMode: "VERTICAL", itemSpacing: 12 })`.
224
+
225
+ **`deep-nesting`** — Flatten intermediate wrappers or extract sub-component:
226
+ - Prompt: "I'll flatten **{nodeName}** by {description from answer}. This changes the layer hierarchy. Proceed?"
227
+ - Apply based on the specific answer (remove wrappers, convert padding, etc.).
228
+
229
+ **`missing-component`** — Convert frame to reusable component:
230
+ - Prompt: "I'll convert **{nodeName}** to a reusable component. Proceed?"
231
+ - If confirmed:
232
+ ```javascript
233
+ const scene = await figma.getNodeByIdAsync(question.nodeId);
234
+ if (scene && scene.type === "FRAME") {
235
+ figma.createComponentFromNode(scene);
236
+ }
237
+ ```
238
+
239
+ **`detached-instance`** — Reconnect to original component:
240
+ - Prompt: "I'll reconnect **{nodeName}** to its original component. Any overrides will be preserved. Proceed?"
241
+ - Requires finding the original component — if not identifiable, fall back to annotation.
242
+
243
+ If the user **declines** any structural modification, add an annotation instead (same as Strategy C).
244
+
245
+ #### Strategy C: Annotation — record on the design for designer reference
246
+
247
+ Rules with `applyStrategy === "annotation"` cannot be auto-fixed via Plugin API. Add the gotcha answer as a Figma annotation so designers see it in Dev Mode. Use the helper — it handles the D1 mutex, D2 in-place upsert, and D4 category assignment.
248
+
249
+ ```javascript
250
+ const scene = await figma.getNodeByIdAsync(question.nodeId);
251
+ CanICodeRoundtrip.upsertCanicodeAnnotation(scene, {
252
+ ruleId: question.ruleId,
253
+ markdown: `**Q:** ${question.question}\n**A:** ${answer}`,
254
+ categoryId: categories.gotcha,
255
+ // Optional: surface live property values in Dev Mode alongside the note.
256
+ // Only include types the node supports (FRAME vs TEXT — see matrix above).
257
+ properties: question.annotationProperties,
258
+ });
259
+ ```
260
+
261
+ Notes:
262
+ - `upsertCanicodeAnnotation` replaces an existing `**[canicode] <ruleId>**` entry on the same node instead of appending — reruns don't accumulate duplicates.
263
+ - `label` and `labelMarkdown` are mutually exclusive on write, but Figma returns both on readback. Never spread `scene.annotations` directly; always call `CanICodeRoundtrip.upsertCanicodeAnnotation` (or `CanICodeRoundtrip.stripAnnotations` if you truly need the normalized array).
264
+ - Prefer annotating the **scene** instance child so designers see the note where they work; mention in the markdown if the fix belongs on the source component but could not be applied (library/external).
265
+
266
+ #### Strategy D: Auto-fix lower-severity issues from analysis
267
+
268
+ The gotcha survey covers only blocking/risk severity. Lower-severity rules appear in `analyzeResult.issues[]` without a survey question. Each issue carries the same pre-computed fields (`applyStrategy`, `targetProperty`, `annotationProperties`, `suggestedName`, `isInstanceChild`, `sourceChildId`). Loop over them:
269
+
270
+ ```javascript
271
+ for (const issue of analyzeResult.issues) {
272
+ if (issue.applyStrategy !== "auto-fix") continue;
273
+
274
+ // Shape an ad-hoc question-like object so the same helpers apply.
275
+ const q = {
276
+ nodeId: issue.nodeId,
277
+ ruleId: issue.ruleId,
278
+ ...(issue.sourceChildId ? { sourceChildId: issue.sourceChildId } : {}),
279
+ };
280
+
281
+ if (issue.targetProperty === "name" && issue.suggestedName) {
282
+ // Naming rules — rename to the pre-computed suggestedName.
283
+ await CanICodeRoundtrip.applyWithInstanceFallback(q, async (target) => {
284
+ if (target) target.name = issue.suggestedName;
285
+ }, { categories });
286
+ } else {
287
+ // raw-value, missing-interaction-state, missing-prototype — designer judgment; annotate.
288
+ const scene = await figma.getNodeByIdAsync(issue.nodeId);
289
+ CanICodeRoundtrip.upsertCanicodeAnnotation(scene, {
290
+ ruleId: issue.ruleId,
291
+ markdown: issue.message,
292
+ categoryId: categories.autoFix,
293
+ // Optional: surface the live value for the affected property in Dev Mode.
294
+ properties: issue.annotationProperties,
295
+ });
296
+ }
297
+ }
298
+ ```
299
+
300
+ `suggestedName` is already capitalized for direct Plugin-API use (e.g. `"Hover"`, `"Default"`, `"Pressed"`). Do not transform it further.
301
+
302
+ #### Execution order
303
+
304
+ 0. **Initialize categories** — first batch calls `const categories = await CanICodeRoundtrip.ensureCanicodeCategories();` and keeps the result in scope for every subsequent call in the same script. (Or re-run ensure at the top of each `use_figma` batch — it is idempotent by label.)
305
+ 1. **Batch all property modifications** (Strategy A) into a single `use_figma` call for efficiency. Pass `{ categories }` to `applyWithInstanceFallback` so fallbacks land in the correct category.
306
+ 2. **Present structural modifications** (Strategy B) one by one, apply confirmed ones.
307
+ 3. **Batch all annotations** (Strategy C + declined structural mods) into a single `use_figma` call — use `categories.gotcha` for the category id.
308
+ 4. **Batch all auto-fixes and annotations for lower-severity issues** (Strategy D) — use `categories.autoFix` for annotated ones, `categories.fallback` is reserved for errors surfaced by `applyWithInstanceFallback` itself.
309
+
310
+ After applying, report what was done:
311
+
312
+ ```
313
+ Applied {N} changes to the Figma design:
314
+ - ✅ {nodeName}: renamed to "hero-section" (non-semantic-name) — scene/instance override
315
+ - 🌐 {nodeName}: minWidth applied on source definition (missing-size-constraint) — propagates to all instances
316
+ - ✅ {nodeName}: itemSpacing → 16px (irregular-spacing)
317
+ - 🔗 {nodeName}: minWidth bound to variable "mobile-width" (missing-size-constraint)
318
+ - ⏭️ {nodeName}: declined by user, added annotation (deep-nesting)
319
+ - 📝 {nodeName}: annotation added to canicode:gotcha (absolute-position-in-auto-layout)
320
+ - 🔧 {nodeName}: auto-fixed to "Hover" (non-standard-naming)
321
+ - 📝 {nodeName}: annotation added to canicode:auto-fix — raw color needs token binding (raw-value)
322
+ ```
323
+
324
+ ### Step 5: Re-analyze and report what the roundtrip addressed
325
+
326
+ Run `analyze` again on the same Figma URL:
327
+
328
+ ```
329
+ analyze({ input: "<figma-url>" })
330
+ ```
331
+
332
+ Under ADR-012's annotate-by-default policy, most instance-child gotchas route to 📝 annotations and do **not** move the numeric grade — so the headline for this step is the **issues-delta** (what the roundtrip captured), not a grade comparison. Grade is kept as a footnote so the Row 8 regression guardrail still applies.
333
+
334
+ **Tally inputs** — derive the counts from the data you already have:
335
+ - `X` (✅ resolved): count of ✅ + 🔧 + 🔗 markers from the Step 4 report block you just emitted (scene/instance-child writes, auto-fix renames, and variable bindings all successfully landed the value).
336
+ - `Y` (📝 annotated): count of 📝 markers from Step 4 — gotcha answers captured as Figma annotations for code-gen reference.
337
+ - `Z` (🌐 definition writes): count of 🌐 markers from Step 4 — only non-zero when the orchestrator opted in with `allowDefinitionWrite: true` (helper context option, not a CLI flag).
338
+ - `W` (⏭️ skipped): count of ⏭️ markers from Step 4 plus any Step 3 questions the user answered with `skip` or `n/a`.
339
+ - `V` (remaining): `issues.length` from the re-analyze response — unresolved gotchas plus non-actionable rules still flagged by the design.
340
+ - `N` (addressed) = `X + Y + Z + W`.
341
+
342
+ If Step 4 produced no report block (e.g. user skipped every question, or no gotcha survey ran), all four counts are zero — that is a legitimate outcome; report the breakdown with zeros rather than treating it as an error.
343
+
344
+ **All gotcha issues resolved** (`V == 0`, i.e. re-analyze surfaces no remaining issues — note this is independent of grade since ADR-012 annotations do not move the score):
345
+ - Tell the user (fill in the counts from the tally above):
346
+
347
+ ```
348
+ Roundtrip complete — N issues addressed:
349
+ ✅ X resolved (auto-fix or property write succeeded)
350
+ 📝 Y annotated on Figma (gotcha answers captured for code-gen)
351
+ 🌐 Z definition writes propagated (only when allowDefinitionWrite: true)
352
+ ⏭️ W skipped (user declined or "skip")
353
+
354
+ V issues remaining (unresolved gotchas + non-actionable rules)
355
+
356
+ Grade: {oldGrade} → {newGrade}. Ready for code generation.
357
+ ```
358
+ - Clean up canicode annotations: remove annotations with `[canicode]` prefix from fixed nodes via `use_figma`. Apply `stripAnnotations` to avoid the D1 mutex:
359
+ ```javascript
360
+ const nodeIds = ["id1", "id2"]; // nodes that now pass
361
+ for (const id of nodeIds) {
362
+ const node = await figma.getNodeByIdAsync(id);
363
+ if (node && "annotations" in node) {
364
+ node.annotations = CanICodeRoundtrip.stripAnnotations(node.annotations).filter(
365
+ a => !a.labelMarkdown?.startsWith("**[canicode]")
366
+ );
367
+ }
368
+ }
369
+ ```
370
+ - Proceed to **Step 6**.
371
+
372
+ **Some issues remain** (`V > 0`):
373
+ - Show the same breakdown and ask whether to proceed:
374
+
375
+ ```
376
+ Roundtrip complete — N issues addressed:
377
+ ✅ X resolved (auto-fix or property write succeeded)
378
+ 📝 Y annotated on Figma (gotcha answers captured for code-gen)
379
+ 🌐 Z definition writes propagated (only when allowDefinitionWrite: true)
380
+ ⏭️ W skipped (user declined or "skip")
381
+
382
+ V issues remaining (unresolved gotchas + non-actionable rules)
383
+
384
+ Grade: {oldGrade} → {newGrade}. Proceed to code generation with remaining context?
385
+ ```
386
+ - If yes → proceed to **Step 6** with remaining gotcha context.
387
+ - If no → stop and let the user address remaining issues manually.
388
+
389
+ ### Step 6: Implement with Figma MCP
390
+
391
+ Follow the **figma-implement-design** skill workflow to generate code from the Figma design.
392
+
393
+ **If annotations or unresolved gotchas remain from Step 5**, provide them as additional context when implementing:
394
+
395
+ - Gotchas with severity **blocking** MUST be addressed — the design cannot be implemented correctly without this information
396
+ - Gotchas with severity **risk** SHOULD be addressed — they indicate potential issues that will surface later
397
+ - Reference the specific node IDs from gotcha answers to locate the affected elements in the design
398
+ - Pass the Figma URL (or `designKey` = `<fileKey>#<nodeId>`) to `figma-implement-design` so it can grep the matching `## #NNN — …` section in `.claude/skills/canicode-gotchas/SKILL.md` instead of reading the whole accumulated file
399
+
400
+ **If all issues were resolved in Steps 4-5**, no additional gotcha context is needed — the design speaks for itself.
401
+
402
+ ## Edge Cases
403
+
404
+ - **No canicode MCP server**: Fall back to `npx canicode analyze --json` and `npx canicode gotcha-survey --json` — both CLI commands return the same shape as the MCP tools. The Figma MCP is still required for `use_figma` in Step 4; there is no CLI fallback for Figma design edits.
405
+ - **No Figma MCP server**: If `get_design_context` or `use_figma` is not found, tell the user to set up the Figma MCP server. Without it, the apply and code generation phases cannot proceed.
406
+ - **No edit permission**: If `use_figma` fails with a permission error, tell the user they need Full seat + file edit permission. Fall back to the one-way flow: skip Steps 4-5 and proceed directly to Step 6 with gotcha answers as code generation context.
407
+ - **User wants analysis only**: Suggest using `/canicode` instead — it runs analysis without the code generation phase.
408
+ - **User wants gotcha survey only**: Suggest using `/canicode-gotchas` instead — it runs the survey and saves answers as a persistent skill file.
409
+ - **Partial gotcha answers**: Apply only the answered questions. Skipped/n/a questions are neither applied nor annotated.
410
+ - **use_figma call fails for a node**: Report the error for that specific node, continue with other nodes. Failed property modifications become annotations so the context is not lost.
411
+ - **Re-analyze shows new issues**: Only address issues from the original gotcha survey. New issues may appear due to structural changes — report them but do not re-enter the gotcha loop.
412
+ - **Very large design (many gotchas)**: The gotcha survey already deduplicates sibling nodes and filters to blocking/risk severity only. If there are still many questions, ask the user if they want to focus on blocking issues only.
413
+ - **External library components**: Applies only when the orchestrator has set `allowDefinitionWrite: true`. Experiment 10's observed case is `getMainComponentAsync()` resolving with `mainComponent.remote === true` — writes then throw *"Cannot write to internal and read-only node"*. The `mainComponent === null` case is documented in the Plugin API but was not reproduced live in Experiment 10; Experiment 11 (#309) unit-test-covers the helper's routing for that branch (override-error + no `sourceChildId` → annotate with `could not apply automatically:` markdown — see ADR-011 Verification), so the code path is regression-locked while live Figma reproduction remains a manual fixture-seeding follow-up. Under the default (`allowDefinitionWrite: false`), the definition write never fires and this throw cannot surface.