canicode 0.11.1 → 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 +2 -2
- package/dist/cli/index.js +165 -183
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +22 -5
- package/dist/index.js +26 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +22 -5
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +38 -2
- package/package.json +2 -2
- package/skills/canicode-gotchas/SKILL.md +53 -7
- package/skills/canicode-roundtrip/SKILL.md +49 -2
- 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/cursor/canicode-gotchas/SKILL.md +53 -7
- package/skills/cursor/canicode-roundtrip/SKILL.md +49 -2
- 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/docs/CUSTOMIZATION.md
CHANGED
|
@@ -220,11 +220,20 @@ Cursor may show an MCP **server id** in the UI that is **not** literally the key
|
|
|
220
220
|
- If roundtrip fails with “cannot find `use_figma`” but Figma MCP shows as connected, open the tool list and note the **exact** tool identifier Cursor exposes (it may be namespaced).
|
|
221
221
|
- Keep the Figma MCP entry in `.cursor/mcp.json` (or `~/.cursor/mcp.json`) per [Figma’s MCP docs](https://developers.figma.com/docs/figma-mcp-server/) — then rely on the host-reported tool name when wiring skills or debugging.
|
|
222
222
|
|
|
223
|
+
### Post-install checklist (MCP + skills + Figma) (#461)
|
|
224
|
+
|
|
225
|
+
After editing MCP JSON or running `canicode init`:
|
|
226
|
+
|
|
227
|
+
1. **Restart the host** (Cursor or Claude Code) or use the product’s **reload MCP** action — on-disk config is not live until the next session.
|
|
228
|
+
2. **Confirm the live tool list** (Cursor: Settings → MCP; Claude: tool picker) — the JSON file is not proof; the model can only call tools the host exposes.
|
|
229
|
+
3. **Figma** — for MCP-driven canvas access, complete any **Connect / file access** steps your Figma account requires so `use_figma` can run against the target file.
|
|
230
|
+
4. **Community / read-only files** — roundtrip Step 4 writes to the file. If the design is read-only, duplicate it into a workspace you own before running `/canicode-roundtrip` or `@canicode-roundtrip`.
|
|
231
|
+
|
|
223
232
|
### Gotcha survey in Cursor
|
|
224
233
|
|
|
225
234
|
1. Add the MCP config above and restart Cursor (or reload MCP).
|
|
226
|
-
2.
|
|
227
|
-
3. In chat,
|
|
235
|
+
2. Install skills: **`canicode init --token … --cursor-skills`** saves the Figma token and installs everything; or **`canicode init --cursor-skills`** without `--token` installs Claude skills plus Cursor copies — you still need **`canicode init --token …`** (or `FIGMA_TOKEN`) before REST **`analyze`** / **`gotcha-survey`** against a live URL. The shared gotchas answer file lives under `.claude/skills/canicode-gotchas/` when the gotchas skill is installed. Authoring is single-source under `.claude/skills/` in the repo; the npm build writes `skills/cursor/` (gotchas strip `# Collected Gotchas`; other skills are full copies).
|
|
236
|
+
3. In chat, **@-mention** **canicode**, **canicode-gotchas**, or **canicode-roundtrip** when your Cursor setup uses Agent-attached skills. Some teams prefer **slash commands** or rules instead of `@` — use whichever your workspace enables; roundtrip still needs **`use_figma`** from the Figma MCP in that session.
|
|
228
237
|
|
|
229
238
|
### Manual test checklist (#407)
|
|
230
239
|
|
|
@@ -234,6 +243,33 @@ Cursor may show an MCP **server id** in the UI that is **not** literally the key
|
|
|
234
243
|
- [ ] After the Q&A loop, `npx canicode upsert-gotcha-section --file … --design-key … --input=-` (JSON payload on stdin per `canicode-gotchas` Step 4b) succeeds and updates `.claude/skills/canicode-gotchas/SKILL.md`.
|
|
235
244
|
- [ ] Optional: @ **canicode-roundtrip** — Step 4 reads `helpers.js` from `.cursor/skills/canicode-roundtrip/helpers.js` after `canicode init --cursor-skills`.
|
|
236
245
|
|
|
246
|
+
### Troubleshooting: MCP not available or roundtrip Step 4 fails
|
|
247
|
+
|
|
248
|
+
Work through these checks in order before concluding that an MCP server is broken.
|
|
249
|
+
|
|
250
|
+
1. **Cursor host — ensure the server is enabled and the tools appear in the live session.**
|
|
251
|
+
MCP servers are not always active immediately after install. Open **Settings → MCP** and verify the server shows a connected status. If the entry is present but the tools are missing from the active tool list, use the reload MCP action (or restart Cursor). The live tool list — not the on-disk JSON — is what the model can actually call.
|
|
252
|
+
|
|
253
|
+
2. **Two separate servers — identify which step failed before attributing blame.**
|
|
254
|
+
canicode (provides `analyze` and `gotcha-survey`) and Figma (provides `use_figma`) are independent MCP servers. Step 1 (analyze) and Step 3 (gotcha-survey) use the canicode server. Step 4 (canvas write) uses the Figma server. A failure in Step 4 is a Figma MCP issue, not a canicode issue — and vice versa. Check which server exposes the missing tool before editing config.
|
|
255
|
+
|
|
256
|
+
3. **Step 4 (canvas write) — `helpers.js` must be prepended to every `use_figma` `code` string.**
|
|
257
|
+
`CanICodeRoundtrip` is not a Figma built-in. It is the global registered by the bundled IIFE in `helpers.js` (at `.claude/skills/canicode-roundtrip/helpers.js` for Claude Code, or `.cursor/skills/canicode-roundtrip/helpers.js` after `canicode init --cursor-skills`). If the bundle is missing from the string, the first Plugin API call throws `ReferenceError: CanICodeRoundtrip is not defined`. This is **not** the same as "canicode MCP is missing" — the canicode server may be healthy; only the Step 4 `use_figma` script is incomplete. Smoke-check with:
|
|
258
|
+
```javascript
|
|
259
|
+
// <contents of helpers.js prepended here>
|
|
260
|
+
return { ok: typeof CanICodeRoundtrip !== 'undefined' };
|
|
261
|
+
```
|
|
262
|
+
See the Step 4 preflight block in [`docs/roundtrip-protocol.md`](https://github.com/let-sunny/canicode/blob/main/docs/roundtrip-protocol.md) and the corresponding preflight check in the `canicode-roundtrip` SKILL.md for the full prepend procedure.
|
|
263
|
+
|
|
264
|
+
4. **Size / delivery — measure the code string before assuming a server bug.**
|
|
265
|
+
If the `use_figma` call silently fails, truncates, or the host reports the tool payload is too large, the `helpers.js` bundle combined with the apply script may exceed the host's per-message or per-tool-call limit. Measure with `Buffer.byteLength(code, "utf8")` (Node) or `wc -c` (shell). If the string is too large for the chat/tool input, write it to a file and paste it into the MCP `use_figma` UI directly instead of relying on the model to pass the full string inline. See also the delivery notes in [#462](https://github.com/let-sunny/canicode/issues/462) and [`docs/roundtrip-protocol.md`](https://github.com/let-sunny/canicode/blob/main/docs/roundtrip-protocol.md).
|
|
266
|
+
|
|
267
|
+
5. **Common symptom → likely cause mapping:**
|
|
268
|
+
- `ReferenceError: CanICodeRoundtrip is not defined` → `helpers.js` was not prepended (check 3 above).
|
|
269
|
+
- Truncated response or no server round-trip returned → delivery size issue (check 4 above).
|
|
270
|
+
- Permission error or "cannot edit" → Figma file seat or edit access; a Viewer seat cannot write via Plugin API.
|
|
271
|
+
- Tools missing from live tool list despite on-disk config → MCP server not enabled or not restarted (check 1 above).
|
|
272
|
+
|
|
237
273
|
---
|
|
238
274
|
|
|
239
275
|
## Telemetry
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canicode",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"mcpName": "io.github.let-sunny/canicode",
|
|
5
5
|
"description": "Lint Figma designs for AI code-gen and roundtrip the answers back into the file. CLI + MCP server.",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsup --config tsup.config.ts && pnpm build:roundtrip && pnpm bundle:skills",
|
|
21
21
|
"build:web": "bash scripts/build-web.sh",
|
|
22
|
-
"build:roundtrip": "tsup --config tsup.roundtrip.config.ts",
|
|
22
|
+
"build:roundtrip": "tsup --config tsup.roundtrip.config.ts && tsx scripts/bundle-roundtrip-cache.ts",
|
|
23
23
|
"bundle:skills": "bash scripts/bundle-skills.sh",
|
|
24
24
|
"dev": "tsup --watch",
|
|
25
25
|
"test": "vitest",
|
|
@@ -52,13 +52,40 @@ If `isReadyForCodeGen` is `true` or `questions` is empty:
|
|
|
52
52
|
- Do NOT write to `.claude/skills/canicode-gotchas/SKILL.md`.
|
|
53
53
|
- Stop here.
|
|
54
54
|
|
|
55
|
+
### Step 3 — preamble: match the user's language
|
|
56
|
+
|
|
57
|
+
Before rendering any question, 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 the user once which language they prefer.
|
|
58
|
+
|
|
59
|
+
When the user's language is non-English, localize only the **human-readable** strings rendered in the prompt templates below: the `question` text, the `why` line (if shown), the `Hint:` body, the `Example:` body, and the batch shared-prompt wording — including the "Reply with one answer to apply to all …, or **split** to answer each individually" sentence and the `skip` / `n/a` affordance sentence that follows it. Translate at render time only; the rule templates in `core/rules/*` stay English-only per CLAUDE.md and the issue's "Out of scope" list — do not rewrite source.
|
|
60
|
+
|
|
61
|
+
Keep the following English even when localizing, because they are identifiers or structural markers that downstream tools grep for: `ruleId`, `nodeId`, `nodeName`, the severity label in brackets (`[blocking]`, `[risk]`, `[missing-info]`, `[suggestion]`), and the entire markdown scaffolding of the Step 4 upsert section (`## #NNN — …` headings, `Design key`, `#### Skipped (N)`, the per-record field labels). `renderGotchaSection` is the source of truth for that on-disk markdown (ADR-016) and its output stays English.
|
|
62
|
+
|
|
63
|
+
In Step 4, pass the user's answer through **verbatim** into the `answers[<nodeId>].answer` field — do **not** back-translate answers to English. `figma-implement-design` is cross-language by design (see #461), and a round-trip to English introduces translation loss and defeats the "shared language for designer/PM" framing.
|
|
64
|
+
|
|
55
65
|
### Step 3: Present questions to the user
|
|
56
66
|
|
|
57
|
-
The survey response carries a pre-computed `groupedQuestions.groups[].batches[]` shape so this skill never has to sort, partition, or maintain a batchable-rule whitelist in prose. The sort key, `_no-source` sentinel, and batchable-rule
|
|
67
|
+
The survey response carries a pre-computed `groupedQuestions.groups[].batches[]` shape so this skill never has to sort, partition, or maintain a batchable-rule whitelist in prose. The sort key, `_no-source` sentinel, and both batchable-rule lists (`BATCHABLE_RULE_IDS` for `safe` mode, `OPT_IN_BATCHABLE_RULE_IDS` for `opt-in` mode) all live in `core/gotcha/group-and-batch-questions.ts` with vitest coverage (per ADR-016). Iterate over it:
|
|
68
|
+
|
|
69
|
+
**Before presenting the first batch**, display this shortcut notice once so the user knows they can exit early at any point:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Survey: {totalBatchCount} question(s) to answer.
|
|
73
|
+
Tip: reply `skip remaining` at any point to bypass the rest with a default no-op annotation and finish immediately.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Where `totalBatchCount` is `groupedQuestions.groups.flatMap((g) => g.batches).length`.
|
|
77
|
+
|
|
78
|
+
**After every 3rd batch** (i.e. after batches 3, 6, 9, …), re-surface the shortcut as a brief reminder before presenting the next batch:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
(You can still reply `skip remaining` to bypass the remaining questions.)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
When the user replies `skip remaining` at any point during Step 3, immediately treat all unanswered batches as skipped (`{ "skipped": true }` for each unanswered `nodeId`) and proceed directly to Step 4 without asking further questions.
|
|
58
85
|
|
|
59
|
-
For every `batch` in `groupedQuestions.groups.flatMap((g) => g.batches)`:
|
|
86
|
+
For every `batch` in `groupedQuestions.groups.flatMap((g) => g.batches)`, branch on `batch.batchMode`:
|
|
60
87
|
|
|
61
|
-
-
|
|
88
|
+
- **`batch.batchMode === "none"`** — single-question batch; the helper guarantees `batch.questions.length === 1`. Render the standard prompt for `batch.questions[0]`:
|
|
62
89
|
|
|
63
90
|
```
|
|
64
91
|
**[{severity}] {ruleId}** — node: {nodeName}
|
|
@@ -69,7 +96,7 @@ For every `batch` in `groupedQuestions.groups.flatMap((g) => g.batches)`:
|
|
|
69
96
|
> Example: {example}
|
|
70
97
|
```
|
|
71
98
|
|
|
72
|
-
-
|
|
99
|
+
- **`batch.batchMode === "safe"` with `batch.questions.length >= 2`** (#369) — rule in `BATCHABLE_RULE_IDS`; one answer is uniformly applicable. Render one shared prompt:
|
|
73
100
|
|
|
74
101
|
```
|
|
75
102
|
**[{severity}] {ruleId}** — {batch.questions.length} instances:
|
|
@@ -87,13 +114,32 @@ For every `batch` in `groupedQuestions.groups.flatMap((g) => g.batches)`:
|
|
|
87
114
|
|
|
88
115
|
Where `sharedQuestionPrompt` reuses 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 the singular phrasing N times).
|
|
89
116
|
|
|
90
|
-
-
|
|
117
|
+
- **`batch.batchMode === "opt-in"` with `batch.questions.length >= 2`** (#426) — rule in `OPT_IN_BATCHABLE_RULE_IDS` (currently `missing-prototype`). The same answer is usually shareable across siblings but may legitimately differ per node — signal that explicitly so the user can opt out of the shared answer with `split`:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
**[{severity}] {batch.ruleId}** — {batch.questions.length} instances of the same rule:
|
|
121
|
+
- {nodeName₁}
|
|
122
|
+
- {nodeName₂}
|
|
123
|
+
- …
|
|
124
|
+
|
|
125
|
+
{sharedQuestionPrompt}
|
|
126
|
+
|
|
127
|
+
Apply this answer to all {batch.questions.length} occurrences of `{batch.ruleId}`, or reply **split** to answer each individually.
|
|
128
|
+
|
|
129
|
+
> Hint: {hint}
|
|
130
|
+
> Example: {example}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Unlike `safe` batches, the prompt frames the answer as a suggested default, not a uniform truth — reuse the rule's existing `example` (e.g. `missing-prototype`'s "navigates to `/product/{id}` detail page") so the user knows the answer can be a pattern, not a literal string shared character-for-character.
|
|
134
|
+
|
|
135
|
+
- **Single-member `safe` or `opt-in` batch (`batch.questions.length === 1`)** — render the single-question template above; the shared-prompt framing collapses to the rule's own wording when there is only one node.
|
|
91
136
|
|
|
92
137
|
Wait for the user's answer before moving to the next batch. The user may:
|
|
93
|
-
- Answer the question / batch directly
|
|
94
|
-
- Say **split** (batch only) to fall back to per-question prompting for that batch
|
|
138
|
+
- Answer the question / batch directly (single value or pattern covers all batch members)
|
|
139
|
+
- Say **split** (batch only) to fall back to per-question prompting for that batch — works the same for both `safe` and `opt-in` batches
|
|
95
140
|
- Say **skip** to skip the question / the entire batch
|
|
96
141
|
- Say **n/a** if the question / the entire batch is not applicable
|
|
142
|
+
- Say **skip remaining** to immediately skip all remaining unanswered batches and proceed to Step 4
|
|
97
143
|
|
|
98
144
|
When applying the batched answer, expand back to per-question records in Step 4 — the gotcha section format stores one record per `nodeId`.
|
|
99
145
|
|
|
@@ -34,6 +34,15 @@ See the Edge Case **No Figma MCP server** below for the one-way fallback when Fi
|
|
|
34
34
|
|
|
35
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
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
|
+
|
|
37
46
|
### Step 1: Analyze the design
|
|
38
47
|
|
|
39
48
|
If the `analyze` MCP tool is available, call it with the user's Figma URL:
|
|
@@ -86,13 +95,51 @@ npx canicode gotcha-survey "<figma-url>" --json
|
|
|
86
95
|
|
|
87
96
|
If `questions` is empty, skip to **Step 6**.
|
|
88
97
|
|
|
98
|
+
#### Step 3 — preamble: match the user's language
|
|
99
|
+
|
|
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.
|
|
101
|
+
|
|
89
102
|
#### Step 3 — grouped survey (`groupedQuestions`)
|
|
90
103
|
|
|
91
|
-
Iterate `groupedQuestions.groups[].batches[]
|
|
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.
|
|
92
105
|
|
|
93
106
|
|
|
94
107
|
### Step 4: Apply gotcha answers to Figma design
|
|
95
108
|
|
|
109
|
+
#### Mandatory preflight — prepend one of the bundled helpers before any `CanICodeRoundtrip.*` call
|
|
110
|
+
|
|
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.
|
|
112
|
+
|
|
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`:
|
|
114
|
+
|
|
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.
|
|
119
|
+
|
|
120
|
+
Artifact paths:
|
|
121
|
+
|
|
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).
|
|
124
|
+
|
|
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.
|
|
126
|
+
|
|
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:
|
|
128
|
+
|
|
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
|
+
```
|
|
133
|
+
|
|
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
|
+
```
|
|
140
|
+
|
|
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).
|
|
142
|
+
|
|
96
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.
|
|
97
144
|
|
|
98
145
|
Use the **`nodeId` from the answered question**. When `question.isInstanceChild` is `true`, treat layout and size-constraint changes as **high impact**: applying them on the source definition affects **every instance** of that component in the file. Ask for explicit user confirmation before writing to the definition node.
|
|
@@ -115,7 +162,7 @@ Every gotcha-survey question (and every entry in `analyzeResult.issues[]`) carri
|
|
|
115
162
|
|
|
116
163
|
#### Instance-child matrix, annotation enum matrix, write tiers, probe, helpers
|
|
117
164
|
|
|
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).
|
|
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).
|
|
119
166
|
|
|
120
167
|
#### Strategy A: Property Modification — apply directly
|
|
121
168
|
|
|
@@ -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
|
+
})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// canicode-roundtrip helpers installer (auto-generated — see scripts/bundle-roundtrip-cache.ts)
|
|
2
|
+
// Prepend to the FIRST use_figma batch of a roundtrip session. Caches the helpers source on
|
|
3
|
+
// figma.root via setSharedPluginData so subsequent batches can prepend the much smaller
|
|
4
|
+
// helpers-bootstrap.js instead of re-pasting ~31KB every call (#424, ADR-020).
|
|
5
|
+
var __CANICODE_HELPERS_SRC__ = "var CanICodeRoundtrip = (function (exports) {\n 'use strict';\n\n // src/core/roundtrip/annotations.ts\n function stripAnnotations(annotations) {\n const input = annotations ?? [];\n const out = [];\n for (const a of input) {\n const hasLM = typeof a.labelMarkdown === \"string\" && a.labelMarkdown.length > 0;\n const hasLabel = typeof a.label === \"string\" && a.label.length > 0;\n if (!hasLM && !hasLabel) continue;\n const base = hasLM ? { labelMarkdown: a.labelMarkdown } : { label: a.label };\n if (a.categoryId) base.categoryId = a.categoryId;\n if (Array.isArray(a.properties) && a.properties.length > 0) {\n base.properties = a.properties;\n }\n out.push(base);\n }\n return out;\n }\n async function ensureCanicodeCategories() {\n const api = figma.annotations;\n const existing = await api.getAnnotationCategoriesAsync();\n const byLabel = new Map(existing.map((c) => [c.label, c.id]));\n async function ensure(label, color) {\n const cached = byLabel.get(label);\n if (cached) return cached;\n const created = await api.addAnnotationCategoryAsync({ label, color });\n byLabel.set(label, created.id);\n return created.id;\n }\n const result = {\n gotcha: await ensure(\"canicode:gotcha\", \"blue\"),\n flag: await ensure(\"canicode:flag\", \"green\"),\n fallback: await ensure(\"canicode:fallback\", \"yellow\")\n };\n const legacyAutoFix = byLabel.get(\"canicode:auto-fix\");\n if (legacyAutoFix) result.legacyAutoFix = legacyAutoFix;\n return result;\n }\n function upsertCanicodeAnnotation(node, input) {\n if (!node || !(\"annotations\" in node)) return false;\n const { ruleId, markdown, categoryId, properties } = input;\n const legacyPrefix = `**[canicode] ${ruleId}**`;\n const footer = `\\u2014 *${ruleId}*`;\n let bodyText = markdown;\n if (bodyText.startsWith(legacyPrefix)) {\n bodyText = bodyText.slice(legacyPrefix.length).replace(/^\\s*\\n+/, \"\");\n }\n const trimmed = bodyText.replace(/\\s+$/, \"\");\n const body = trimmed.endsWith(footer) ? trimmed : `${trimmed}\n\n${footer}`;\n const existing = stripAnnotations(node.annotations);\n const entry = { labelMarkdown: body };\n if (categoryId) entry.categoryId = categoryId;\n if (properties && properties.length > 0) entry.properties = properties;\n const matchesRuleId = (text) => {\n if (typeof text !== \"string\") return false;\n return text.startsWith(legacyPrefix) || text.includes(footer);\n };\n const idx = existing.findIndex(\n (a) => matchesRuleId(a.labelMarkdown) || matchesRuleId(a.label)\n );\n if (idx >= 0) existing[idx] = entry;\n else existing.push(entry);\n try {\n node.annotations = existing;\n return true;\n } catch (e) {\n const msg = String(e?.message ?? e);\n const isNodeTypeReject = /invalid property .+ for a .+ node/i.test(msg);\n if (!entry.properties || !isNodeTypeReject) throw e;\n delete entry.properties;\n if (idx >= 0) existing[idx] = entry;\n node.annotations = existing;\n return true;\n }\n }\n\n // src/core/roundtrip/annotation-payload.ts\n var CANICODE_JSON_FENCE = \"```canicode-json\";\n function formatIntentValueForDisplay(value) {\n if (value === void 0) return \"undefined\";\n if (value === null) return \"null\";\n if (typeof value === \"object\") {\n try {\n return `\\`${JSON.stringify(value)}\\``;\n } catch {\n return String(value);\n }\n }\n return `\\`${String(value)}\\``;\n }\n function buildCodegenDirective(args) {\n const { sceneNodeId, intent } = args;\n const val = intent.value === void 0 ? \"undefined\" : JSON.stringify(intent.value);\n return `When generating code for node ${sceneNodeId}, set ${intent.field} to ${val} (user intent, scope: ${intent.scope}). Prefer this over the current Figma scene value when they disagree.`;\n }\n function sceneOutcomeToAck(result, reason) {\n return reason !== void 0 ? { result, reason } : { result };\n }\n function buildOutcomeHumanLine(args) {\n if (args.skippedDefinitionDueToAdr012) {\n const adrHint = \" Canicode skipped writing the source component without `allowDefinitionWrite: true` (ADR-012 safer default). The instance-level change did not apply as intended in the scene.\";\n if (args.reason === \"silent-ignore\") {\n return \"**Scene write outcome:** The write ran, but the property value did not change on this instance (silent-ignore).\" + adrHint;\n }\n return \"**Scene write outcome:** Figma rejected an instance-level change\" + (args.errorMessage ? `: ${args.errorMessage}` : \"\") + \".\" + adrHint;\n }\n if (args.reason === \"silent-ignore\") {\n return \"**Scene write outcome:** The write ran, but the property value did not change on this instance (silent-ignore). No source definition was available to escalate.\";\n }\n if (args.reason === \"override-error\") {\n return \"**Scene write outcome:** Figma rejected an instance-level change\" + (args.errorMessage ? `: ${args.errorMessage}` : \"\") + \". No source definition was available to escalate.\";\n }\n return \"**Scene write outcome:** Could not apply automatically\" + (args.errorMessage ? `: ${args.errorMessage}` : \"\") + \".\";\n }\n function buildAdr012PropagationParagraph(args) {\n const { componentName, replicaCount } = args;\n const fanOutHint = typeof replicaCount === \"number\" && replicaCount >= 2 ? ` This batched question covers ${replicaCount} instance scenes \\u2014 changing **${componentName}** at the definition still affects every inheriting instance, not just one row in the batch.` : \"\";\n return `Canicode's safer default (ADR-012) is to skip writing the source component **${componentName}** without explicit opt-in, because that write propagates to every non-overridden instance of **${componentName}** in the file.${fanOutHint} Prefer a manual override on **this** instance when you only need a local fix. Use \\`allowDefinitionWrite: true\\` only when you intend to change **${componentName}** for all inheriting instances \\u2014 it is not a neutral shortcut for a single-instance tweak.`;\n }\n function buildDefinitionWriteSkippedBody(args) {\n const {\n ruleId,\n sceneNodeId,\n componentName,\n reason,\n errorMessage,\n replicaCount,\n intent\n } = args;\n const ackIntent = intent ? {\n field: intent.field,\n value: intent.value,\n scope: intent.scope\n } : void 0;\n const sceneWriteOutcome = sceneOutcomeToAck(\"user-declined-propagation\", \"adr-012-opt-in-disabled\");\n const codegenDirective = intent !== void 0 ? buildCodegenDirective({ sceneNodeId, intent }) : void 0;\n const jsonBlock = {\n v: 1,\n ruleId,\n nodeId: sceneNodeId,\n ...ackIntent ? { intent: ackIntent } : {},\n sceneWriteOutcome,\n ...codegenDirective ? { codegenDirective } : {}\n };\n const userAnswerLine = intent !== void 0 ? `**User answered:** ${formatIntentValueForDisplay(intent.value)} for **${intent.field}** (scope: ${intent.scope}).` : null;\n const outcomeLine = buildOutcomeHumanLine({\n reason,\n ...errorMessage !== void 0 ? { errorMessage } : {},\n skippedDefinitionDueToAdr012: true\n });\n const adrBlock = buildAdr012PropagationParagraph({\n componentName,\n ...replicaCount !== void 0 ? { replicaCount } : {}\n });\n const proseParts = [userAnswerLine, outcomeLine, adrBlock].filter(\n (p) => p !== null\n );\n const prose = proseParts.join(\"\\n\\n\");\n return appendJsonFenceAndFooter(prose, jsonBlock, ruleId);\n }\n function buildNoDefinitionFallbackBody(args) {\n const { ruleId, sceneNodeId, reason, errorMessage, intent } = args;\n const ackIntent = intent ? { field: intent.field, value: intent.value, scope: intent.scope } : void 0;\n const outcomeResult = reason === \"silent-ignore\" ? \"silent-ignored\" : reason === \"override-error\" ? \"api-rejected\" : \"api-rejected\";\n const sceneWriteOutcome = sceneOutcomeToAck(\n outcomeResult,\n reason === \"silent-ignore\" ? \"silent-ignore-no-definition\" : \"no-definition-escalation\"\n );\n const codegenDirective = intent !== void 0 ? buildCodegenDirective({ sceneNodeId, intent }) : void 0;\n const jsonBlock = {\n v: 1,\n ruleId,\n nodeId: sceneNodeId,\n ...ackIntent ? { intent: ackIntent } : {},\n sceneWriteOutcome,\n ...codegenDirective ? { codegenDirective } : {}\n };\n const userAnswerLine = intent !== void 0 ? `**User answered:** ${formatIntentValueForDisplay(intent.value)} for **${intent.field}** (scope: ${intent.scope}).` : null;\n const outcomeLine = buildOutcomeHumanLine({\n reason,\n ...errorMessage !== void 0 ? { errorMessage } : {},\n skippedDefinitionDueToAdr012: false\n });\n const prose = [userAnswerLine, outcomeLine].filter((p) => p !== null).join(\"\\n\\n\");\n return appendJsonFenceAndFooter(prose, jsonBlock, ruleId);\n }\n function buildDefinitionTierFailureBody(args) {\n const { ruleId, sceneNodeId, intent, kind, errorMessage } = args;\n const sceneWriteOutcome = sceneOutcomeToAck(\n kind === \"read-only-library\" ? \"api-rejected\" : \"api-rejected\",\n kind === \"read-only-library\" ? \"definition-read-only\" : \"definition-write-failed\"\n );\n const codegenDirective = intent !== void 0 ? buildCodegenDirective({ sceneNodeId, intent }) : void 0;\n const jsonBlock = {\n v: 1,\n ruleId,\n nodeId: sceneNodeId,\n ...intent ? {\n intent: {\n field: intent.field,\n value: intent.value,\n scope: intent.scope\n }\n } : {},\n sceneWriteOutcome,\n ...codegenDirective ? { codegenDirective } : {}\n };\n const human = kind === \"read-only-library\" ? \"source component lives in an external library and is read-only from this file \\u2014 apply the fix in the library file itself.\" : `could not apply at source definition: ${errorMessage}`;\n const userAnswerLine = intent !== void 0 ? `**User answered:** ${formatIntentValueForDisplay(intent.value)} for **${intent.field}** (scope: ${intent.scope}).` : null;\n const outcomeLine = `**Scene write outcome:** ${human}`;\n const prose = [userAnswerLine, outcomeLine].filter((p) => p !== null).join(\"\\n\\n\");\n return appendJsonFenceAndFooter(prose, jsonBlock, ruleId);\n }\n function appendJsonFenceAndFooter(prose, jsonBlock, ruleId) {\n const footer = `\\u2014 *${ruleId}*`;\n const hasIntent = jsonBlock.intent !== void 0;\n if (!hasIntent) {\n return `${prose}\n\n${footer}`;\n }\n const jsonText = JSON.stringify(jsonBlock, null, 0);\n return `${prose}\n\n${CANICODE_JSON_FENCE}\n${jsonText}\n\\`\\`\\`\n\n${footer}`;\n }\n var FENCED_JSON_RE = new RegExp(\n `${CANICODE_JSON_FENCE.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}\\\\s*([\\\\s\\\\S]*?)\\\\s*\\`\\`\\``,\n \"m\"\n );\n function parseCanicodeJsonPayloadFromMarkdown(text) {\n const m = FENCED_JSON_RE.exec(text);\n if (!m?.[1]) return void 0;\n try {\n const raw = JSON.parse(m[1].trim());\n if (!raw || typeof raw !== \"object\") return void 0;\n const o = raw;\n if (o.v !== 1 || typeof o.ruleId !== \"string\") return void 0;\n return raw;\n } catch {\n return void 0;\n }\n }\n\n // src/core/roundtrip/apply-with-instance-fallback.ts\n var DEFINITION_WRITE_SKIPPED_EVENT = \"cic_roundtrip_definition_write_skipped\";\n function categoryIdForAnnotate(categories, kind, roundtripIntent) {\n if (kind === \"adr012-definition-skipped\") {\n return categories.fallback;\n }\n if (roundtripIntent !== void 0) {\n return categories.gotcha;\n }\n return categories.flag;\n }\n function resolveSourceComponentName(definition, question) {\n if (definition && typeof definition.name === \"string\" && definition.name) {\n return definition.name;\n }\n const ic = question.instanceContext;\n if (ic && typeof ic.sourceComponentName === \"string\" && ic.sourceComponentName) {\n return ic.sourceComponentName;\n }\n return \"the source component\";\n }\n async function routeToDefinitionOrAnnotate(definition, writeFn, ctx) {\n if (definition && !ctx.allowDefinitionWrite && ctx.reason !== \"non-override-error\") {\n const componentName = resolveSourceComponentName(definition, ctx.question);\n const replicaCount = typeof ctx.question.replicas === \"number\" && Number.isInteger(ctx.question.replicas) ? ctx.question.replicas : void 0;\n if (ctx.categories) {\n upsertCanicodeAnnotation(ctx.scene, {\n ruleId: ctx.question.ruleId,\n markdown: buildDefinitionWriteSkippedBody({\n ruleId: ctx.question.ruleId,\n sceneNodeId: ctx.scene.id,\n componentName,\n reason: ctx.reason,\n ...ctx.errorMessage !== void 0 ? { errorMessage: ctx.errorMessage } : {},\n ...replicaCount !== void 0 ? { replicaCount } : {},\n ...ctx.roundtripIntent !== void 0 ? { intent: ctx.roundtripIntent } : {}\n }),\n categoryId: categoryIdForAnnotate(\n ctx.categories,\n \"adr012-definition-skipped\",\n ctx.roundtripIntent\n )\n });\n }\n ctx.telemetry?.(DEFINITION_WRITE_SKIPPED_EVENT, {\n ruleId: ctx.question.ruleId,\n reason: ctx.reason\n });\n return {\n icon: \"\\u{1F4DD}\",\n label: \"definition write skipped (opt-in disabled)\"\n };\n }\n if (!definition) {\n if (ctx.categories) {\n const markdown = buildNoDefinitionFallbackBody({\n ruleId: ctx.question.ruleId,\n sceneNodeId: ctx.scene.id,\n reason: ctx.reason,\n ...ctx.errorMessage !== void 0 ? { errorMessage: ctx.errorMessage } : {},\n ...ctx.roundtripIntent !== void 0 ? { intent: ctx.roundtripIntent } : {}\n });\n upsertCanicodeAnnotation(ctx.scene, {\n ruleId: ctx.question.ruleId,\n markdown,\n categoryId: categoryIdForAnnotate(\n ctx.categories,\n \"other-failure\",\n ctx.roundtripIntent\n )\n });\n }\n return ctx.reason === \"silent-ignore\" ? { icon: \"\\u{1F4DD}\", label: \"silent-ignore, annotated\" } : { icon: \"\\u{1F4DD}\", label: `error: ${ctx.errorMessage ?? \"\"}` };\n }\n try {\n await writeFn(definition);\n return {\n icon: \"\\u{1F310}\",\n label: ctx.reason === \"silent-ignore\" ? \"source definition (silent-ignore fallback)\" : \"source definition\"\n };\n } catch (defErr) {\n const defMsg = String(defErr?.message ?? defErr);\n const isRemoteReadOnly = definition.remote === true || /read-only/i.test(defMsg);\n if (ctx.categories) {\n upsertCanicodeAnnotation(ctx.scene, {\n ruleId: ctx.question.ruleId,\n markdown: buildDefinitionTierFailureBody({\n ruleId: ctx.question.ruleId,\n sceneNodeId: ctx.scene.id,\n ...ctx.roundtripIntent !== void 0 ? { intent: ctx.roundtripIntent } : {},\n kind: isRemoteReadOnly ? \"read-only-library\" : \"definition-error\",\n errorMessage: defMsg\n }),\n categoryId: categoryIdForAnnotate(\n ctx.categories,\n \"other-failure\",\n ctx.roundtripIntent\n )\n });\n }\n return {\n icon: \"\\u{1F4DD}\",\n label: isRemoteReadOnly ? \"external library (read-only)\" : `definition error: ${defMsg}`\n };\n }\n }\n async function applyWithInstanceFallback(question, writeFn, context = {}) {\n const { categories, allowDefinitionWrite = false, telemetry, roundtripIntent } = context;\n const scene = await figma.getNodeByIdAsync(question.nodeId);\n if (!scene) return { icon: \"\\u{1F4DD}\", label: \"missing node\" };\n const definition = question.sourceChildId ? await figma.getNodeByIdAsync(question.sourceChildId) : null;\n try {\n const changed = await writeFn(scene);\n if (changed === false) {\n return routeToDefinitionOrAnnotate(definition, writeFn, {\n question,\n scene,\n categories,\n reason: \"silent-ignore\",\n allowDefinitionWrite,\n telemetry,\n ...roundtripIntent !== void 0 ? { roundtripIntent } : {}\n });\n }\n return { icon: \"\\u2705\", label: \"instance/scene\" };\n } catch (e) {\n const msg = String(e?.message ?? e);\n const looksLikeInstanceOverride = /cannot be overridden/i.test(msg) || /override/i.test(msg);\n if (!looksLikeInstanceOverride) {\n return routeToDefinitionOrAnnotate(null, writeFn, {\n question,\n scene,\n categories,\n reason: \"non-override-error\",\n errorMessage: msg,\n allowDefinitionWrite,\n telemetry,\n ...roundtripIntent !== void 0 ? { roundtripIntent } : {}\n });\n }\n return routeToDefinitionOrAnnotate(definition, writeFn, {\n question,\n scene,\n categories,\n reason: \"override-error\",\n errorMessage: msg,\n allowDefinitionWrite,\n telemetry,\n ...roundtripIntent !== void 0 ? { roundtripIntent } : {}\n });\n }\n }\n\n // src/core/roundtrip/apply-property-mod.ts\n async function resolveVariableByName(name) {\n const locals = await figma.variables.getLocalVariablesAsync();\n return locals.find((v) => v.name === name) ?? null;\n }\n function parseValue(raw) {\n if (raw && typeof raw === \"object\" && \"variable\" in raw) {\n const v = raw;\n const parsed = { kind: \"binding\", name: v.variable };\n if (\"fallback\" in v) parsed.fallback = v.fallback;\n return parsed;\n }\n if (raw && typeof raw === \"object\" && \"fallback\" in raw) {\n return { kind: \"scalar\", scalar: raw.fallback };\n }\n return { kind: \"scalar\", scalar: raw };\n }\n function isPaintProp(prop) {\n return prop === \"fills\" || prop === \"strokes\";\n }\n function applyPropertyBinding(target, prop, variable) {\n if (isPaintProp(prop)) {\n const current = target[prop];\n if (current === figma.mixed || !Array.isArray(current)) return false;\n const paints = current;\n const bound = paints.map(\n (paint) => figma.variables.setBoundVariableForPaint(paint, \"color\", variable)\n );\n target[prop] = bound;\n return true;\n }\n target.setBoundVariable(prop, variable);\n return true;\n }\n function buildRoundtripIntentFromPropertyAnswer(question, answerValue) {\n const raw = question.targetProperty;\n if (raw === void 0) return void 0;\n const props = Array.isArray(raw) ? raw : [raw];\n if (props.length === 0) return void 0;\n if (props.length === 1) {\n const prop = props[0];\n const perProp = answerValue && typeof answerValue === \"object\" && !(\"variable\" in answerValue) && !Array.isArray(answerValue) ? answerValue[prop] : answerValue;\n const parsed = parseValueForIntent(perProp);\n if (parsed === void 0) return void 0;\n return { field: prop, value: parsed, scope: \"instance\" };\n }\n const obj = answerValue && typeof answerValue === \"object\" && !(\"variable\" in answerValue) && !Array.isArray(answerValue) ? answerValue : void 0;\n const picked = {};\n for (const p of props) {\n if (obj && p in obj && obj[p] !== void 0) picked[p] = obj[p];\n }\n if (Object.keys(picked).length === 0) return void 0;\n return {\n field: props.join(\", \"),\n value: picked,\n scope: \"instance\"\n };\n }\n function parseValueForIntent(raw) {\n if (raw && typeof raw === \"object\" && \"variable\" in raw) {\n return { variable: raw.variable };\n }\n if (raw && typeof raw === \"object\" && \"fallback\" in raw) {\n return raw.fallback;\n }\n return raw;\n }\n function applyPropertyScalar(target, prop, scalar) {\n const rec = target;\n const before = rec[prop];\n rec[prop] = scalar;\n if (rec[prop] === before && before !== scalar) return false;\n return true;\n }\n async function applyPropertyMod(question, answerValue, context = {}) {\n const roundtripIntent = buildRoundtripIntentFromPropertyAnswer(\n question,\n answerValue\n );\n const props = Array.isArray(question.targetProperty) ? question.targetProperty : question.targetProperty !== void 0 ? [question.targetProperty] : [];\n return applyWithInstanceFallback(\n question,\n async (target) => {\n if (!target) return void 0;\n let changed = void 0;\n for (const prop of props) {\n if (!(prop in target)) continue;\n const perProp = answerValue && typeof answerValue === \"object\" && !(\"variable\" in answerValue) && !Array.isArray(answerValue) ? answerValue[prop] : answerValue;\n const parsed = parseValue(perProp);\n if (parsed.kind === \"binding\") {\n const variable = await resolveVariableByName(parsed.name);\n if (variable) {\n applyPropertyBinding(target, prop, variable);\n continue;\n }\n if (parsed.fallback !== void 0) {\n if (!applyPropertyScalar(target, prop, parsed.fallback)) {\n changed = false;\n }\n }\n continue;\n }\n if (parsed.scalar === void 0) continue;\n if (!applyPropertyScalar(target, prop, parsed.scalar)) {\n changed = false;\n }\n }\n return changed;\n },\n {\n ...context,\n ...roundtripIntent !== void 0 ? { roundtripIntent } : {}\n }\n );\n }\n\n // src/core/roundtrip/probe-definition-writability.ts\n async function probeDefinitionWritability(questions) {\n const verdict = /* @__PURE__ */ new Map();\n const unwritableNames = [];\n const seenName = /* @__PURE__ */ new Set();\n for (const q of questions) {\n const id = q.sourceChildId;\n if (!id) continue;\n if (verdict.has(id)) continue;\n const node = await figma.getNodeByIdAsync(id);\n const writability = resolveWritability(node);\n const isUnwritable = writability.isUnwritable;\n verdict.set(id, isUnwritable ? \"unwritable\" : \"writable\");\n if (isUnwritable) {\n const name = typeof writability.componentName === \"string\" && writability.componentName || typeof node?.name === \"string\" && node.name || q.instanceContext?.sourceComponentName || id;\n if (!seenName.has(name)) {\n seenName.add(name);\n unwritableNames.push(name);\n }\n }\n }\n const totalCount = verdict.size;\n let unwritableCount = 0;\n for (const v of verdict.values()) if (v === \"unwritable\") unwritableCount++;\n return {\n totalCount,\n unwritableCount,\n unwritableSourceNames: unwritableNames,\n allUnwritable: totalCount > 0 && unwritableCount === totalCount,\n partiallyUnwritable: unwritableCount > 0 && unwritableCount < totalCount\n };\n }\n function resolveWritability(node) {\n if (node === null) return { isUnwritable: true };\n if (\"remote\" in node && typeof node.remote === \"boolean\") {\n return { isUnwritable: node.remote === true };\n }\n const containing = findContainingComponent(node);\n if (!containing) {\n return { isUnwritable: false };\n }\n const isUnwritable = \"remote\" in containing && containing.remote === true;\n return {\n isUnwritable,\n ...isUnwritable && typeof containing.name === \"string\" ? { componentName: containing.name } : {}\n };\n }\n function findContainingComponent(node) {\n let cur = node;\n for (let i = 0; i < 100 && cur; i++) {\n if (cur.type === \"COMPONENT\" || cur.type === \"COMPONENT_SET\") return cur;\n cur = cur.parent ?? null;\n }\n return null;\n }\n\n // src/core/roundtrip/read-acknowledgments.ts\n var FOOTER_RE = /—\\s+\\*([A-Za-z0-9-]+)\\*\\s*$/;\n var LEGACY_PREFIX_RE = /^\\*\\*\\[canicode\\]\\s+([A-Za-z0-9-]+)\\*\\*/;\n function extractAcknowledgmentsFromNode(node, canicodeCategoryIds) {\n if (!node || !(\"annotations\" in node)) return [];\n const annotations = node.annotations ?? [];\n if (annotations.length === 0) return [];\n const out = [];\n for (const a of annotations) {\n const text = (typeof a.labelMarkdown === \"string\" && a.labelMarkdown.length > 0 ? a.labelMarkdown : \"\") || (typeof a.label === \"string\" && a.label.length > 0 ? a.label : \"\");\n if (!text) continue;\n if (canicodeCategoryIds) {\n if (!a.categoryId || !canicodeCategoryIds.has(a.categoryId)) continue;\n }\n const ruleId = extractRuleId(text);\n if (!ruleId) continue;\n const payload = parseCanicodeJsonPayloadFromMarkdown(text);\n const payloadAligned = payload && payload.ruleId === ruleId;\n out.push({\n nodeId: node.id,\n ruleId,\n ...payloadAligned && payload.intent ? { intent: payload.intent } : {},\n ...payloadAligned && payload.sceneWriteOutcome ? { sceneWriteOutcome: payload.sceneWriteOutcome } : {},\n ...payloadAligned && payload.codegenDirective ? { codegenDirective: payload.codegenDirective } : {}\n });\n }\n return out;\n }\n function extractRuleId(text) {\n const footer = FOOTER_RE.exec(text);\n if (footer) return footer[1] ?? null;\n const legacy = LEGACY_PREFIX_RE.exec(text);\n if (legacy) return legacy[1] ?? null;\n return null;\n }\n async function readCanicodeAcknowledgments(rootNodeId, categories) {\n const root = await figma.getNodeByIdAsync(rootNodeId);\n if (!root) return [];\n const canicodeCategoryIds = categories ? new Set(\n [\n categories.gotcha,\n categories.flag,\n categories.fallback,\n categories.legacyAutoFix\n ].filter((id) => typeof id === \"string\" && id.length > 0)\n ) : void 0;\n const out = [];\n walk(root, canicodeCategoryIds, out);\n return out;\n }\n function safeChildren(node) {\n try {\n const c = node.children;\n return Array.isArray(c) ? c : [];\n } catch {\n return [];\n }\n }\n function walk(node, canicodeCategoryIds, out) {\n try {\n const local = extractAcknowledgmentsFromNode(node, canicodeCategoryIds);\n for (const a of local) out.push(a);\n } catch {\n }\n for (const child of safeChildren(node)) {\n if (child && typeof child === \"object\") walk(child, canicodeCategoryIds, out);\n }\n }\n\n // src/core/roundtrip/compute-roundtrip-tally.ts\n function computeRoundtripTally(args) {\n const { stepFourReport, reanalyzeResponse } = args;\n const { resolved, annotated, definitionWritten, skipped } = stepFourReport;\n const { issueCount, acknowledgedCount } = reanalyzeResponse;\n if (acknowledgedCount > issueCount) {\n throw new Error(\n `computeRoundtripTally: reanalyzeResponse.acknowledgedCount (${acknowledgedCount}) cannot exceed issueCount (${issueCount}). Acknowledged issues are a subset of remaining issues.`\n );\n }\n return {\n X: resolved,\n Y: annotated,\n Z: definitionWritten,\n W: skipped,\n N: resolved + annotated + definitionWritten + skipped,\n V: issueCount,\n V_ack: acknowledgedCount,\n V_open: issueCount - acknowledgedCount\n };\n }\n\n // src/core/roundtrip/apply-auto-fix.ts\n function pickNodeName(issue, resolved) {\n if (resolved && typeof resolved.name === \"string\" && resolved.name.length > 0) {\n return resolved.name;\n }\n if (typeof issue.nodePath === \"string\" && issue.nodePath.length > 0) {\n const segments = issue.nodePath.split(/\\s*[›>/]\\s*/);\n const tail = segments[segments.length - 1];\n if (tail && tail.length > 0) return tail;\n }\n return issue.nodeId;\n }\n function mapInstanceFallbackIcon(result) {\n if (result.icon === \"\\u2705\") return \"\\u{1F527}\";\n return result.icon;\n }\n async function applyAutoFix(issue, context) {\n const { categories } = context;\n const ruleId = issue.ruleId;\n if (issue.targetProperty === \"name\" && typeof issue.suggestedName === \"string\") {\n const suggestedName = issue.suggestedName;\n const question = {\n nodeId: issue.nodeId,\n ruleId,\n ...issue.sourceChildId ? { sourceChildId: issue.sourceChildId } : {}\n };\n const result = await applyWithInstanceFallback(\n question,\n (target) => {\n if (target) {\n target.name = suggestedName;\n }\n },\n {\n categories,\n ...context.allowDefinitionWrite !== void 0 ? { allowDefinitionWrite: context.allowDefinitionWrite } : {},\n ...context.telemetry !== void 0 ? { telemetry: context.telemetry } : {}\n }\n );\n const sceneAfter = await figma.getNodeByIdAsync(issue.nodeId);\n return {\n outcome: mapInstanceFallbackIcon(result),\n nodeId: issue.nodeId,\n nodeName: pickNodeName(issue, sceneAfter),\n ruleId,\n label: result.label\n };\n }\n const scene = await figma.getNodeByIdAsync(issue.nodeId);\n const markdown = issue.message ?? `Auto-flagged: ${ruleId}`;\n if (scene) {\n upsertCanicodeAnnotation(scene, {\n ruleId,\n markdown,\n categoryId: categories.flag,\n ...issue.annotationProperties && issue.annotationProperties.length > 0 ? { properties: issue.annotationProperties } : {}\n });\n }\n return {\n outcome: \"\\u{1F4DD}\",\n nodeId: issue.nodeId,\n nodeName: pickNodeName(issue, scene),\n ruleId,\n label: scene ? `annotation added to canicode:flag \\u2014 ${ruleId}` : `missing node (annotation skipped) \\u2014 ${ruleId}`\n };\n }\n async function applyAutoFixes(issues, context) {\n const out = [];\n for (const issue of issues) {\n if (issue.applyStrategy !== \"auto-fix\") {\n out.push({\n outcome: \"\\u23ED\\uFE0F\",\n nodeId: issue.nodeId,\n nodeName: pickNodeName(issue, null),\n ruleId: issue.ruleId,\n label: `skipped \\u2014 applyStrategy is ${issue.applyStrategy ?? \"absent\"}`\n });\n continue;\n }\n out.push(await applyAutoFix(issue, context));\n }\n return out;\n }\n\n // src/core/roundtrip/remove-canicode-annotations.ts\n var LEGACY_CANICODE_PREFIX = \"**[canicode]\";\n function isCanicodeAnnotation(annotation, categories) {\n const canicodeIds = new Set(\n [\n categories.gotcha,\n categories.flag,\n categories.fallback,\n categories.legacyAutoFix\n ].filter((id) => Boolean(id))\n );\n if (annotation.categoryId && canicodeIds.has(annotation.categoryId)) {\n return true;\n }\n if (annotation.labelMarkdown?.startsWith(LEGACY_CANICODE_PREFIX)) {\n return true;\n }\n return false;\n }\n function removeCanicodeAnnotations(annotations, categories) {\n return annotations.filter((a) => !isCanicodeAnnotation(a, categories));\n }\n\n exports.applyAutoFix = applyAutoFix;\n exports.applyAutoFixes = applyAutoFixes;\n exports.applyPropertyMod = applyPropertyMod;\n exports.applyWithInstanceFallback = applyWithInstanceFallback;\n exports.computeRoundtripTally = computeRoundtripTally;\n exports.ensureCanicodeCategories = ensureCanicodeCategories;\n exports.extractAcknowledgmentsFromNode = extractAcknowledgmentsFromNode;\n exports.isCanicodeAnnotation = isCanicodeAnnotation;\n exports.probeDefinitionWritability = probeDefinitionWritability;\n exports.readCanicodeAcknowledgments = readCanicodeAcknowledgments;\n exports.removeCanicodeAnnotations = removeCanicodeAnnotations;\n exports.resolveVariableByName = resolveVariableByName;\n exports.stripAnnotations = stripAnnotations;\n exports.upsertCanicodeAnnotation = upsertCanicodeAnnotation;\n\n return exports;\n\n})({});\n";
|
|
6
|
+
var __CANICODE_HELPERS_VERSION__ = "0.11.2";
|
|
7
|
+
(0, eval)(__CANICODE_HELPERS_SRC__);
|
|
8
|
+
try {
|
|
9
|
+
figma.root.setSharedPluginData("canicode", "helpersSrc", __CANICODE_HELPERS_SRC__);
|
|
10
|
+
figma.root.setSharedPluginData("canicode", "helpersVersion", __CANICODE_HELPERS_VERSION__);
|
|
11
|
+
globalThis.__canicodeInstallResult = { cachePersisted: true };
|
|
12
|
+
} catch (err) {
|
|
13
|
+
globalThis.__canicodeInstallResult = { cachePersisted: false, reason: String((err && err.message) || err) };
|
|
14
|
+
}
|
|
@@ -52,13 +52,40 @@ If `isReadyForCodeGen` is `true` or `questions` is empty:
|
|
|
52
52
|
- Do NOT write to `.claude/skills/canicode-gotchas/SKILL.md`.
|
|
53
53
|
- Stop here.
|
|
54
54
|
|
|
55
|
+
### Step 3 — preamble: match the user's language
|
|
56
|
+
|
|
57
|
+
Before rendering any question, 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 the user once which language they prefer.
|
|
58
|
+
|
|
59
|
+
When the user's language is non-English, localize only the **human-readable** strings rendered in the prompt templates below: the `question` text, the `why` line (if shown), the `Hint:` body, the `Example:` body, and the batch shared-prompt wording — including the "Reply with one answer to apply to all …, or **split** to answer each individually" sentence and the `skip` / `n/a` affordance sentence that follows it. Translate at render time only; the rule templates in `core/rules/*` stay English-only per CLAUDE.md and the issue's "Out of scope" list — do not rewrite source.
|
|
60
|
+
|
|
61
|
+
Keep the following English even when localizing, because they are identifiers or structural markers that downstream tools grep for: `ruleId`, `nodeId`, `nodeName`, the severity label in brackets (`[blocking]`, `[risk]`, `[missing-info]`, `[suggestion]`), and the entire markdown scaffolding of the Step 4 upsert section (`## #NNN — …` headings, `Design key`, `#### Skipped (N)`, the per-record field labels). `renderGotchaSection` is the source of truth for that on-disk markdown (ADR-016) and its output stays English.
|
|
62
|
+
|
|
63
|
+
In Step 4, pass the user's answer through **verbatim** into the `answers[<nodeId>].answer` field — do **not** back-translate answers to English. `figma-implement-design` is cross-language by design (see #461), and a round-trip to English introduces translation loss and defeats the "shared language for designer/PM" framing.
|
|
64
|
+
|
|
55
65
|
### Step 3: Present questions to the user
|
|
56
66
|
|
|
57
|
-
The survey response carries a pre-computed `groupedQuestions.groups[].batches[]` shape so this skill never has to sort, partition, or maintain a batchable-rule whitelist in prose. The sort key, `_no-source` sentinel, and batchable-rule
|
|
67
|
+
The survey response carries a pre-computed `groupedQuestions.groups[].batches[]` shape so this skill never has to sort, partition, or maintain a batchable-rule whitelist in prose. The sort key, `_no-source` sentinel, and both batchable-rule lists (`BATCHABLE_RULE_IDS` for `safe` mode, `OPT_IN_BATCHABLE_RULE_IDS` for `opt-in` mode) all live in `core/gotcha/group-and-batch-questions.ts` with vitest coverage (per ADR-016). Iterate over it:
|
|
68
|
+
|
|
69
|
+
**Before presenting the first batch**, display this shortcut notice once so the user knows they can exit early at any point:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Survey: {totalBatchCount} question(s) to answer.
|
|
73
|
+
Tip: reply `skip remaining` at any point to bypass the rest with a default no-op annotation and finish immediately.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Where `totalBatchCount` is `groupedQuestions.groups.flatMap((g) => g.batches).length`.
|
|
77
|
+
|
|
78
|
+
**After every 3rd batch** (i.e. after batches 3, 6, 9, …), re-surface the shortcut as a brief reminder before presenting the next batch:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
(You can still reply `skip remaining` to bypass the remaining questions.)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
When the user replies `skip remaining` at any point during Step 3, immediately treat all unanswered batches as skipped (`{ "skipped": true }` for each unanswered `nodeId`) and proceed directly to Step 4 without asking further questions.
|
|
58
85
|
|
|
59
|
-
For every `batch` in `groupedQuestions.groups.flatMap((g) => g.batches)`:
|
|
86
|
+
For every `batch` in `groupedQuestions.groups.flatMap((g) => g.batches)`, branch on `batch.batchMode`:
|
|
60
87
|
|
|
61
|
-
-
|
|
88
|
+
- **`batch.batchMode === "none"`** — single-question batch; the helper guarantees `batch.questions.length === 1`. Render the standard prompt for `batch.questions[0]`:
|
|
62
89
|
|
|
63
90
|
```
|
|
64
91
|
**[{severity}] {ruleId}** — node: {nodeName}
|
|
@@ -69,7 +96,7 @@ For every `batch` in `groupedQuestions.groups.flatMap((g) => g.batches)`:
|
|
|
69
96
|
> Example: {example}
|
|
70
97
|
```
|
|
71
98
|
|
|
72
|
-
-
|
|
99
|
+
- **`batch.batchMode === "safe"` with `batch.questions.length >= 2`** (#369) — rule in `BATCHABLE_RULE_IDS`; one answer is uniformly applicable. Render one shared prompt:
|
|
73
100
|
|
|
74
101
|
```
|
|
75
102
|
**[{severity}] {ruleId}** — {batch.questions.length} instances:
|
|
@@ -87,13 +114,32 @@ For every `batch` in `groupedQuestions.groups.flatMap((g) => g.batches)`:
|
|
|
87
114
|
|
|
88
115
|
Where `sharedQuestionPrompt` reuses 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 the singular phrasing N times).
|
|
89
116
|
|
|
90
|
-
-
|
|
117
|
+
- **`batch.batchMode === "opt-in"` with `batch.questions.length >= 2`** (#426) — rule in `OPT_IN_BATCHABLE_RULE_IDS` (currently `missing-prototype`). The same answer is usually shareable across siblings but may legitimately differ per node — signal that explicitly so the user can opt out of the shared answer with `split`:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
**[{severity}] {batch.ruleId}** — {batch.questions.length} instances of the same rule:
|
|
121
|
+
- {nodeName₁}
|
|
122
|
+
- {nodeName₂}
|
|
123
|
+
- …
|
|
124
|
+
|
|
125
|
+
{sharedQuestionPrompt}
|
|
126
|
+
|
|
127
|
+
Apply this answer to all {batch.questions.length} occurrences of `{batch.ruleId}`, or reply **split** to answer each individually.
|
|
128
|
+
|
|
129
|
+
> Hint: {hint}
|
|
130
|
+
> Example: {example}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Unlike `safe` batches, the prompt frames the answer as a suggested default, not a uniform truth — reuse the rule's existing `example` (e.g. `missing-prototype`'s "navigates to `/product/{id}` detail page") so the user knows the answer can be a pattern, not a literal string shared character-for-character.
|
|
134
|
+
|
|
135
|
+
- **Single-member `safe` or `opt-in` batch (`batch.questions.length === 1`)** — render the single-question template above; the shared-prompt framing collapses to the rule's own wording when there is only one node.
|
|
91
136
|
|
|
92
137
|
Wait for the user's answer before moving to the next batch. The user may:
|
|
93
|
-
- Answer the question / batch directly
|
|
94
|
-
- Say **split** (batch only) to fall back to per-question prompting for that batch
|
|
138
|
+
- Answer the question / batch directly (single value or pattern covers all batch members)
|
|
139
|
+
- Say **split** (batch only) to fall back to per-question prompting for that batch — works the same for both `safe` and `opt-in` batches
|
|
95
140
|
- Say **skip** to skip the question / the entire batch
|
|
96
141
|
- Say **n/a** if the question / the entire batch is not applicable
|
|
142
|
+
- Say **skip remaining** to immediately skip all remaining unanswered batches and proceed to Step 4
|
|
97
143
|
|
|
98
144
|
When applying the batched answer, expand back to per-question records in Step 4 — the gotcha section format stores one record per `nodeId`.
|
|
99
145
|
|
|
@@ -34,6 +34,15 @@ See the Edge Case **No Figma MCP server** below for the one-way fallback when Fi
|
|
|
34
34
|
|
|
35
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
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
|
+
|
|
37
46
|
### Step 1: Analyze the design
|
|
38
47
|
|
|
39
48
|
If the `analyze` MCP tool is available, call it with the user's Figma URL:
|
|
@@ -86,13 +95,51 @@ npx canicode gotcha-survey "<figma-url>" --json
|
|
|
86
95
|
|
|
87
96
|
If `questions` is empty, skip to **Step 6**.
|
|
88
97
|
|
|
98
|
+
#### Step 3 — preamble: match the user's language
|
|
99
|
+
|
|
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.
|
|
101
|
+
|
|
89
102
|
#### Step 3 — grouped survey (`groupedQuestions`)
|
|
90
103
|
|
|
91
|
-
Iterate `groupedQuestions.groups[].batches[]
|
|
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.
|
|
92
105
|
|
|
93
106
|
|
|
94
107
|
### Step 4: Apply gotcha answers to Figma design
|
|
95
108
|
|
|
109
|
+
#### Mandatory preflight — prepend one of the bundled helpers before any `CanICodeRoundtrip.*` call
|
|
110
|
+
|
|
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.
|
|
112
|
+
|
|
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`:
|
|
114
|
+
|
|
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.
|
|
119
|
+
|
|
120
|
+
Artifact paths:
|
|
121
|
+
|
|
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).
|
|
124
|
+
|
|
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.
|
|
126
|
+
|
|
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:
|
|
128
|
+
|
|
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
|
+
```
|
|
133
|
+
|
|
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
|
+
```
|
|
140
|
+
|
|
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).
|
|
142
|
+
|
|
96
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.
|
|
97
144
|
|
|
98
145
|
Use the **`nodeId` from the answered question**. When `question.isInstanceChild` is `true`, treat layout and size-constraint changes as **high impact**: applying them on the source definition affects **every instance** of that component in the file. Ask for explicit user confirmation before writing to the definition node.
|
|
@@ -115,7 +162,7 @@ Every gotcha-survey question (and every entry in `analyzeResult.issues[]`) carri
|
|
|
115
162
|
|
|
116
163
|
#### Instance-child matrix, annotation enum matrix, write tiers, probe, helpers
|
|
117
164
|
|
|
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).
|
|
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).
|
|
119
166
|
|
|
120
167
|
#### Strategy A: Property Modification — apply directly
|
|
121
168
|
|