canicode 0.11.0 → 0.11.2
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 +18 -6
- package/dist/cli/index.js +670 -188
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +41 -21
- package/dist/index.js +45 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +45 -8
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +62 -3
- package/package.json +2 -2
- package/skills/canicode/SKILL.md +6 -0
- package/skills/canicode-gotchas/SKILL.md +91 -66
- package/skills/canicode-roundtrip/SKILL.md +74 -248
- package/skills/canicode-roundtrip/canicode-roundtrip-helpers.d.ts +54 -0
- package/skills/canicode-roundtrip/helpers-bootstrap.js +21 -0
- package/skills/canicode-roundtrip/helpers-installer.js +14 -0
- package/skills/canicode-roundtrip/helpers.js +287 -17
- package/skills/cursor/canicode/SKILL.md +6 -0
- package/skills/cursor/canicode-gotchas/SKILL.md +91 -66
- package/skills/cursor/canicode-roundtrip/SKILL.md +74 -248
- package/skills/cursor/canicode-roundtrip/canicode-roundtrip-helpers.d.ts +54 -0
- package/skills/cursor/canicode-roundtrip/helpers-bootstrap.js +21 -0
- package/skills/cursor/canicode-roundtrip/helpers-installer.js +14 -0
- 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,17 @@ 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
|
+
|
|
37
|
+
### Debugging: MCP not available or Step 4 fails
|
|
38
|
+
|
|
39
|
+
Work through this matrix before concluding a server is broken. Full detail and the symptom → cause table are in [CUSTOMIZATION.md — Troubleshooting](https://github.com/let-sunny/canicode/blob/main/docs/CUSTOMIZATION.md#troubleshooting-mcp-not-available-or-roundtrip-step-4-fails).
|
|
40
|
+
|
|
41
|
+
1. **Settings + reload** — open **Settings → MCP**, confirm the server shows as enabled for this workspace, and reload MCP or restart the host. The live tool list (not the on-disk JSON) is what the model can actually call.
|
|
42
|
+
2. **Figma + canicode both present** — canicode provides `analyze` / `gotcha-survey`; Figma provides `use_figma`. A failure in Step 4 is a Figma MCP issue; a failure in Steps 1–3 is a canicode issue. Identify which server exposes the missing tool before editing config.
|
|
43
|
+
3. **Prepend + smoke check** — every `use_figma` `code` string must begin with one of the bundled helper artifacts. Preferred path (#424): prepend `helpers-installer.js` on the first batch and `helpers-bootstrap.js` on every subsequent batch — the installer caches the helpers source on `figma.root` via `setSharedPluginData`, and the bootstrap loads it back. Conservative single-artifact fallback: prepend `helpers.js` on every batch. In all paths, `typeof CanICodeRoundtrip === 'undefined'` after prepend means the bundle was not in the string. On the cache path, a `globalThis.__canicodeBootstrapResult` with `canicodeBootstrapResult: "cache-missing"` or `"version-mismatch"` means the agent must re-prepend `helpers-installer.js` on the next batch. See the Step 4 preflight block above for the exact prepend procedure and smoke-check snippet. This is distinct from "canicode MCP missing."
|
|
44
|
+
4. **Size / paste fallback** — if the host cannot pass the full code string (truncation or tool-payload limit), measure `Buffer.byteLength(code, "utf8")` (or `wc -c`) and, if too large, paste the code directly into the MCP `use_figma` UI instead of relying on the model to pass it inline. See [`docs/roundtrip-protocol.md`](https://github.com/let-sunny/canicode/blob/main/docs/roundtrip-protocol.md) for delivery notes.
|
|
45
|
+
|
|
33
46
|
### Step 1: Analyze the design
|
|
34
47
|
|
|
35
48
|
If the `analyze` MCP tool is available, call it with the user's Figma URL:
|
|
@@ -82,89 +95,50 @@ npx canicode gotcha-survey "<figma-url>" --json
|
|
|
82
95
|
|
|
83
96
|
If `questions` is empty, skip to **Step 6**.
|
|
84
97
|
|
|
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
|
-
```
|
|
98
|
+
#### Step 3 — preamble: match the user's language
|
|
119
99
|
|
|
120
|
-
|
|
100
|
+
Detect the user's conversation language from their recent messages in **this** session (Korean vs. English vs. other is usually unambiguous; when ambiguous, default to English and ask once). When the user's language is non-English, localize only the human-readable rendering of questions, `why`, `Hint:`, `Example:`, and the batch shared-prompt wording (including the `split` / `skip` / `n/a` affordance sentence). Keep identifiers and structural markers English: `ruleId`, `nodeId`, severity label in brackets, and the entire upsert-section markdown scaffolding (`## #NNN — …`, `Design key`, `#### Skipped (N)`) — downstream tools grep these, and `renderGotchaSection` is the source of truth for on-disk markdown (ADR-016). In the Appendix Step 3 upsert, pass the user's answer through **verbatim** into `answers[<nodeId>].answer`; do **not** back-translate — `figma-implement-design` is cross-language by design (#461). See `.claude/skills/canicode-gotchas/SKILL.md` Step 3 preamble for the full rule.
|
|
121
101
|
|
|
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
|
-
```
|
|
102
|
+
#### Step 3 — grouped survey (`groupedQuestions`)
|
|
125
103
|
|
|
126
|
-
|
|
104
|
+
Iterate `groupedQuestions.groups[].batches[]` and branch on `batch.batchMode` (`"safe"` — one uniform answer, `"opt-in"` — shared answer offered as default with per-node `split` override (#426), `"none"` — single-question). Instance notes, batch prompt templates per mode, replicas, split/skip/n/a, "skip remaining" early-exit affordance (surface before the first batch, re-surface every 3rd), 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.
|
|
127
105
|
|
|
128
|
-
```
|
|
129
|
-
**[{severity}] {batch.ruleId}** — {batch.questions.length} instances:
|
|
130
|
-
- {nodeName₁}{ruleSpecificContext₁}
|
|
131
|
-
- {nodeName₂}{ruleSpecificContext₂}
|
|
132
|
-
- …
|
|
133
106
|
|
|
134
|
-
|
|
107
|
+
### Step 4: Apply gotcha answers to Figma design
|
|
135
108
|
|
|
136
|
-
|
|
109
|
+
#### Mandatory preflight — prepend one of the bundled helpers before any `CanICodeRoundtrip.*` call
|
|
137
110
|
|
|
138
|
-
|
|
139
|
-
> Example: {example}
|
|
140
|
-
```
|
|
111
|
+
`CanICodeRoundtrip` is **not** a Figma or MCP built-in. It is the global registered by a bundled IIFE shipped next to this skill — it only exists after you read the right artifact and prepend its contents verbatim at the top of every `use_figma` script string. Skipping this step throws `ReferenceError: 'CanICodeRoundtrip' is not defined` on the first `use_figma` batch.
|
|
141
112
|
|
|
142
|
-
|
|
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.
|
|
113
|
+
Preferred protocol (cached delivery, #424, ADR-020) — **batch 1** prepends `helpers-installer.js` (roughly `helpers.js` size plus a small generated wrapper; one embedded copy of the source). **Batches 2+** prepend only the tiny bootstrap, so the session stays under `use_figma`'s ~50KB soft code-string budget instead of re-pasting the full helpers every time. The cache stores the verbatim helpers UTF-8 on `figma.root` via `setSharedPluginData`:
|
|
146
114
|
|
|
147
|
-
|
|
115
|
+
- **Batch 1 (install + optional smoke check):** prepend `helpers-installer.js`. It defines `CanICodeRoundtrip` for the current batch AND writes the helpers source + canicode version onto `figma.root` shared plugin data (namespace `"canicode"`, keys `"helpersSrc"` / `"helpersVersion"` — same bytes as `helpers.js`, not a second JSON wrapper).
|
|
116
|
+
- **Batches 2+ (bootstrap):** prepend `helpers-bootstrap.js`. It reads the cached source, version-checks it against the canicode version baked in at build time, and re-evals to register the global. The bootstrap is only a few hundred bytes.
|
|
117
|
+
- **Cross-session continuity:** shared plugin data persists with the file. On a later session against the same file, the agent MAY start straight on `helpers-bootstrap.js` — if the smoke check below returns `{ ok: true }` the cache is live for this canicode version. If the first bootstrap batch instead throws `ReferenceError` whose message starts with `canicode-bootstrap:` (or `globalThis.__canicodeBootstrapResult.canicodeBootstrapResult` is `"cache-missing"` or `"version-mismatch"`), re-prepend `helpers-installer.js` on the next batch and carry on.
|
|
118
|
+
- **Conservative fallback:** `helpers.js` still ships as the single-artifact option — prepend it on every batch when setSharedPluginData isn't available (host strips it) or when simplifying for a smoke-debugging session.
|
|
148
119
|
|
|
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
|
-
```
|
|
120
|
+
Artifact paths:
|
|
152
121
|
|
|
153
|
-
-
|
|
122
|
+
- **Claude Code / default `canicode init`:** `.claude/skills/canicode-roundtrip/helpers-installer.js`, `helpers-bootstrap.js`, and `helpers.js` (fallback).
|
|
123
|
+
- **Cursor after `canicode init --cursor-skills`:** `.cursor/skills/canicode-roundtrip/helpers-installer.js`, `helpers-bootstrap.js`, and `helpers.js` (fallback).
|
|
154
124
|
|
|
155
|
-
|
|
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
|
|
125
|
+
**Editor-only types (#473)** — `canicode-roundtrip-helpers.d.ts` (same directory) declares the `CanICodeRoundtrip` global for TypeScript-aware editors. It does not run in Figma. When drafting `use_figma` code in a `.ts` scratch file, add `/// <reference path="./canicode-roundtrip-helpers.d.ts" />` (adjust the relative path) so hover and completion match the bundled API surface.
|
|
160
126
|
|
|
161
|
-
|
|
127
|
+
Optional smoke check — run this as the first `use_figma` call of Step 4 (with the appropriate artifact prepended) before any real apply batch. The return shape is unchanged from the single-artifact protocol:
|
|
162
128
|
|
|
163
|
-
|
|
129
|
+
```javascript
|
|
130
|
+
// <contents of helpers-installer.js OR helpers-bootstrap.js (OR helpers.js fallback) prepended here>
|
|
131
|
+
return { ok: typeof CanICodeRoundtrip !== 'undefined' };
|
|
132
|
+
```
|
|
164
133
|
|
|
165
|
-
|
|
134
|
+
<!-- adr-016-ack: structured bootstrap marker is the agent-facing contract for the cache-miss / version-mismatch branch — example shows which field to read, not how to derive it -->
|
|
135
|
+
```javascript
|
|
136
|
+
// On a bootstrap batch, if the batch rejects with a canicode-bootstrap:* ReferenceError,
|
|
137
|
+
// inspect globalThis.__canicodeBootstrapResult and re-prepend helpers-installer.js:
|
|
138
|
+
// { canicodeBootstrapResult: "cache-missing" | "version-mismatch", expected, actual }
|
|
139
|
+
```
|
|
166
140
|
|
|
167
|
-
|
|
141
|
+
See [`docs/roundtrip-protocol.md` → Shared helpers (bundled)](https://github.com/let-sunny/canicode/blob/main/docs/roundtrip-protocol.md#shared-helpers-bundled) for the full helper catalogue and the cached-delivery subsection (ADR-016 — deterministic install + load logic lives in the bundled artifacts, not skill prose).
|
|
168
142
|
|
|
169
143
|
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.
|
|
170
144
|
|
|
@@ -186,120 +160,9 @@ Every gotcha-survey question (and every entry in `analyzeResult.issues[]`) carri
|
|
|
186
160
|
| `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
161
|
| `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
162
|
|
|
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 |
|
|
219
|
-
|
|
220
|
-
`upsertCanicodeAnnotation` wraps the write in `try/catch`: if `properties` fails node-type validation it retries without them, so the markdown body always survives. You can pass `properties` speculatively.
|
|
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.
|
|
163
|
+
#### Instance-child matrix, annotation enum matrix, write tiers, probe, helpers
|
|
223
164
|
|
|
224
|
-
|
|
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.
|
|
165
|
+
Full tables, Experiment 08/09 references, definition-write probe branches, the `suggestedDefaultApply` threshold heuristic for the picker (#428), 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
166
|
|
|
304
167
|
#### Strategy A: Property Modification — apply directly
|
|
305
168
|
|
|
@@ -330,50 +193,9 @@ Answer shape guide (LLM judgment — the user's answer is prose; parse according
|
|
|
330
193
|
|
|
331
194
|
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
195
|
|
|
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.
|
|
196
|
+
#### Strategy B: Structural modification
|
|
375
197
|
|
|
376
|
-
|
|
198
|
+
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
199
|
|
|
378
200
|
#### Strategy C: Annotation — record on the design for designer reference
|
|
379
201
|
|
|
@@ -418,7 +240,7 @@ const outcomes = await CanICodeRoundtrip.applyAutoFixes(analyzeResult.issues, {
|
|
|
418
240
|
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
241
|
2. **Present structural modifications** (Strategy B) one by one, apply confirmed ones.
|
|
420
242
|
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")
|
|
243
|
+
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
244
|
|
|
423
245
|
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
246
|
|
|
@@ -433,6 +255,14 @@ Applied {N} changes to the Figma design:
|
|
|
433
255
|
- 🔧 {nodeName}: auto-fixed to "Hover" (non-standard-naming)
|
|
434
256
|
- 📝 {nodeName}: annotation added to canicode:flag — raw color needs token binding (raw-value)
|
|
435
257
|
|
|
258
|
+
After each emoji line above, mirror a **structured per-item row** so scene-write vs annotation fallback is visible every run (#435):
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
{ruleId} @ {nodeName}
|
|
262
|
+
attempt: scene write (`question.targetProperty` / binding shape from answer)
|
|
263
|
+
result: {emoji outcome} ({short reason — e.g. silent-ignore ADR-012 → annotated, override-error → annotated, tier-2 propagated})
|
|
264
|
+
```
|
|
265
|
+
|
|
436
266
|
stepFourReport = {
|
|
437
267
|
resolved: <count of ✅ + 🔧 + 🔗 lines>, // scene writes, auto-fix renames, variable bindings
|
|
438
268
|
annotated: <count of 📝 lines>, // including ⏭️ declines that fell back to annotation
|
|
@@ -443,11 +273,15 @@ stepFourReport = {
|
|
|
443
273
|
|
|
444
274
|
Hold `stepFourReport` in scope through Step 5 — it is the input to `CanICodeRoundtrip.computeRoundtripTally` below.
|
|
445
275
|
|
|
276
|
+
#### Auto-chain acknowledgments after apply (#440)
|
|
277
|
+
|
|
278
|
+
**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.
|
|
279
|
+
|
|
446
280
|
### Step 5: Re-analyze and report what the roundtrip addressed
|
|
447
281
|
|
|
448
282
|
#### Step 5a: Harvest canicode-authored annotations as acknowledgments (#371)
|
|
449
283
|
|
|
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
|
|
284
|
+
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
285
|
|
|
452
286
|
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
287
|
|
|
@@ -482,7 +316,7 @@ The response now carries:
|
|
|
482
316
|
- `issues[i].acknowledged: true` (per matched issue) — survives into the report and downstream skills.
|
|
483
317
|
- `summary` text — when `acknowledgedCount > 0`, the Total line reads `Total: N (A acknowledged via canicode annotations / N-A unaddressed)`.
|
|
484
318
|
|
|
485
|
-
Under ADR-012's annotate-by-default policy,
|
|
319
|
+
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
320
|
|
|
487
321
|
**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
322
|
|
|
@@ -498,7 +332,7 @@ const tally = CanICodeRoundtrip.computeRoundtripTally({
|
|
|
498
332
|
|
|
499
333
|
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
334
|
|
|
501
|
-
**All gotcha issues resolved** (`V == 0`, i.e. re-analyze surfaces no remaining issues
|
|
335
|
+
**All gotcha issues resolved** (`V == 0`, i.e. re-analyze surfaces no remaining issues):
|
|
502
336
|
- Tell the user (fill in the counts from the tally above):
|
|
503
337
|
|
|
504
338
|
```
|
|
@@ -510,7 +344,7 @@ If Step 4 produced no `stepFourReport` (e.g. user skipped every question, or no
|
|
|
510
344
|
—
|
|
511
345
|
V issues remaining (unresolved gotchas + non-actionable rules)
|
|
512
346
|
|
|
513
|
-
|
|
347
|
+
Ready for code generation. *(Optional:) Report still shows grade **{grade}** — informational only.*
|
|
514
348
|
```
|
|
515
349
|
- 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
350
|
<!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside removeCanicodeAnnotations -->
|
|
@@ -542,16 +376,16 @@ for (const id of nodeIds) {
|
|
|
542
376
|
↳ V_ack acknowledged via canicode annotations (carried into code-gen)
|
|
543
377
|
↳ V_open unaddressed (no annotation — your follow-up backlog)
|
|
544
378
|
|
|
545
|
-
|
|
379
|
+
Proceed to code generation with remaining context? *(Optional footnote: report grade **{grade}**.)*
|
|
546
380
|
```
|
|
547
381
|
|
|
548
382
|
When `V_ack == 0` (re-analyze returned `acknowledgedCount: 0`), keep the single `V issues remaining (unresolved gotchas + non-actionable rules)` line.
|
|
549
383
|
- If yes → proceed to **Step 6** with remaining gotcha context.
|
|
550
|
-
- If no → stop and emit the **Stop wrap-up** below;
|
|
384
|
+
- If no → stop and emit the **Stop wrap-up** below; lead with the delta, not grade.
|
|
551
385
|
|
|
552
386
|
#### Wrap-up message rubric (Stop branch)
|
|
553
387
|
|
|
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`)
|
|
388
|
+
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
389
|
|
|
556
390
|
```
|
|
557
391
|
Stopped — N issues addressed, V remaining for manual follow-up:
|
|
@@ -564,10 +398,10 @@ Stopped — N issues addressed, V remaining for manual follow-up:
|
|
|
564
398
|
↳ V_ack acknowledged via canicode annotations
|
|
565
399
|
↳ V_open unaddressed
|
|
566
400
|
|
|
567
|
-
|
|
401
|
+
*(Optional)* Report grade: **{grade}**.
|
|
568
402
|
```
|
|
569
403
|
|
|
570
|
-
When `V_ack == 0`, drop the `↳` lines and leave a single `V remaining` row. Anti-pattern
|
|
404
|
+
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
405
|
|
|
572
406
|
### Step 6: Implement with Figma MCP
|
|
573
407
|
|
|
@@ -585,7 +419,7 @@ Follow the **figma-implement-design** skill workflow to generate code from the F
|
|
|
585
419
|
|
|
586
420
|
#### Wrap-up message rubric (post-handoff)
|
|
587
421
|
|
|
588
|
-
After `figma-implement-design` returns, summarise the roundtrip in the same shape as the Step 5 / Stop wrap-up — issues-delta first,
|
|
422
|
+
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
423
|
|
|
590
424
|
```
|
|
591
425
|
Roundtrip complete — N issues addressed, code generated:
|
|
@@ -598,21 +432,13 @@ Roundtrip complete — N issues addressed, code generated:
|
|
|
598
432
|
↳ V_ack acknowledged via canicode annotations
|
|
599
433
|
↳ V_open unaddressed
|
|
600
434
|
|
|
601
|
-
|
|
435
|
+
*(Optional)* Report grade: **{grade}**.
|
|
602
436
|
Code: <files generated / next-step pointer from figma-implement-design>
|
|
603
437
|
```
|
|
604
438
|
|
|
605
439
|
(Drop the `↳` lines when `V_ack == 0`.)
|
|
606
440
|
|
|
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.
|
|
441
|
+
## Edge cases
|
|
442
|
+
|
|
443
|
+
Full list — **[Appendix Edge Cases](https://github.com/let-sunny/canicode/blob/main/docs/roundtrip-protocol.md#appendix--edge-cases-full-list)**.
|
|
444
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor-only ambient types for the bundled roundtrip IIFE global (#473).
|
|
3
|
+
* Figma's plugin sandbox never loads this file; it exists so agents and humans
|
|
4
|
+
* get autocomplete when authoring `use_figma` batches in a TypeScript-aware
|
|
5
|
+
* editor. Signatures are intentionally loose (`unknown`) — tighten against
|
|
6
|
+
* `src/core/roundtrip/*.ts` when a surface changes.
|
|
7
|
+
*
|
|
8
|
+
* Optional: `/// <reference path="./canicode-roundtrip-helpers.d.ts" />` at the
|
|
9
|
+
* top of a scratch `.ts` file next to pasted batch code (path relative to that file).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type CicUnknown = unknown;
|
|
13
|
+
type CicRecord = Record<string, CicUnknown>;
|
|
14
|
+
|
|
15
|
+
/** Public surface registered by `helpers.js` / installer + bootstrap eval. */
|
|
16
|
+
interface CanICodeRoundtripGlobal {
|
|
17
|
+
stripAnnotations(annotations: readonly CicUnknown[]): CicUnknown[];
|
|
18
|
+
ensureCanicodeCategories(): Promise<CicRecord>;
|
|
19
|
+
upsertCanicodeAnnotation(node: CicRecord, input: CicRecord): boolean;
|
|
20
|
+
applyWithInstanceFallback(
|
|
21
|
+
question: CicRecord,
|
|
22
|
+
writeFn: (target: CicRecord) => CicUnknown,
|
|
23
|
+
context?: CicRecord,
|
|
24
|
+
): Promise<CicRecord>;
|
|
25
|
+
applyPropertyMod(
|
|
26
|
+
question: CicRecord,
|
|
27
|
+
answerValue: CicUnknown,
|
|
28
|
+
context?: CicRecord,
|
|
29
|
+
): Promise<CicRecord>;
|
|
30
|
+
resolveVariableByName(name: string): Promise<CicRecord | null>;
|
|
31
|
+
probeDefinitionWritability(questions: readonly CicRecord[]): Promise<CicRecord>;
|
|
32
|
+
extractAcknowledgmentsFromNode(
|
|
33
|
+
node: CicRecord,
|
|
34
|
+
canicodeCategoryIds?: ReadonlySet<string>,
|
|
35
|
+
): CicRecord[];
|
|
36
|
+
readCanicodeAcknowledgments(
|
|
37
|
+
rootNodeId: string,
|
|
38
|
+
categories?: CicRecord,
|
|
39
|
+
): Promise<CicRecord[]>;
|
|
40
|
+
computeRoundtripTally(args: CicRecord): CicRecord;
|
|
41
|
+
applyAutoFix(issue: CicRecord, context: CicRecord): Promise<CicRecord>;
|
|
42
|
+
applyAutoFixes(issues: readonly CicRecord[], context: CicRecord): Promise<CicRecord[]>;
|
|
43
|
+
isCanicodeAnnotation(annotation: CicRecord, categories: CicRecord): boolean;
|
|
44
|
+
removeCanicodeAnnotations(
|
|
45
|
+
annotations: readonly CicUnknown[],
|
|
46
|
+
categories: CicRecord,
|
|
47
|
+
): CicUnknown[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare global {
|
|
51
|
+
var CanICodeRoundtrip: CanICodeRoundtripGlobal;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// canicode-roundtrip helpers bootstrap (auto-generated — see scripts/bundle-roundtrip-cache.ts)
|
|
2
|
+
// Prepend to every use_figma batch AFTER the installer batch. Loads the cached helpers source
|
|
3
|
+
// from figma.root shared plugin data, version-checks it against the baked-in canicode version,
|
|
4
|
+
// and evals to register globalThis.CanICodeRoundtrip (#424, ADR-020). On cache-miss or
|
|
5
|
+
// version-mismatch, surfaces { canicodeBootstrapResult, expected, actual } on
|
|
6
|
+
// globalThis.__canicodeBootstrapResult and throws ReferenceError so the agent re-prepends the
|
|
7
|
+
// installer on the next batch.
|
|
8
|
+
(function __canicodeBootstrap() {
|
|
9
|
+
var expected = "0.11.2";
|
|
10
|
+
var src = figma.root.getSharedPluginData("canicode", "helpersSrc");
|
|
11
|
+
var actual = figma.root.getSharedPluginData("canicode", "helpersVersion");
|
|
12
|
+
if (!src) {
|
|
13
|
+
globalThis.__canicodeBootstrapResult = { canicodeBootstrapResult: "cache-missing", expected: expected, actual: actual || null };
|
|
14
|
+
throw new ReferenceError("canicode-bootstrap:cache-missing (expected " + expected + ") — re-prepend helpers-installer.js");
|
|
15
|
+
}
|
|
16
|
+
if (actual !== expected) {
|
|
17
|
+
globalThis.__canicodeBootstrapResult = { canicodeBootstrapResult: "version-mismatch", expected: expected, actual: actual };
|
|
18
|
+
throw new ReferenceError("canicode-bootstrap:version-mismatch (expected " + expected + ", actual " + actual + ") — re-prepend helpers-installer.js");
|
|
19
|
+
}
|
|
20
|
+
(0, eval)(src);
|
|
21
|
+
})();
|