canicode 0.10.2 → 0.10.4
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/README.md +36 -31
- package/dist/cli/index.js +401 -22
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +261 -3
- package/dist/index.js +102 -10
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +200 -24
- package/dist/mcp/server.js.map +1 -1
- package/package.json +3 -2
- package/skills/canicode-gotchas/SKILL.md +66 -28
- package/skills/canicode-roundtrip/SKILL.md +290 -86
- package/skills/canicode-roundtrip/helpers.js +270 -10
|
@@ -11,7 +11,7 @@ Orchestrate the full design-to-code roundtrip: analyze a Figma design for readin
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
13
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
|
|
14
|
+
- **canicode MCP server** (preferred): `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp` — long-form flags only; the short-form `-y -p` collides with `claude mcp add`'s parser (#366). The MCP server reads `FIGMA_TOKEN` from `~/.canicode/config.json` or the host environment, so do **not** pass `-e FIGMA_TOKEN=…` here (#364).
|
|
15
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
16
|
- **FIGMA_TOKEN** configured for live Figma URLs
|
|
17
17
|
- **Figma Full seat + file edit permission** (required for `use_figma` to modify the design)
|
|
@@ -82,39 +82,91 @@ npx canicode gotcha-survey "<figma-url>" --json
|
|
|
82
82
|
|
|
83
83
|
If `questions` is empty, skip to **Step 6**.
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
#### Step 3a: Why the response carries a pre-grouped+batched view
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
The naive "one-question-at-a-time" loop produces two well-known UX failures on real designs:
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
```
|
|
89
|
+
- **Repeated Instance note (#370)** — when 10 consecutive questions share the same `instanceContext.sourceComponentId`, the standard "_Instance note: …source component **X**…_" paragraph prints 10 times. After the first occurrence it adds zero new information and consumes ~2 screens of vertical space.
|
|
90
|
+
- **Repeated identical answer (#369)** — when 7 consecutive questions all carry the same `ruleId` (e.g. `missing-size-constraint`) and the user's reasonable answer would be the same for all of them (e.g. `min-width: 320px, max-width: 1200px`), the user types the same thing 7 times in a row.
|
|
92
91
|
|
|
93
|
-
|
|
92
|
+
`gotcha-survey` already ships the resolution on its `groupedQuestions` field. Sort key (`(sourceComponentId ?? "_no-source", ruleId, nodeName)`), source-component grouping, and the batchable-rule whitelist (`missing-size-constraint`, `irregular-spacing`, `no-auto-layout`, `fixed-size-in-auto-layout`) all live in `core/gotcha/group-and-batch-questions.ts` with vitest coverage. Per ADR-016, do **not** re-implement the sort, partition, or whitelist in prose — iterate over `groupedQuestions.groups[].batches[]` directly.
|
|
94
93
|
|
|
95
|
-
|
|
96
|
-
**[{severity}] {ruleId}** — node: {nodeName}
|
|
94
|
+
#### Step 3b: Prompt each group, then each batch within it
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
For each `group` in `response.groupedQuestions.groups`:
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
- **`group.instanceContext === null`** — this is the trailing group of non-instance questions. Skip the header and prompt each batch directly.
|
|
99
|
+
- **`group.instanceContext !== null`** — emit the Instance note **once** as a group header (#370):
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
─────────────────────────────────────────
|
|
103
|
+
The next {sum of batch.questions.length} questions all target instance children of source component **{instanceContext.sourceComponentName ?? instanceContext.sourceComponentId ?? "unknown"}** (definition node `{instanceContext.sourceNodeId}`). Layout and size fixes may need to apply on the source and propagate to all instances — you will be asked to confirm before any definition-level write.
|
|
104
|
+
─────────────────────────────────────────
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For each `batch` inside the group:
|
|
108
|
+
|
|
109
|
+
- **`batch.questions.length === 1`** — render the standard single-question block for `batch.questions[0]`:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
**[{severity}] {ruleId}** — node: {nodeName}
|
|
113
|
+
|
|
114
|
+
{question}
|
|
115
|
+
|
|
116
|
+
> Hint: {hint}
|
|
117
|
+
> Example: {example}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**If `question.replicas` is present (#356 dedup)**, prepend one note above the standard block:
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
```
|
|
123
|
+
_Replicas: This question represents **{replicas} instances** of the same source-component child sharing the same rule. Your single answer will be applied to all of them in Step 4 (one annotation/write per instance scene)._
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- **`batch.questions.length >= 2 && batch.batchable === true`** (#369) — render one batch prompt covering all members. Use `batch.totalScenes` (already summed across each member's `replicas`) for the Figma-scene fan-out hint:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
**[{severity}] {batch.ruleId}** — {batch.questions.length} instances:
|
|
130
|
+
- {nodeName₁}{ruleSpecificContext₁}
|
|
131
|
+
- {nodeName₂}{ruleSpecificContext₂}
|
|
132
|
+
- …
|
|
108
133
|
|
|
109
|
-
|
|
134
|
+
{sharedQuestionPrompt}
|
|
135
|
+
|
|
136
|
+
Reply with one answer to apply to all {batch.questions.length}, or **split** to answer each individually.
|
|
137
|
+
|
|
138
|
+
> Hint: {hint}
|
|
139
|
+
> Example: {example}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Where:
|
|
143
|
+
- `sharedQuestionPrompt` is the rule's `question` text with the per-node noun replaced by the rule's plural noun (e.g. "These layers all use FILL sizing without min/max constraints. What size boundaries should they share?" instead of repeating "What size boundaries should this layer have?" N times).
|
|
144
|
+
- `ruleSpecificContext` is short and rule-specific: e.g. for `missing-size-constraint` show the current `width`/`height` if the question has them; for `irregular-spacing` show the current `itemSpacing`; otherwise omit.
|
|
145
|
+
- On `split`, fall back to the per-question loop for that batch only — keep the rest of the group's batches as-is.
|
|
146
|
+
|
|
147
|
+
When `batch.totalScenes > batch.questions.length` (at least one member carries replicas), append one note so the user knows their single answer fans out further than the listed nodes:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
_Replicas: your one answer will land on **{batch.totalScenes}** Figma scenes total in Step 4 (some of these {batch.questions.length} questions already represent multiple instances of the same source-component child)._
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **`batch.batchable === false`** is always rendered as a single-question prompt — the helper guarantees `questions.length === 1` for those (identity-typed answers like `non-semantic-name`, structural-mod rules).
|
|
154
|
+
|
|
155
|
+
Wait for the user's answer before moving to the next batch. For each batch, the user may:
|
|
156
|
+
- Answer the question directly (single value covers all batch members)
|
|
157
|
+
- Say **split** (batch only) to fall back to per-question prompting for that batch
|
|
158
|
+
- Say **skip** to skip the question / the entire batch
|
|
159
|
+
- Say **n/a** if the question / the entire batch is not applicable
|
|
160
|
+
|
|
161
|
+
When applying the batched answer, expand back to per-question records before storing — the gotcha section format and Step 4 apply loop both expect one record per `nodeId`.
|
|
162
|
+
|
|
163
|
+
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 `survey.designKey` (the canonical identifier the gotcha-survey response carries — see `/canicode-gotchas` Step 4a) 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
164
|
|
|
111
165
|
Then proceed to **Step 4** to apply answers to the Figma design.
|
|
112
166
|
|
|
113
167
|
### Step 4: Apply gotcha answers to Figma design
|
|
114
168
|
|
|
115
|
-
|
|
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.
|
|
169
|
+
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.
|
|
118
170
|
|
|
119
171
|
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
172
|
|
|
@@ -131,6 +183,8 @@ Every gotcha-survey question (and every entry in `analyzeResult.issues[]`) carri
|
|
|
131
183
|
| `isInstanceChild` | `boolean` | Whether the `nodeId` targets a node inside an INSTANCE subtree. |
|
|
132
184
|
| `sourceChildId` | `string` \| (absent) | Definition node id inside the source component. Use directly with `figma.getNodeByIdAsync`. |
|
|
133
185
|
| `instanceContext` | object \| (absent) | Survey questions only. `{ parentInstanceNodeId, sourceNodeId, sourceComponentId?, sourceComponentName? }` for the Step 3 user-facing note. |
|
|
186
|
+
| `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. |
|
|
187
|
+
| `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. |
|
|
134
188
|
|
|
135
189
|
#### Instance-child property overridability (Plugin API)
|
|
136
190
|
|
|
@@ -177,6 +231,51 @@ The helper walks the tiers in order; variable binding is an alternative writeFn
|
|
|
177
231
|
|
|
178
232
|
**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
233
|
|
|
234
|
+
**Pre-flight writability probe (#357).** Before showing the user the Definition write picker, call `CanICodeRoundtrip.probeDefinitionWritability(questions)` inside a small `use_figma` batch. The probe loads every distinct `sourceChildId` once and classifies it as writable or unwritable using the same detection as the runtime fallback (Experiment 10 `remote === true` and Experiment 11 unresolved-`null`). The result decides which version of the picker to show:
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
// Inside a use_figma batch:
|
|
238
|
+
const probe = await CanICodeRoundtrip.probeDefinitionWritability(questions);
|
|
239
|
+
return { events: [], probe };
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Branches on `probe`:
|
|
243
|
+
|
|
244
|
+
- **`allUnwritable === true`** — every candidate source is in an external library (or unresolved). Opting in is structurally a no-op; every write would throw "Cannot write to internal and read-only node" and fall through to scene annotation anyway. Show the user a single-option picker:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
Definition write policy
|
|
248
|
+
|
|
249
|
+
This file's source components live in an external library and are
|
|
250
|
+
read-only from here ({unwritableSourceNames.join(", ")}). Tier 2
|
|
251
|
+
propagation cannot fire — every "opt-in" write would fall through
|
|
252
|
+
to a scene annotation regardless.
|
|
253
|
+
|
|
254
|
+
❯ 1. Annotate only (only viable option for this file)
|
|
255
|
+
2. Cancel — duplicate the library locally first to enable propagation
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Skip the opt-in branch entirely and call the helpers with the default `allowDefinitionWrite: false`.
|
|
259
|
+
|
|
260
|
+
- **`partiallyUnwritable === true`** — some sources are local, some remote. Surface the split:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
Definition write policy
|
|
264
|
+
|
|
265
|
+
{unwritableCount} of {totalCount} source components are remote
|
|
266
|
+
(read-only) and will fall through to annotation; the remaining
|
|
267
|
+
{totalCount - unwritableCount} are local and will propagate.
|
|
268
|
+
Remote sources: {unwritableSourceNames.join(", ")}.
|
|
269
|
+
|
|
270
|
+
Continue with allowDefinitionWrite: true?
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
When confirmed, propagate to the local sources and let the helper's runtime fallback annotate the remote ones — the existing Experiment-10 retry path absorbs them without aborting the batch.
|
|
274
|
+
|
|
275
|
+
- **`allUnwritable === false && partiallyUnwritable === false`** (the all-local / no-candidates case) — show the existing batch-level picker prose. No probe-driven adjustment needed.
|
|
276
|
+
|
|
277
|
+
The probe is read-only and idempotent; running it before the picker adds one round-trip but saves the user a confusing "I opted in, why did I get annotations?" moment that #342 surfaced live on Simple Design System (Community).
|
|
278
|
+
|
|
180
279
|
**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
280
|
|
|
182
281
|
**Usage in a roundtrip session:**
|
|
@@ -185,11 +284,18 @@ The helper walks the tiers in order; variable binding is an alternative writeFn
|
|
|
185
284
|
2. Prepend its contents verbatim at the top of every `use_figma` batch body — it registers a single global `CanICodeRoundtrip`.
|
|
186
285
|
3. Reference exposed globals as `CanICodeRoundtrip.*`:
|
|
187
286
|
- `stripAnnotations(annotations)` — normalizes the D1 label/labelMarkdown mutex on readback.
|
|
188
|
-
- `ensureCanicodeCategories()` — returns `{ gotcha,
|
|
287
|
+
- `ensureCanicodeCategories()` — returns `{ gotcha, flag, fallback }` category id map (D4); idempotent, safe to call at the top of every batch. May also include `legacyAutoFix` when the file already carries the pre-#355 `canicode:auto-fix` category from earlier roundtrips — read-only on the canicode side, used only by Step 5 cleanup to sweep old annotations.
|
|
189
288
|
- `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
289
|
- `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
290
|
- `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
291
|
- `resolveVariableByName(name)` — local-variable exact-name lookup; returns `null` for remote library variables not imported into this file.
|
|
292
|
+
- `probeDefinitionWritability(questions)` — async pre-flight (#357). Returns `{ totalCount, unwritableCount, unwritableSourceNames, allUnwritable, partiallyUnwritable }`. Use BEFORE the Definition write picker so the picker can drop the opt-in branch when every candidate is in an external library / unresolved (saves the user a wasted "I opted in, why did I get annotations?" decision). Read-only probe, dedupes by `sourceChildId`.
|
|
293
|
+
- `extractAcknowledgmentsFromNode(node, canicodeCategoryIds?)` — synchronous pure helper (#371). Reads one node's annotations and returns `{ nodeId, ruleId }[]` for entries gated by canicode `categoryId` plus a recognisable `— *<ruleId>*` footer (or legacy `**[canicode] <ruleId>**` prefix). When `canicodeCategoryIds` is omitted, footer-text matching alone is sufficient (test mode).
|
|
294
|
+
- `readCanicodeAcknowledgments(rootNodeId, categories?)` — async tree walker (#371). Loads `rootNodeId` via `figma.getNodeByIdAsync`, recurses through `children`, and accumulates one acknowledgment per recognised entry. Used at the top of Step 5a to harvest the side channel that lets the analysis pipeline distinguish "still broken" from "the designer has a plan" — pass the result straight to `analyze({ acknowledgments })`. Errors on individual nodes are swallowed so locked / external nodes don't abort the sweep.
|
|
295
|
+
- `computeRoundtripTally({ stepFourReport, reanalyzeResponse })` — pure helper (#383). Takes the structured Step 4 outcome counts (`{ resolved, annotated, definitionWritten, skipped }`) plus a narrowed re-analyze view (`{ issueCount, acknowledgedCount }`) and returns `{ X, Y, Z, W, N, V, V_ack, V_open }`. Replaces the LLM-side emoji-bullet re-counting in Step 5 — render the returned object directly into the wrap-up templates. Throws when `acknowledgedCount > issueCount` (impossible state).
|
|
296
|
+
- `applyAutoFix(issue, { categories, allowDefinitionWrite?, telemetry? })` — Strategy D entry point (#386). Branches on `targetProperty === "name" && suggestedName` once: renames the node via `applyWithInstanceFallback` (so naming auto-fixes share the same tier-1/2/3 policy as Strategy A) or writes a `categories.flag` annotation carrying `issue.message` and `issue.annotationProperties`. Returns one `AutoFixOutcome` (`{ outcome, nodeId, nodeName, ruleId, label }`) where `outcome` is `🔧` / `🌐` / `📝` so Step 4 can bump the structured `stepFourReport` counters without parsing prose. Replaces the inline JS the SKILL used to carry (per ADR-016).
|
|
297
|
+
- `applyAutoFixes(issues, { categories, allowDefinitionWrite?, telemetry? })` — loop wrapper (#386). Filters `issues` to `applyStrategy === "auto-fix"` (skipped entries surface as `⏭️` outcomes for symmetry) and applies each one in sequence. Returns the full `AutoFixOutcome[]`.
|
|
298
|
+
- `removeCanicodeAnnotations(annotations, categories)` — pure filter. Returns `annotations` with every canicode-authored entry removed (gates on `categories.gotcha` / `flag` / `fallback` / `legacyAutoFix` plus the legacy `**[canicode]` body prefix). Use after `stripAnnotations` in the Step 5 cleanup loop — replaces the inline filter predicate the SKILL used to carry. `isCanicodeAnnotation(annotation, categories)` is the single-entry version, exported for callers that need the predicate alone.
|
|
193
299
|
|
|
194
300
|
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
301
|
|
|
@@ -203,6 +309,16 @@ Rules with `applyStrategy === "property-mod"`. Call the bundled helper — it br
|
|
|
203
309
|
await CanICodeRoundtrip.applyPropertyMod(question, answerValue, { categories });
|
|
204
310
|
```
|
|
205
311
|
|
|
312
|
+
**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):
|
|
313
|
+
|
|
314
|
+
<!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside applyPropertyMod -->
|
|
315
|
+
```javascript
|
|
316
|
+
const targets = [question.nodeId, ...(question.replicaNodeIds ?? [])];
|
|
317
|
+
for (const nodeId of targets) {
|
|
318
|
+
await CanICodeRoundtrip.applyPropertyMod({ ...question, nodeId }, answerValue, { categories });
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
206
322
|
Answer shape guide (LLM judgment — the user's answer is prose; parse accordingly):
|
|
207
323
|
- **`non-semantic-name`**: string — the new node name.
|
|
208
324
|
- **`irregular-spacing`**: number for gap (subType `gap`), or `{ paddingTop, paddingRight, paddingBottom, paddingLeft }` for padding.
|
|
@@ -218,6 +334,23 @@ The name must match **the variable's `name` field exactly** — including any sl
|
|
|
218
334
|
|
|
219
335
|
Rules with `applyStrategy === "structural-mod"`. Show the proposed change and **ask for user confirmation** before applying.
|
|
220
336
|
|
|
337
|
+
> **Instance-child guard (#368).** Strategy B mutations restructure the layer tree — `createComponentFromNode`, `flatten`, wrapper removal, instance-link reconnection. None of these compose safely with the Plugin API's instance-override rules: on a node where `question.isInstanceChild === true`, calling `createComponentFromNode` either throws *"Cannot create a component from a node inside an instance"* or detaches the parent instance entirely (the picked instance is replaced by a one-off frame, severing every existing override and propagation link). Restructuring deep-nested wrappers inside an instance child has the same risk surface — even when the call doesn't throw, the resulting structure cannot ride the source-component's propagation in future updates.
|
|
338
|
+
>
|
|
339
|
+
> Before showing the per-rule prompt below, check `question.isInstanceChild`. If it is true, **do not run the destructive call**. Surface this a/b prompt instead and default to **(a)**:
|
|
340
|
+
>
|
|
341
|
+
> ```
|
|
342
|
+
> **{ruleId}** would normally restructure **{nodeName}** here, but this node lives inside instance **{instanceContext.parentInstanceNodeId}** of source component **{instanceContext.sourceComponentName or sourceComponentId or "unknown"}** (definition node `{instanceContext.sourceNodeId}`). On instance children Plugin API restructuring either fails outright or detaches the parent instance.
|
|
343
|
+
>
|
|
344
|
+
> ❯ a) Annotate the scene with a recommendation to apply the change on the source definition (safe — picks up via canicode-gotchas in code-gen, source designer can act on it later)
|
|
345
|
+
> b) Detach the parent instance and attempt the restructuring on the resulting one-off frame (destructive — every existing instance override is lost and the node no longer rides the source component's propagation)
|
|
346
|
+
> ```
|
|
347
|
+
>
|
|
348
|
+
> On **(a)**, route to Strategy C — call `upsertCanicodeAnnotation(scene, { ruleId: question.ruleId, markdown: "**Q:** … **A:** Apply on source definition `${instanceContext.sourceNodeId}` (`${instanceContext.sourceComponentName ?? "unknown"}`) — instance-child restructuring would detach the parent instance.", categoryId: categories.gotcha })`. Reference `instanceContext.sourceComponentName` and `instanceContext.sourceNodeId` in the body so the source designer can locate the target.
|
|
349
|
+
>
|
|
350
|
+
> On **(b)**, gate behind a second confirmation that explicitly names the side effects ("This will detach instance **{parentInstanceNodeId}** — all overrides on it will be lost and it will stop receiving updates from **{sourceComponentName}**. Type the parent instance name to confirm."). Only then execute the per-rule destructive call below.
|
|
351
|
+
>
|
|
352
|
+
> The same posture as ADR-012's `allowDefinitionWrite: false` default: instance-child structural mutations are off-by-default and require explicit user opt-in *per node*, not per batch — the destructive call here doesn't have a quiet fallback the way Strategy A's `applyWithInstanceFallback` does.
|
|
353
|
+
|
|
221
354
|
**`non-layout-container`** — Convert Group/Section to Auto Layout frame:
|
|
222
355
|
- Prompt: "I'll convert **{nodeName}** to an Auto Layout frame with {direction} layout and {spacing}px gap. Proceed?"
|
|
223
356
|
- If confirmed: `applyPropertyMod(question, { layoutMode: "VERTICAL", itemSpacing: 12 })`.
|
|
@@ -240,64 +373,44 @@ if (scene && scene.type === "FRAME") {
|
|
|
240
373
|
- Prompt: "I'll reconnect **{nodeName}** to its original component. Any overrides will be preserved. Proceed?"
|
|
241
374
|
- Requires finding the original component — if not identifiable, fall back to annotation.
|
|
242
375
|
|
|
243
|
-
If the user **declines** any structural modification, add an annotation instead (same as Strategy C).
|
|
376
|
+
If the user **declines** any structural modification (or the instance-child guard above routes to **(a)**), add an annotation instead (same as Strategy C).
|
|
244
377
|
|
|
245
378
|
#### Strategy C: Annotation — record on the design for designer reference
|
|
246
379
|
|
|
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.
|
|
380
|
+
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:
|
|
248
381
|
|
|
382
|
+
<!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside upsertCanicodeAnnotation -->
|
|
249
383
|
```javascript
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
384
|
+
const targets = [question.nodeId, ...(question.replicaNodeIds ?? [])];
|
|
385
|
+
for (const nodeId of targets) {
|
|
386
|
+
const scene = await figma.getNodeByIdAsync(nodeId);
|
|
387
|
+
CanICodeRoundtrip.upsertCanicodeAnnotation(scene, {
|
|
388
|
+
ruleId: question.ruleId,
|
|
389
|
+
markdown: `**Q:** ${question.question}\n**A:** ${answer}`,
|
|
390
|
+
categoryId: categories.gotcha,
|
|
391
|
+
// Optional: surface live property values in Dev Mode alongside the note.
|
|
392
|
+
// Only include types the node supports (FRAME vs TEXT — see matrix above).
|
|
393
|
+
properties: question.annotationProperties,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
259
396
|
```
|
|
260
397
|
|
|
261
398
|
Notes:
|
|
262
|
-
- `upsertCanicodeAnnotation`
|
|
399
|
+
- `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.
|
|
263
400
|
- `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
401
|
- 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
402
|
|
|
266
403
|
#### Strategy D: Auto-fix lower-severity issues from analysis
|
|
267
404
|
|
|
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`).
|
|
405
|
+
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`). 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:
|
|
269
406
|
|
|
270
407
|
```javascript
|
|
271
|
-
|
|
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
|
-
}
|
|
408
|
+
const outcomes = await CanICodeRoundtrip.applyAutoFixes(analyzeResult.issues, { categories });
|
|
298
409
|
```
|
|
299
410
|
|
|
300
|
-
`
|
|
411
|
+
`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.
|
|
412
|
+
|
|
413
|
+
`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).
|
|
301
414
|
|
|
302
415
|
#### Execution order
|
|
303
416
|
|
|
@@ -305,9 +418,9 @@ for (const issue of analyzeResult.issues) {
|
|
|
305
418
|
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
419
|
2. **Present structural modifications** (Strategy B) one by one, apply confirmed ones.
|
|
307
420
|
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.
|
|
421
|
+
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` is reserved for errors surfaced by `applyWithInstanceFallback` itself.
|
|
309
422
|
|
|
310
|
-
After applying,
|
|
423
|
+
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:
|
|
311
424
|
|
|
312
425
|
```
|
|
313
426
|
Applied {N} changes to the Figma design:
|
|
@@ -318,30 +431,74 @@ Applied {N} changes to the Figma design:
|
|
|
318
431
|
- ⏭️ {nodeName}: declined by user, added annotation (deep-nesting)
|
|
319
432
|
- 📝 {nodeName}: annotation added to canicode:gotcha (absolute-position-in-auto-layout)
|
|
320
433
|
- 🔧 {nodeName}: auto-fixed to "Hover" (non-standard-naming)
|
|
321
|
-
- 📝 {nodeName}: annotation added to canicode:
|
|
434
|
+
- 📝 {nodeName}: annotation added to canicode:flag — raw color needs token binding (raw-value)
|
|
435
|
+
|
|
436
|
+
stepFourReport = {
|
|
437
|
+
resolved: <count of ✅ + 🔧 + 🔗 lines>, // scene writes, auto-fix renames, variable bindings
|
|
438
|
+
annotated: <count of 📝 lines>, // including ⏭️ declines that fell back to annotation
|
|
439
|
+
definitionWritten: <count of 🌐 lines>, // only non-zero with allowDefinitionWrite: true
|
|
440
|
+
skipped: <count of ⏭️ lines + Step 3 skip/n/a> // user-declined questions
|
|
441
|
+
}
|
|
322
442
|
```
|
|
323
443
|
|
|
444
|
+
Hold `stepFourReport` in scope through Step 5 — it is the input to `CanICodeRoundtrip.computeRoundtripTally` below.
|
|
445
|
+
|
|
324
446
|
### Step 5: Re-analyze and report what the roundtrip addressed
|
|
325
447
|
|
|
326
|
-
|
|
448
|
+
#### Step 5a: Harvest canicode-authored annotations as acknowledgments (#371)
|
|
449
|
+
|
|
450
|
+
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 grade and issue count look identical to the pre-roundtrip state — `32 → 32` — even when every gotcha has been captured per ADR-012.
|
|
327
451
|
|
|
452
|
+
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:
|
|
453
|
+
|
|
454
|
+
```javascript
|
|
455
|
+
// Inside a use_figma batch:
|
|
456
|
+
const categories = await CanICodeRoundtrip.ensureCanicodeCategories();
|
|
457
|
+
const acknowledgments = await CanICodeRoundtrip.readCanicodeAcknowledgments(
|
|
458
|
+
targetNodeId ?? figma.root.id,
|
|
459
|
+
categories
|
|
460
|
+
);
|
|
461
|
+
return { events: [], acknowledgments };
|
|
328
462
|
```
|
|
329
|
-
|
|
463
|
+
|
|
464
|
+
`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.
|
|
465
|
+
|
|
466
|
+
#### Step 5b: Re-analyze with acknowledgments
|
|
467
|
+
|
|
468
|
+
Pass the harvested array straight into `analyze` so the engine flags matching issues as `acknowledged: true` and the density score gives them half weight:
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
analyze({ input: "<figma-url>", acknowledgments })
|
|
330
472
|
```
|
|
331
473
|
|
|
332
|
-
|
|
474
|
+
**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:
|
|
333
475
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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`.
|
|
476
|
+
```bash
|
|
477
|
+
npx canicode analyze "<figma-url>" --json --acknowledgments /tmp/canicode-acks.json
|
|
478
|
+
```
|
|
341
479
|
|
|
342
|
-
|
|
480
|
+
The response now carries:
|
|
481
|
+
- `acknowledgedCount` (top level) — how many issues matched an acknowledgment.
|
|
482
|
+
- `issues[i].acknowledged: true` (per matched issue) — survives into the report and downstream skills.
|
|
483
|
+
- `summary` text — when `acknowledgedCount > 0`, the Total line reads `Total: N (A acknowledged via canicode annotations / N-A unaddressed)`.
|
|
343
484
|
|
|
344
|
-
|
|
485
|
+
Under ADR-012's annotate-by-default policy, most instance-child gotchas route to 📝 annotations and do **not** move the numeric grade — but the half-weight density now produces a small visible movement when annotations are recognised. The headline for this step remains the **issues-delta** (what the roundtrip captured); grade movement is a secondary signal.
|
|
486
|
+
|
|
487
|
+
**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:
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
const tally = CanICodeRoundtrip.computeRoundtripTally({
|
|
491
|
+
stepFourReport, // the object emitted at the end of Step 4
|
|
492
|
+
reanalyzeResponse: { // narrowed view of the re-analyze response
|
|
493
|
+
issueCount: response.issueCount,
|
|
494
|
+
acknowledgedCount: response.acknowledgedCount,
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
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`.)
|
|
500
|
+
|
|
501
|
+
**All gotcha issues resolved** (`V == 0`, i.e. re-analyze surfaces no remaining issues — note this is mostly independent of grade since ADR-012 annotations only move the score by the half-weight reduction enabled in Step 5b):
|
|
345
502
|
- Tell the user (fill in the counts from the tally above):
|
|
346
503
|
|
|
347
504
|
```
|
|
@@ -355,14 +512,16 @@ If Step 4 produced no report block (e.g. user skipped every question, or no gotc
|
|
|
355
512
|
|
|
356
513
|
Grade: {oldGrade} → {newGrade}. Ready for code generation.
|
|
357
514
|
```
|
|
358
|
-
- Clean up canicode annotations
|
|
515
|
+
- 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:
|
|
516
|
+
<!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside removeCanicodeAnnotations -->
|
|
359
517
|
```javascript
|
|
360
518
|
const nodeIds = ["id1", "id2"]; // nodes that now pass
|
|
361
519
|
for (const id of nodeIds) {
|
|
362
520
|
const node = await figma.getNodeByIdAsync(id);
|
|
363
521
|
if (node && "annotations" in node) {
|
|
364
|
-
node.annotations = CanICodeRoundtrip.
|
|
365
|
-
|
|
522
|
+
node.annotations = CanICodeRoundtrip.removeCanicodeAnnotations(
|
|
523
|
+
CanICodeRoundtrip.stripAnnotations(node.annotations),
|
|
524
|
+
categories,
|
|
366
525
|
);
|
|
367
526
|
}
|
|
368
527
|
}
|
|
@@ -370,7 +529,7 @@ for (const id of nodeIds) {
|
|
|
370
529
|
- Proceed to **Step 6**.
|
|
371
530
|
|
|
372
531
|
**Some issues remain** (`V > 0`):
|
|
373
|
-
- Show the same breakdown and ask whether to proceed:
|
|
532
|
+
- 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":
|
|
374
533
|
|
|
375
534
|
```
|
|
376
535
|
Roundtrip complete — N issues addressed:
|
|
@@ -379,12 +538,36 @@ for (const id of nodeIds) {
|
|
|
379
538
|
🌐 Z definition writes propagated (only when allowDefinitionWrite: true)
|
|
380
539
|
⏭️ W skipped (user declined or "skip")
|
|
381
540
|
—
|
|
382
|
-
V issues remaining
|
|
541
|
+
V issues remaining
|
|
542
|
+
↳ V_ack acknowledged via canicode annotations (carried into code-gen)
|
|
543
|
+
↳ V_open unaddressed (no annotation — your follow-up backlog)
|
|
383
544
|
|
|
384
545
|
Grade: {oldGrade} → {newGrade}. Proceed to code generation with remaining context?
|
|
385
546
|
```
|
|
547
|
+
|
|
548
|
+
When `V_ack == 0` (re-analyze returned `acknowledgedCount: 0`), keep the single `V issues remaining (unresolved gotchas + non-actionable rules)` line.
|
|
386
549
|
- If yes → proceed to **Step 6** with remaining gotcha context.
|
|
387
|
-
- If no → stop and
|
|
550
|
+
- If no → stop and emit the **Stop wrap-up** below; do **not** restate the grade as the lead.
|
|
551
|
+
|
|
552
|
+
#### Wrap-up message rubric (Stop branch)
|
|
553
|
+
|
|
554
|
+
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`) — grade movement, if any, belongs as a footnote line **after** the delta, not as the lead bullet. Reason: the value canicode delivers under the ADR-012 default is the annotation count carried into code-gen, not score movement (per [#341](https://github.com/let-sunny/canicode/issues/341), [#352](https://github.com/let-sunny/canicode/issues/352)).
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
Stopped — N issues addressed, V remaining for manual follow-up:
|
|
558
|
+
✅ X resolved
|
|
559
|
+
📝 Y annotated on Figma (carried into code-gen via canicode-gotchas)
|
|
560
|
+
🌐 Z definition writes propagated
|
|
561
|
+
⏭️ W skipped
|
|
562
|
+
—
|
|
563
|
+
V remaining
|
|
564
|
+
↳ V_ack acknowledged via canicode annotations
|
|
565
|
+
↳ V_open unaddressed
|
|
566
|
+
|
|
567
|
+
Grade: {oldGrade} → {newGrade}.
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
When `V_ack == 0`, drop the `↳` lines and leave a single `V remaining` row. Anti-pattern (do **not** lead with a grade-only sentence like "Grade: C → C+. Most size-constraint gotchas are now annotations…"). Lead with the delta block; mention grade once, on its own footnote line, plain prose only.
|
|
388
571
|
|
|
389
572
|
### Step 6: Implement with Figma MCP
|
|
390
573
|
|
|
@@ -395,10 +578,31 @@ Follow the **figma-implement-design** skill workflow to generate code from the F
|
|
|
395
578
|
- Gotchas with severity **blocking** MUST be addressed — the design cannot be implemented correctly without this information
|
|
396
579
|
- Gotchas with severity **risk** SHOULD be addressed — they indicate potential issues that will surface later
|
|
397
580
|
- Reference the specific node IDs from gotcha answers to locate the affected elements in the design
|
|
398
|
-
- Pass the Figma URL
|
|
581
|
+
- 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
|
|
399
582
|
|
|
400
583
|
**If all issues were resolved in Steps 4-5**, no additional gotcha context is needed — the design speaks for itself.
|
|
401
584
|
|
|
585
|
+
#### Wrap-up message rubric (post-handoff)
|
|
586
|
+
|
|
587
|
+
After `figma-implement-design` returns, summarise the roundtrip in the same shape as the Step 5 / Stop wrap-up — issues-delta first, grade as a footnote, then the code-gen outcome. Do **not** lead with grade movement (per [#352](https://github.com/let-sunny/canicode/issues/352)):
|
|
588
|
+
|
|
589
|
+
```
|
|
590
|
+
Roundtrip complete — N issues addressed, code generated:
|
|
591
|
+
✅ X resolved
|
|
592
|
+
📝 Y annotated on Figma (referenced during code-gen)
|
|
593
|
+
🌐 Z definition writes propagated
|
|
594
|
+
⏭️ W skipped
|
|
595
|
+
—
|
|
596
|
+
V issues remaining
|
|
597
|
+
↳ V_ack acknowledged via canicode annotations
|
|
598
|
+
↳ V_open unaddressed
|
|
599
|
+
|
|
600
|
+
Grade: {oldGrade} → {newGrade}.
|
|
601
|
+
Code: <files generated / next-step pointer from figma-implement-design>
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
(Drop the `↳` lines when `V_ack == 0`.)
|
|
605
|
+
|
|
402
606
|
## Edge Cases
|
|
403
607
|
|
|
404
608
|
- **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.
|
|
@@ -410,4 +614,4 @@ Follow the **figma-implement-design** skill workflow to generate code from the F
|
|
|
410
614
|
- **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
615
|
- **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
616
|
- **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.
|
|
617
|
+
- **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. **The pre-flight `probeDefinitionWritability` (#357) detects both branches up-front** so the Definition write picker can drop the opt-in option entirely when every candidate is unwritable, saving the user a wasted decision before the runtime fallback kicks in.
|