canicode 0.10.5 → 0.11.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,397 @@
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
+ **Channel contrast:** **`canicode-gotchas`** stores answers in **local** `.claude/skills/canicode-gotchas/SKILL.md` only (memo — no Figma write). **`canicode-roundtrip`** (**this skill**) writes to the **Figma canvas** via Plugin API (`use_figma`). If you only need Q&A persistence, use gotchas; if you need annotations and fixes on the file, use roundtrip.
10
+
11
+ 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 gotchas were captured, then generate code. Success means **gotchas answered and carried into annotations / writes** — not a numeric grade bump (analyze still reports grade for continuity; roundtrip success is lint-first).
12
+
13
+ ## Prerequisites
14
+
15
+ - **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`. Register it with your host (e.g. Claude Code: `claude mcp add -s project -t http figma https://mcp.figma.com/mcp`; Cursor: add the Figma MCP entry per host docs / project `.mcp.json`).
16
+ - **canicode MCP** (preferred): **Claude Code:** `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp` — long-form flags only; short `-y -p` collides with `claude mcp add`'s parser (#366); do **not** pass `-e FIGMA_TOKEN=…` here (#364). **Cursor / other hosts:** add `canicode-mcp` to MCP config — see [Customization guide](https://github.com/let-sunny/canicode/blob/main/docs/CUSTOMIZATION.md#cursor-mcp-canicode). The server reads `FIGMA_TOKEN` from `~/.canicode/config.json` or the environment.
17
+ - **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`.
18
+ - **FIGMA_TOKEN** configured for live Figma URLs
19
+ - **Figma Full seat + file edit permission** (required for `use_figma` to modify the design)
20
+
21
+ ## Workflow
22
+
23
+ ### Step 0: Verify Figma MCP tools are loaded
24
+
25
+ Before Step 1, verify that `use_figma` is callable in **this** session — not merely listed in `.mcp.json`. Newly registered MCP servers require a **host restart or MCP reload** so tools appear (e.g. Claude Code: restart after `claude mcp add …`; Cursor: restart Cursor or reload MCP after editing `.cursor/mcp.json`). Reading `.mcp.json` is not a substitute for checking the live tool list you have access to right now.
26
+
27
+ 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:
28
+
29
+ 1. Confirm `.mcp.json` (project or user) registers the Figma MCP entry (e.g. `figma` under `mcpServers`).
30
+ 2. Restart the IDE / agent host (or reload MCP) so the newly registered tools load.
31
+ 3. Re-invoke the roundtrip (Claude Code slash command `/canicode-roundtrip`, or Cursor: @ **canicode-roundtrip** with the Figma URL).
32
+
33
+ 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.
34
+
35
+ **canicode MCP (same cold-session pattern):** If `analyze` / `gotcha-survey` MCP tools are missing but `.mcp.json` lists canicode, you are on the `npx canicode …` fallback. Tell the user to restart the host or reload MCP after `claude mcp add canicode …` (or the Cursor equivalent) so the canicode tools appear — same communication fix as #433; the CLI path is not an error.
36
+
37
+ ### Step 1: Analyze the design
38
+
39
+ If the `analyze` MCP tool is available, call it with the user's Figma URL:
40
+
41
+ ```
42
+ analyze({ input: "<figma-url>" })
43
+ ```
44
+
45
+ **Without canicode MCP** — shell out to the CLI (same JSON shape):
46
+
47
+ ```bash
48
+ npx canicode analyze "<figma-url>" --json
49
+ ```
50
+
51
+ The response includes:
52
+ - `scores.overall.grade`: design grade (S, A+, A, B+, B, C+, C, D, F)
53
+ - `isReadyForCodeGen`: boolean gate for gotcha skip
54
+ - `issues`: array of design issues found
55
+ - `summary`: human-readable analysis summary
56
+
57
+ Show the user a brief summary:
58
+
59
+ ```
60
+ Design grade: **{grade}** ({percentage}%) — {issueCount} issues found.
61
+ ```
62
+
63
+ ### Step 2: Gate — check if gotchas are needed
64
+
65
+ If `isReadyForCodeGen` is `true` (grade S, A+, or A):
66
+ - Tell the user: "This design scored **{grade}** — ready for code generation with no gotchas needed."
67
+ - Skip directly to **Step 6**.
68
+
69
+ If `isReadyForCodeGen` is `false` (grade B+ or below):
70
+ - Tell the user: "This design scored **{grade}** — running gotcha survey to identify implementation pitfalls."
71
+ - Proceed to **Step 3**.
72
+
73
+ ### Step 3: Run gotcha survey and collect answers
74
+
75
+ If the `gotcha-survey` MCP tool is available, call it:
76
+
77
+ ```
78
+ gotcha-survey({ input: "<figma-url>" })
79
+ ```
80
+
81
+ **Without canicode MCP** — shell out to the CLI (same JSON shape):
82
+
83
+ ```bash
84
+ npx canicode gotcha-survey "<figma-url>" --json
85
+ ```
86
+
87
+ If `questions` is empty, skip to **Step 6**.
88
+
89
+ #### Step 3 — grouped survey (`groupedQuestions`)
90
+
91
+ Iterate `groupedQuestions.groups[].batches[]`. Instance notes, batch prompts, replicas, split/skip/n/a, stdin upsert — **[Appendix Step 3](https://github.com/let-sunny/canicode/blob/main/docs/roundtrip-protocol.md#appendix--step-3-grouped-survey-groupedquestions)**. Per ADR-016, do not re-implement grouping.
92
+
93
+
94
+ ### Step 4: Apply gotcha answers to Figma design
95
+
96
+ 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. The `fileKey` is not needed at this step — the bundled helpers operate on `nodeId` directly.
97
+
98
+ 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.
99
+
100
+ #### Input shape from canicode
101
+
102
+ Every gotcha-survey question (and every entry in `analyzeResult.issues[]`) carries these pre-computed fields:
103
+
104
+ | Field | Type | Meaning |
105
+ |-------|------|---------|
106
+ | `applyStrategy` | `"property-mod"` \| `"structural-mod"` \| `"annotation"` \| `"auto-fix"` | Which strategy branch to enter (A/B/C/D). |
107
+ | `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. |
108
+ | `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). |
109
+ | `suggestedName` | `string` \| (absent) | Naming rules only — pre-capitalized value to write to `node.name` (e.g. `"Hover"`). |
110
+ | `isInstanceChild` | `boolean` | Whether the `nodeId` targets a node inside an INSTANCE subtree. |
111
+ | `sourceChildId` | `string` \| (absent) | Definition node id inside the source component. Use directly with `figma.getNodeByIdAsync`. |
112
+ | `instanceContext` | object \| (absent) | Survey questions only. `{ parentInstanceNodeId, sourceNodeId, sourceComponentId?, sourceComponentName? }` for the Step 3 user-facing note. |
113
+ | `replicas` | `number` \| (absent) | Survey questions only (#356). Total instance count when this one question represents N instance-child issues sharing the same `(sourceComponentId, sourceNodeId, ruleId)` tuple. Absent for single-instance questions. |
114
+ | `replicaNodeIds` | `string[]` \| (absent) | Survey questions only (#356). All OTHER instance scene node ids the answer should land on. The apply step iterates `[nodeId, ...replicaNodeIds]`. Absent when `replicas` is absent. |
115
+
116
+ #### Instance-child matrix, annotation enum matrix, write tiers, probe, helpers
117
+
118
+ Full tables, Experiment 08/09 references, definition-write probe branches, and the bundled `CanICodeRoundtrip` API catalogue live in [`docs/roundtrip-protocol.md`](https://github.com/let-sunny/canicode/blob/main/docs/roundtrip-protocol.md) on `main`. Open it when you need the matrices or helper list — do not re-derive write rules from memory (ADR-016).
119
+
120
+ #### Strategy A: Property Modification — apply directly
121
+
122
+ 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`.
123
+
124
+ ```javascript
125
+ await CanICodeRoundtrip.applyPropertyMod(question, answerValue, { categories });
126
+ ```
127
+
128
+ **Replicas (#356)** — when `question.replicaNodeIds` is present, the same answer must land on every replica instance. Iterate the merged set so each scene gets its own per-node failure routing (under the ADR-012 default each replica annotates independently; with `allowDefinitionWrite: true` they share the one definition write because they share the source):
129
+
130
+ <!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside applyPropertyMod -->
131
+ ```javascript
132
+ const targets = [question.nodeId, ...(question.replicaNodeIds ?? [])];
133
+ for (const nodeId of targets) {
134
+ await CanICodeRoundtrip.applyPropertyMod({ ...question, nodeId }, answerValue, { categories });
135
+ }
136
+ ```
137
+
138
+ Answer shape guide (LLM judgment — the user's answer is prose; parse accordingly):
139
+ - **`non-semantic-name`**: string — the new node name.
140
+ - **`irregular-spacing`**: number for gap (subType `gap`), or `{ paddingTop, paddingRight, paddingBottom, paddingLeft }` for padding.
141
+ - **`fixed-size-in-auto-layout`**: `"FILL"` \| `"HUG"` \| `"FIXED"` — applied to each axis listed in `targetProperty`.
142
+ - **`missing-size-constraint`**: partial `{ minWidth, maxWidth }` — include only the keys the answer supplied.
143
+ - **`no-auto-layout`**: `{ layoutMode, itemSpacing }`; optionally extend with padding/alignment from the answer.
144
+
145
+ **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 }`.
146
+
147
+ 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.
148
+
149
+ #### Strategy B: Structural modification
150
+
151
+ Instance-child guard and per-rule prompts — **[Appendix Strategy B](https://github.com/let-sunny/canicode/blob/main/docs/roundtrip-protocol.md#appendix--strategy-b-structural-modification)**. Decline / guard → Strategy C annotation.
152
+
153
+ #### Strategy C: Annotation — record on the design for designer reference
154
+
155
+ 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. When `question.replicaNodeIds` is present (#356), iterate the merged set so every replica instance gets the annotation:
156
+
157
+ <!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside upsertCanicodeAnnotation -->
158
+ ```javascript
159
+ const targets = [question.nodeId, ...(question.replicaNodeIds ?? [])];
160
+ for (const nodeId of targets) {
161
+ const scene = await figma.getNodeByIdAsync(nodeId);
162
+ CanICodeRoundtrip.upsertCanicodeAnnotation(scene, {
163
+ ruleId: question.ruleId,
164
+ markdown: `**Q:** ${question.question}\n**A:** ${answer}`,
165
+ categoryId: categories.gotcha,
166
+ // Optional: surface live property values in Dev Mode alongside the note.
167
+ // Only include types the node supports (FRAME vs TEXT — see matrix above).
168
+ properties: question.annotationProperties,
169
+ });
170
+ }
171
+ ```
172
+
173
+ Notes:
174
+ - `upsertCanicodeAnnotation` writes the recommendation directly as the body and appends an italic `— *<ruleId>*` footer. The footer is the dedup marker — reruns replace the existing entry in place. The category badge (`canicode:gotcha` / `canicode:flag` / `canicode:fallback`) above the body already brands the annotation, so the body no longer leads with `**[canicode] <ruleId>**` (#353). Pre-#353 entries are still recognised on rerun and replaced with the new format.
175
+ - `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).
176
+ - 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).
177
+
178
+ #### Strategy D: Auto-fix lower-severity issues from analysis
179
+
180
+ The gotcha survey covers blocking/risk severity plus `missing-info` severity from info-collection rules (#406 — currently `missing-prototype`, `missing-interaction-state`). All other 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`). The bundled helper handles the loop, the filter (`applyStrategy === "auto-fix"`), the naming-vs-annotation branch, and the per-issue outcome accumulator in one call:
181
+
182
+ ```javascript
183
+ const outcomes = await CanICodeRoundtrip.applyAutoFixes(analyzeResult.issues, { categories });
184
+ ```
185
+
186
+ `outcomes` is an array of `{ outcome, nodeId, nodeName, ruleId, label }`. `outcome` is one of `🔧` (rename succeeded), `🌐` (definition write propagated — only when `allowDefinitionWrite: true`), `📝` (annotation written, including the fallback path), or `⏭️` (issue's `applyStrategy` was not `"auto-fix"` so it was skipped). Bump the matching `stepFourReport` counter for each entry — `🔧` → `resolved`, `🌐` → `definitionWritten`, `📝` → `annotated`, `⏭️` → `skipped` — so the Step 5 tally (`CanICodeRoundtrip.computeRoundtripTally`, #383) consumes the same structured shape as Strategies A/B/C.
187
+
188
+ `suggestedName` is already capitalized for direct Plugin-API use (e.g. `"Hover"`, `"Default"`, `"Pressed"`). The helper writes it through `applyWithInstanceFallback` so locked / read-only / instance-override nodes annotate cleanly instead of aborting the batch — see the source at `src/core/roundtrip/apply-auto-fix.ts` (#386, ADR-016).
189
+
190
+ #### Execution order
191
+
192
+ 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.)
193
+ 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.
194
+ 2. **Present structural modifications** (Strategy B) one by one, apply confirmed ones.
195
+ 3. **Batch all annotations** (Strategy C + declined structural mods) into a single `use_figma` call — use `categories.gotcha` for the category id.
196
+ 4. **Batch all auto-fixes and annotations for lower-severity issues** (Strategy D) — use `categories.flag` for annotated ones (renamed from `autoFix` per #355 — the category means "flagged for designer attention", not "fixed"). `categories.fallback` from `applyWithInstanceFallback` is **only** the true ADR-012 path (annotate instead of propagating to a source definition); other helper annotate paths use `gotcha` or `flag` (#444).
197
+
198
+ After applying, **emit a structured `stepFourReport`** alongside the human-readable per-question lines. Step 5 reads from this object — it does **not** re-parse the per-question lines (per ADR-016). Increment each counter as Strategy A/B/C/D complete:
199
+
200
+ ```
201
+ Applied {N} changes to the Figma design:
202
+ - ✅ {nodeName}: renamed to "hero-section" (non-semantic-name) — scene/instance override
203
+ - 🌐 {nodeName}: minWidth applied on source definition (missing-size-constraint) — propagates to all instances
204
+ - ✅ {nodeName}: itemSpacing → 16px (irregular-spacing)
205
+ - 🔗 {nodeName}: minWidth bound to variable "mobile-width" (missing-size-constraint)
206
+ - ⏭️ {nodeName}: declined by user, added annotation (deep-nesting)
207
+ - 📝 {nodeName}: annotation added to canicode:gotcha (absolute-position-in-auto-layout)
208
+ - 🔧 {nodeName}: auto-fixed to "Hover" (non-standard-naming)
209
+ - 📝 {nodeName}: annotation added to canicode:flag — raw color needs token binding (raw-value)
210
+
211
+ After each emoji line above, mirror a **structured per-item row** so scene-write vs annotation fallback is visible every run (#435):
212
+
213
+ ```
214
+ {ruleId} @ {nodeName}
215
+ attempt: scene write (`question.targetProperty` / binding shape from answer)
216
+ result: {emoji outcome} ({short reason — e.g. silent-ignore ADR-012 → annotated, override-error → annotated, tier-2 propagated})
217
+ ```
218
+
219
+ stepFourReport = {
220
+ resolved: <count of ✅ + 🔧 + 🔗 lines>, // scene writes, auto-fix renames, variable bindings
221
+ annotated: <count of 📝 lines>, // including ⏭️ declines that fell back to annotation
222
+ definitionWritten: <count of 🌐 lines>, // only non-zero with allowDefinitionWrite: true
223
+ skipped: <count of ⏭️ lines + Step 3 skip/n/a> // user-declined questions
224
+ }
225
+ ```
226
+
227
+ Hold `stepFourReport` in scope through Step 5 — it is the input to `CanICodeRoundtrip.computeRoundtripTally` below.
228
+
229
+ #### Auto-chain acknowledgments after apply (#440)
230
+
231
+ **After every Step 4 apply pass** (any Strategies A–D batch that ran), **do not wait for a separate user prompt** — in the **same session**, immediately run **Step 5a → Step 5b**: `readCanicodeAcknowledgments`, then `analyze({ input, acknowledgments })`. This is **not** conditional on the Step 4 summary containing a 📝 line: pure ✅ / 🔗 scene writes still need the re-analyze + tally for a consistent roundtrip report; when 📝 annotations exist, chaining is **mandatory** so REST analyze can see them — otherwise **`issueCount` stays flat** (`32 → 32`) even when gotchas were captured (#371). Emit the harvest + re-analyze before the conversational wrap-up so **`acknowledgedCount`** and `computeRoundtripTally` land in the **same** apply-summary response as the Step 4 totals.
232
+
233
+ ### Step 5: Re-analyze and report what the roundtrip addressed
234
+
235
+ #### Step 5a: Harvest canicode-authored annotations as acknowledgments (#371)
236
+
237
+ Before re-running `analyze`, collect every `(nodeId, ruleId)` pair that Step 4 wrote as a Figma annotation. The REST API does not expose annotations, so this side channel is the only way the analysis pipeline learns that a roundtrip-touched issue is "the designer has a plan" rather than "still broken". Without it the **issue list** looks unchanged (`32 → 32` issues) — even when every gotcha has been captured per ADR-012.
238
+
239
+ Run a short `use_figma` batch that walks the same subtree the original `analyze` covered (`targetNodeId` if you used one, else `figma.root.id`), reads canicode-categorised annotations, and serialises the result:
240
+
241
+ ```javascript
242
+ // Inside a use_figma batch:
243
+ const categories = await CanICodeRoundtrip.ensureCanicodeCategories();
244
+ const acknowledgments = await CanICodeRoundtrip.readCanicodeAcknowledgments(
245
+ targetNodeId ?? figma.root.id,
246
+ categories
247
+ );
248
+ return { events: [], acknowledgments };
249
+ ```
250
+
251
+ `readCanicodeAcknowledgments` walks `node.children` recursively, gates on the `canicode:gotcha` / `canicode:flag` / `canicode:fallback` (and legacy `canicode:auto-fix`) category ids, and extracts the ruleId from the annotation footer (`— *<ruleId>*`) or the legacy `**[canicode] <ruleId>**` prefix. The categoryId guard keeps user-authored notes that happen to end in italic kebab-case from being mistaken for canicode acknowledgments.
252
+
253
+ #### Step 5b: Re-analyze with acknowledgments
254
+
255
+ Pass the harvested array straight into `analyze` so the engine flags matching issues as `acknowledged: true` and the density score gives them half weight:
256
+
257
+ ```
258
+ analyze({ input: "<figma-url>", acknowledgments })
259
+ ```
260
+
261
+ **Without canicode MCP** — the CLI accepts the same input via `--acknowledgments <path>` (JSON file containing the array). Write the array to a temp file from the `use_figma` return, then:
262
+
263
+ ```bash
264
+ npx canicode analyze "<figma-url>" --json --acknowledgments /tmp/canicode-acks.json
265
+ ```
266
+
267
+ The response now carries:
268
+ - `acknowledgedCount` (top level) — how many issues matched an acknowledgment.
269
+ - `issues[i].acknowledged: true` (per matched issue) — survives into the report and downstream skills.
270
+ - `summary` text — when `acknowledgedCount > 0`, the Total line reads `Total: N (A acknowledged via canicode annotations / N-A unaddressed)`.
271
+
272
+ Under ADR-012's annotate-by-default policy, many writes become 📝 annotations. Treat **issues-delta + `acknowledgedCount`** as the headline success signal — not grade movement (#423).
273
+
274
+ **Tally** — call `CanICodeRoundtrip.computeRoundtripTally` with the structured `stepFourReport` you assembled in Step 4 and the re-analyze response from Step 5b. The helper handles every count derivation (`N = X + Y + Z + W`, `V_open = V - V_ack`) and validates that `acknowledgedCount` cannot exceed `issueCount`. Render the returned `{ X, Y, Z, W, N, V, V_ack, V_open }` straight into the templates below — do **not** re-derive any of these from the Step 4 prose:
275
+
276
+ ```javascript
277
+ const tally = CanICodeRoundtrip.computeRoundtripTally({
278
+ stepFourReport, // the object emitted at the end of Step 4
279
+ reanalyzeResponse: { // narrowed view of the re-analyze response
280
+ issueCount: response.issueCount,
281
+ acknowledgedCount: response.acknowledgedCount,
282
+ },
283
+ });
284
+ ```
285
+
286
+ If Step 4 produced no `stepFourReport` (e.g. user skipped every question, or no gotcha survey ran), pass an all-zero object — `tally.N === 0`, `tally.V_open === tally.V`, and the templates below render the breakdown with zeros rather than treating it as an error. (Skipping Step 5a and passing no `acknowledgments` argument is also valid in this case — the response simply has `acknowledgedCount: 0`.)
287
+
288
+ **All gotcha issues resolved** (`V == 0`, i.e. re-analyze surfaces no remaining issues):
289
+ - Tell the user (fill in the counts from the tally above):
290
+
291
+ ```
292
+ Roundtrip complete — N issues addressed:
293
+ ✅ X resolved (auto-fix or property write succeeded)
294
+ 📝 Y annotated on Figma (gotcha answers captured for code-gen)
295
+ 🌐 Z definition writes propagated (only when allowDefinitionWrite: true)
296
+ ⏭️ W skipped (user declined or "skip")
297
+
298
+ V issues remaining (unresolved gotchas + non-actionable rules)
299
+
300
+ Ready for code generation. *(Optional:) Report still shows grade **{grade}** — informational only.*
301
+ ```
302
+ - Clean up canicode annotations on fixed nodes via `use_figma`. Use the bundled `removeCanicodeAnnotations` helper — it gates on **categoryId** (the durable canicode-side identifier — the body no longer carries a `[canicode]` prefix per #353), includes `legacyAutoFix` if `ensureCanicodeCategories` returned it (pre-#355 `canicode:auto-fix` sweep), and also matches the legacy `**[canicode]` body prefix as a secondary marker for entries on files that have not been re-roundtripped yet. The match logic lives in `src/core/roundtrip/remove-canicode-annotations.ts` with vitest coverage so prose stays ADR-016-compliant:
303
+ <!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside removeCanicodeAnnotations -->
304
+ ```javascript
305
+ const nodeIds = ["id1", "id2"]; // nodes that now pass
306
+ for (const id of nodeIds) {
307
+ const node = await figma.getNodeByIdAsync(id);
308
+ if (node && "annotations" in node) {
309
+ node.annotations = CanICodeRoundtrip.removeCanicodeAnnotations(
310
+ CanICodeRoundtrip.stripAnnotations(node.annotations),
311
+ categories,
312
+ );
313
+ }
314
+ }
315
+ ```
316
+ - Proceed to **Step 6**.
317
+
318
+ **Some issues remain** (`V > 0`):
319
+ - Show the same breakdown and ask whether to proceed. When `V_ack > 0`, expand the remaining line into the acknowledged/unaddressed split surfaced by the re-analyze (#371) so the user can see how much of `V` is "captured for code-gen" vs "still on the user's plate":
320
+
321
+ ```
322
+ Roundtrip complete — N issues addressed:
323
+ ✅ X resolved (auto-fix or property write succeeded)
324
+ 📝 Y annotated on Figma (gotcha answers captured for code-gen)
325
+ 🌐 Z definition writes propagated (only when allowDefinitionWrite: true)
326
+ ⏭️ W skipped (user declined or "skip")
327
+
328
+ V issues remaining
329
+ ↳ V_ack acknowledged via canicode annotations (carried into code-gen)
330
+ ↳ V_open unaddressed (no annotation — your follow-up backlog)
331
+
332
+ Proceed to code generation with remaining context? *(Optional footnote: report grade **{grade}**.)*
333
+ ```
334
+
335
+ When `V_ack == 0` (re-analyze returned `acknowledgedCount: 0`), keep the single `V issues remaining (unresolved gotchas + non-actionable rules)` line.
336
+ - If yes → proceed to **Step 6** with remaining gotcha context.
337
+ - If no → stop and emit the **Stop wrap-up** below; lead with the delta, not grade.
338
+
339
+ #### Wrap-up message rubric (Stop branch)
340
+
341
+ When the user picks **Stop** here, the closing message is the *last thing the user sees of canicode* in this session. Keep the **issues-delta** as the headline (`✅ X / 📝 Y / 🌐 Z / ⏭️ W / V remaining`). Value delivered is **gotchas captured for code-gen** (#423). Optional single line: current report grade — never lead with grade-only framing.
342
+
343
+ ```
344
+ Stopped — N issues addressed, V remaining for manual follow-up:
345
+ ✅ X resolved
346
+ 📝 Y annotated on Figma (carried into code-gen via canicode-gotchas)
347
+ 🌐 Z definition writes propagated
348
+ ⏭️ W skipped
349
+
350
+ V remaining
351
+ ↳ V_ack acknowledged via canicode annotations
352
+ ↳ V_open unaddressed
353
+
354
+ *(Optional)* Report grade: **{grade}**.
355
+ ```
356
+
357
+ When `V_ack == 0`, drop the `↳` lines and leave a single `V remaining` row. Anti-pattern: leading with grade-only sentences. Lead with the delta block.
358
+
359
+ ### Step 6: Implement with Figma MCP
360
+
361
+ Follow the **figma-implement-design** skill workflow to generate code from the Figma design.
362
+
363
+ **If annotations or unresolved gotchas remain from Step 5**, provide them as additional context when implementing:
364
+
365
+ - Gotchas with severity **blocking** MUST be addressed — the design cannot be implemented correctly without this information
366
+ - Gotchas with severity **risk** SHOULD be addressed — they indicate potential issues that will surface later
367
+ - Gotchas with severity **missing-info** from info-collection rules (`purpose === "info-collection"`, e.g. `missing-prototype`, `missing-interaction-state`) are annotation-primary (#406): the answer describes implementation context Figma cannot encode (click target, state variants). Treat them as code-generation context rather than violations to fix — the rule's score impact is minimal by design
368
+ - Reference the specific node IDs from gotcha answers to locate the affected elements in the design
369
+ - Pass the Figma URL or `survey.designKey` 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
370
+
371
+ **If all issues were resolved in Steps 4-5**, no additional gotcha context is needed — the design speaks for itself.
372
+
373
+ #### Wrap-up message rubric (post-handoff)
374
+
375
+ After `figma-implement-design` returns, summarise the roundtrip in the same shape as the Step 5 / Stop wrap-up — issues-delta first, then code-gen outcome; grade at most one optional footline (#423).
376
+
377
+ ```
378
+ Roundtrip complete — N issues addressed, code generated:
379
+ ✅ X resolved
380
+ 📝 Y annotated on Figma (referenced during code-gen)
381
+ 🌐 Z definition writes propagated
382
+ ⏭️ W skipped
383
+
384
+ V issues remaining
385
+ ↳ V_ack acknowledged via canicode annotations
386
+ ↳ V_open unaddressed
387
+
388
+ *(Optional)* Report grade: **{grade}**.
389
+ Code: <files generated / next-step pointer from figma-implement-design>
390
+ ```
391
+
392
+ (Drop the `↳` lines when `V_ack == 0`.)
393
+
394
+ ## Edge cases
395
+
396
+ Full list — **[Appendix Edge Cases](https://github.com/let-sunny/canicode/blob/main/docs/roundtrip-protocol.md#appendix--edge-cases-full-list)**.
397
+