canicode 0.11.0 → 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.
- package/README.md +16 -4
- package/dist/cli/index.js +520 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +20 -17
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +24 -4
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +24 -1
- package/package.json +1 -1
- package/skills/canicode/SKILL.md +6 -0
- package/skills/canicode-gotchas/SKILL.md +38 -59
- package/skills/canicode-roundtrip/SKILL.md +39 -260
- package/skills/canicode-roundtrip/helpers.js +287 -17
- package/skills/cursor/canicode/SKILL.md +6 -0
- package/skills/cursor/canicode-gotchas/SKILL.md +38 -59
- package/skills/cursor/canicode-roundtrip/SKILL.md +39 -260
- package/skills/cursor/canicode-roundtrip/helpers.js +287 -17
|
@@ -6,7 +6,9 @@ disable-model-invocation: false
|
|
|
6
6
|
|
|
7
7
|
# CanICode Roundtrip — True Design-to-Code Roundtrip
|
|
8
8
|
|
|
9
|
-
|
|
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).
|
|
10
12
|
|
|
11
13
|
## Prerequisites
|
|
12
14
|
|
|
@@ -30,6 +32,8 @@ If `use_figma` is unavailable in the current session, **Do NOT proceed to Step 1
|
|
|
30
32
|
|
|
31
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.
|
|
32
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
|
+
|
|
33
37
|
### Step 1: Analyze the design
|
|
34
38
|
|
|
35
39
|
If the `analyze` MCP tool is available, call it with the user's Figma URL:
|
|
@@ -82,87 +86,10 @@ npx canicode gotcha-survey "<figma-url>" --json
|
|
|
82
86
|
|
|
83
87
|
If `questions` is empty, skip to **Step 6**.
|
|
84
88
|
|
|
85
|
-
#### Step
|
|
86
|
-
|
|
87
|
-
The naive "one-question-at-a-time" loop produces two well-known UX failures on real designs:
|
|
88
|
-
|
|
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.
|
|
91
|
-
|
|
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.
|
|
93
|
-
|
|
94
|
-
#### Step 3b: Prompt each group, then each batch within it
|
|
95
|
-
|
|
96
|
-
For each `group` in `response.groupedQuestions.groups`:
|
|
97
|
-
|
|
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:
|
|
121
|
-
|
|
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
|
-
- …
|
|
133
|
-
|
|
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
|
|
89
|
+
#### Step 3 — grouped survey (`groupedQuestions`)
|
|
160
90
|
|
|
161
|
-
|
|
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.
|
|
162
92
|
|
|
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.
|
|
164
|
-
|
|
165
|
-
Then proceed to **Step 4** to apply answers to the Figma design.
|
|
166
93
|
|
|
167
94
|
### Step 4: Apply gotcha answers to Figma design
|
|
168
95
|
|
|
@@ -186,120 +113,9 @@ Every gotcha-survey question (and every entry in `analyzeResult.issues[]`) carri
|
|
|
186
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. |
|
|
187
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. |
|
|
188
115
|
|
|
189
|
-
#### Instance-child
|
|
190
|
-
|
|
191
|
-
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.
|
|
192
|
-
|
|
193
|
-
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).
|
|
194
|
-
|
|
195
|
-
| Property | Raw-value write on instance child | Variable binding | Notes |
|
|
196
|
-
|----------|----------------------------------|------------------|-------|
|
|
197
|
-
| `node.name` | ✅ | — | Prefer scene node first. |
|
|
198
|
-
| `annotations` | ✅ | — | Good fallback when another property cannot be set. |
|
|
199
|
-
| `itemSpacing`, `paddingTop/Right/Bottom/Left` | ✅ | ✅ | |
|
|
200
|
-
| `primaryAxisAlignItems`, `counterAxisAlignItems`, `layoutAlign` | ✅ | — | |
|
|
201
|
-
| `cornerRadius`, `opacity` | ✅ | ✅ | |
|
|
202
|
-
| `fills`, `strokes` (raw color) | ✅ | ✅ via `setBoundVariableForPaint(paint, "color", v)` | |
|
|
203
|
-
| `layoutSizingHorizontal` / `layoutSizingVertical` | ✅ | — | |
|
|
204
|
-
| `layoutMode` | ⚠️ on some nodes | — | Some instance children silently ignore the write (no throw, no change). |
|
|
205
|
-
| **`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. |
|
|
206
|
-
| `fontSize`, `lineHeight`, `letterSpacing`, `paragraphSpacing` (TEXT) | ✅ | ✅ | |
|
|
207
|
-
| `characters` (TEXT) | ✅ | ✅ STRING variable | |
|
|
208
|
-
|
|
209
|
-
#### Annotation `properties` matrix
|
|
210
|
-
|
|
211
|
-
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.
|
|
212
|
-
|
|
213
|
-
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`):
|
|
214
|
-
|
|
215
|
-
| Node type | Accepted (scene) | Additionally rejected on instance child | Rejected in all contexts |
|
|
216
|
-
|-----------|------------------|-----------------------------------------|--------------------------|
|
|
217
|
-
| 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 |
|
|
218
|
-
| 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 |
|
|
116
|
+
#### Instance-child matrix, annotation enum matrix, write tiers, probe, helpers
|
|
219
117
|
|
|
220
|
-
`
|
|
221
|
-
|
|
222
|
-
> **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.
|
|
223
|
-
|
|
224
|
-
**Write policy (ordered tiers):**
|
|
225
|
-
|
|
226
|
-
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.
|
|
227
|
-
|
|
228
|
-
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 ✅.
|
|
229
|
-
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 🌐.
|
|
230
|
-
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 actual no-op (the property silently ignored the write or the override was rejected) and points to the source component as the correct write target. 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 📝.
|
|
231
|
-
|
|
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.
|
|
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
|
-
|
|
279
|
-
**Shared helpers (bundled)** — the deterministic helpers live in TypeScript at `src/core/roundtrip/*.ts` and are bundled to a single IIFE shipped next to this skill as `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.
|
|
280
|
-
|
|
281
|
-
**Usage in a roundtrip session:**
|
|
282
|
-
|
|
283
|
-
1. Read `helpers.js` from the same directory as this skill once at the start of Step 4 — typically `.claude/skills/canicode-roundtrip/helpers.js` (Claude Code / default `canicode init`) or `.cursor/skills/canicode-roundtrip/helpers.js` (Cursor with `canicode init --cursor-skills`).
|
|
284
|
-
2. Prepend its contents verbatim at the top of every `use_figma` batch body — it registers a single global `CanICodeRoundtrip`.
|
|
285
|
-
3. Reference exposed globals as `CanICodeRoundtrip.*`:
|
|
286
|
-
- `stripAnnotations(annotations)` — normalizes the D1 label/labelMarkdown mutex on readback.
|
|
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.
|
|
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.
|
|
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.
|
|
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`.
|
|
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.
|
|
299
|
-
|
|
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.
|
|
301
|
-
|
|
302
|
-
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.
|
|
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).
|
|
303
119
|
|
|
304
120
|
#### Strategy A: Property Modification — apply directly
|
|
305
121
|
|
|
@@ -330,50 +146,9 @@ Answer shape guide (LLM judgment — the user's answer is prose; parse according
|
|
|
330
146
|
|
|
331
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.
|
|
332
148
|
|
|
333
|
-
#### Strategy B: Structural
|
|
334
|
-
|
|
335
|
-
Rules with `applyStrategy === "structural-mod"`. Show the proposed change and **ask for user confirmation** before applying.
|
|
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
|
-
|
|
354
|
-
**`non-layout-container`** — Convert Group/Section to Auto Layout frame:
|
|
355
|
-
- Prompt: "I'll convert **{nodeName}** to an Auto Layout frame with {direction} layout and {spacing}px gap. Proceed?"
|
|
356
|
-
- If confirmed: `applyPropertyMod(question, { layoutMode: "VERTICAL", itemSpacing: 12 })`.
|
|
357
|
-
|
|
358
|
-
**`deep-nesting`** — Flatten intermediate wrappers or extract sub-component:
|
|
359
|
-
- Prompt: "I'll flatten **{nodeName}** by {description from answer}. This changes the layer hierarchy. Proceed?"
|
|
360
|
-
- Apply based on the specific answer (remove wrappers, convert padding, etc.).
|
|
361
|
-
|
|
362
|
-
**`missing-component`** — Convert frame to reusable component:
|
|
363
|
-
- Prompt: "I'll convert **{nodeName}** to a reusable component. Proceed?"
|
|
364
|
-
- If confirmed:
|
|
365
|
-
```javascript
|
|
366
|
-
const scene = await figma.getNodeByIdAsync(question.nodeId);
|
|
367
|
-
if (scene && scene.type === "FRAME") {
|
|
368
|
-
figma.createComponentFromNode(scene);
|
|
369
|
-
}
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
**`detached-instance`** — Reconnect to original component:
|
|
373
|
-
- Prompt: "I'll reconnect **{nodeName}** to its original component. Any overrides will be preserved. Proceed?"
|
|
374
|
-
- Requires finding the original component — if not identifiable, fall back to annotation.
|
|
149
|
+
#### Strategy B: Structural modification
|
|
375
150
|
|
|
376
|
-
|
|
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.
|
|
377
152
|
|
|
378
153
|
#### Strategy C: Annotation — record on the design for designer reference
|
|
379
154
|
|
|
@@ -418,7 +193,7 @@ const outcomes = await CanICodeRoundtrip.applyAutoFixes(analyzeResult.issues, {
|
|
|
418
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.
|
|
419
194
|
2. **Present structural modifications** (Strategy B) one by one, apply confirmed ones.
|
|
420
195
|
3. **Batch all annotations** (Strategy C + declined structural mods) into a single `use_figma` call — use `categories.gotcha` for the category id.
|
|
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")
|
|
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).
|
|
422
197
|
|
|
423
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:
|
|
424
199
|
|
|
@@ -433,6 +208,14 @@ Applied {N} changes to the Figma design:
|
|
|
433
208
|
- 🔧 {nodeName}: auto-fixed to "Hover" (non-standard-naming)
|
|
434
209
|
- 📝 {nodeName}: annotation added to canicode:flag — raw color needs token binding (raw-value)
|
|
435
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
|
+
|
|
436
219
|
stepFourReport = {
|
|
437
220
|
resolved: <count of ✅ + 🔧 + 🔗 lines>, // scene writes, auto-fix renames, variable bindings
|
|
438
221
|
annotated: <count of 📝 lines>, // including ⏭️ declines that fell back to annotation
|
|
@@ -443,11 +226,15 @@ stepFourReport = {
|
|
|
443
226
|
|
|
444
227
|
Hold `stepFourReport` in scope through Step 5 — it is the input to `CanICodeRoundtrip.computeRoundtripTally` below.
|
|
445
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
|
+
|
|
446
233
|
### Step 5: Re-analyze and report what the roundtrip addressed
|
|
447
234
|
|
|
448
235
|
#### Step 5a: Harvest canicode-authored annotations as acknowledgments (#371)
|
|
449
236
|
|
|
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
|
|
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.
|
|
451
238
|
|
|
452
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:
|
|
453
240
|
|
|
@@ -482,7 +269,7 @@ The response now carries:
|
|
|
482
269
|
- `issues[i].acknowledged: true` (per matched issue) — survives into the report and downstream skills.
|
|
483
270
|
- `summary` text — when `acknowledgedCount > 0`, the Total line reads `Total: N (A acknowledged via canicode annotations / N-A unaddressed)`.
|
|
484
271
|
|
|
485
|
-
Under ADR-012's annotate-by-default policy,
|
|
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).
|
|
486
273
|
|
|
487
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:
|
|
488
275
|
|
|
@@ -498,7 +285,7 @@ const tally = CanICodeRoundtrip.computeRoundtripTally({
|
|
|
498
285
|
|
|
499
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`.)
|
|
500
287
|
|
|
501
|
-
**All gotcha issues resolved** (`V == 0`, i.e. re-analyze surfaces no remaining issues
|
|
288
|
+
**All gotcha issues resolved** (`V == 0`, i.e. re-analyze surfaces no remaining issues):
|
|
502
289
|
- Tell the user (fill in the counts from the tally above):
|
|
503
290
|
|
|
504
291
|
```
|
|
@@ -510,7 +297,7 @@ If Step 4 produced no `stepFourReport` (e.g. user skipped every question, or no
|
|
|
510
297
|
—
|
|
511
298
|
V issues remaining (unresolved gotchas + non-actionable rules)
|
|
512
299
|
|
|
513
|
-
|
|
300
|
+
Ready for code generation. *(Optional:) Report still shows grade **{grade}** — informational only.*
|
|
514
301
|
```
|
|
515
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:
|
|
516
303
|
<!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside removeCanicodeAnnotations -->
|
|
@@ -542,16 +329,16 @@ for (const id of nodeIds) {
|
|
|
542
329
|
↳ V_ack acknowledged via canicode annotations (carried into code-gen)
|
|
543
330
|
↳ V_open unaddressed (no annotation — your follow-up backlog)
|
|
544
331
|
|
|
545
|
-
|
|
332
|
+
Proceed to code generation with remaining context? *(Optional footnote: report grade **{grade}**.)*
|
|
546
333
|
```
|
|
547
334
|
|
|
548
335
|
When `V_ack == 0` (re-analyze returned `acknowledgedCount: 0`), keep the single `V issues remaining (unresolved gotchas + non-actionable rules)` line.
|
|
549
336
|
- If yes → proceed to **Step 6** with remaining gotcha context.
|
|
550
|
-
- If no → stop and emit the **Stop wrap-up** below;
|
|
337
|
+
- If no → stop and emit the **Stop wrap-up** below; lead with the delta, not grade.
|
|
551
338
|
|
|
552
339
|
#### Wrap-up message rubric (Stop branch)
|
|
553
340
|
|
|
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`)
|
|
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.
|
|
555
342
|
|
|
556
343
|
```
|
|
557
344
|
Stopped — N issues addressed, V remaining for manual follow-up:
|
|
@@ -564,10 +351,10 @@ Stopped — N issues addressed, V remaining for manual follow-up:
|
|
|
564
351
|
↳ V_ack acknowledged via canicode annotations
|
|
565
352
|
↳ V_open unaddressed
|
|
566
353
|
|
|
567
|
-
|
|
354
|
+
*(Optional)* Report grade: **{grade}**.
|
|
568
355
|
```
|
|
569
356
|
|
|
570
|
-
When `V_ack == 0`, drop the `↳` lines and leave a single `V remaining` row. Anti-pattern
|
|
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.
|
|
571
358
|
|
|
572
359
|
### Step 6: Implement with Figma MCP
|
|
573
360
|
|
|
@@ -585,7 +372,7 @@ Follow the **figma-implement-design** skill workflow to generate code from the F
|
|
|
585
372
|
|
|
586
373
|
#### Wrap-up message rubric (post-handoff)
|
|
587
374
|
|
|
588
|
-
After `figma-implement-design` returns, summarise the roundtrip in the same shape as the Step 5 / Stop wrap-up — issues-delta first,
|
|
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).
|
|
589
376
|
|
|
590
377
|
```
|
|
591
378
|
Roundtrip complete — N issues addressed, code generated:
|
|
@@ -598,21 +385,13 @@ Roundtrip complete — N issues addressed, code generated:
|
|
|
598
385
|
↳ V_ack acknowledged via canicode annotations
|
|
599
386
|
↳ V_open unaddressed
|
|
600
387
|
|
|
601
|
-
|
|
388
|
+
*(Optional)* Report grade: **{grade}**.
|
|
602
389
|
Code: <files generated / next-step pointer from figma-implement-design>
|
|
603
390
|
```
|
|
604
391
|
|
|
605
392
|
(Drop the `↳` lines when `V_ack == 0`.)
|
|
606
393
|
|
|
607
|
-
## Edge
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
- **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.
|
|
612
|
-
- **User wants analysis only**: Suggest using `/canicode` instead — it runs analysis without the code generation phase.
|
|
613
|
-
- **User wants gotcha survey only**: Suggest using `/canicode-gotchas` instead — it runs the survey and saves answers as a persistent skill file.
|
|
614
|
-
- **Partial gotcha answers**: Apply only the answered questions. Skipped/n/a questions are neither applied nor annotated.
|
|
615
|
-
- **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.
|
|
616
|
-
- **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.
|
|
617
|
-
- **Very large design (many gotchas)**: The gotcha survey already deduplicates sibling nodes and filters to blocking/risk plus `missing-info` from info-collection rules (#406). If there are still many questions, ask the user if they want to focus on blocking issues only.
|
|
618
|
-
- **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.
|
|
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
|
+
|