| name | canicode-roundtrip |
| description | Run the full design-to-code roundtrip โ analyze a Figma design, surface gotchas, write
designer answers back into the file via the Figma Plugin API (use_figma), re-analyze to
confirm the gotchas were captured, hand off to figma-implement-design, then optionally
register a Code Connect mapping pointing the Figma component at the just-generated code
so future roundtrips reuse the implementation. Mutates the Figma file: requires Figma
full seat + edit access.
TRIGGER when: the user shares a figma.com/design/... URL and wants production-quality
code, or asks for "design-to-code roundtrip", "fix the gotchas", or "annotate the Figma
file" with implementation context; the user wants a single command for analyze โ survey โ
apply โ implement; the user wants Figma annotations or property fixes written before code
generation.
SKIP when: the user only wants a one-shot readiness report (route to canicode); the user
only wants Q&A captured locally without touching the Figma file (route to
canicode-gotchas); the user has no Figma edit access; the Figma MCP `use_figma` tool is
not loaded in this session (Step 0 will halt anyway).
|
| disable-model-invocation | false |
CanICode Roundtrip โ True Design-to-Code Roundtrip
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.
Output language (#546): Detect the user's conversation language from their messages in this session. When the user is conversing in a non-English language (e.g. Korean, Japanese, Spanish), every human-readable line you render โ Step 1 design summary, Step 2 grade banner, Step 3 question / Hint: / Example: / batch shared-prompt wording, Step 4 apply summary, Step 5 wrap-up rubric, Step 6 handoff line, Step 7 prompts and wrap-up โ must be rendered in that language. Identifiers stay English: ruleId, nodeId, severity label in brackets, marker glyphs (๐/โ
/๐/โญ๏ธ), the upsert-section markdown scaffolding. The full localization scope and exclusions are in Step 3's preamble below. Default to English only when the user's language is genuinely ambiguous (and ask once).
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).
Prerequisites
- Figma MCP server installed (provides
get_design_context, get_screenshot, use_figma, and other Figma tools) โ REQUIRED, there is no CLI fallback for use_figma. Register it with your host (e.g. Claude Code: claude mcp add -s project -t http figma https://mcp.figma.com/mcp; Cursor: add the Figma MCP entry per host docs / project .mcp.json).
- canicode MCP (preferred): Claude Code:
claude mcp add canicode -- npx --yes --package=canicode canicode-mcp โ long-form flags only; short -y -p collides with claude mcp add's parser (#366); do not pass -e FIGMA_TOKEN=โฆ here (#364). Cursor / other hosts: add canicode-mcp to MCP config โ see Customization guide. The server reads FIGMA_TOKEN from ~/.canicode/config.json or the environment.
- Without canicode MCP (fallback): Steps 1 (analyze) and 3 (gotcha-survey) shell out to
npx canicode <command> --json โ same JSON shape as the MCP tools. Step 4 (apply to Figma) still requires Figma MCP use_figma.
- FIGMA_TOKEN configured for live Figma URLs
- Figma Full seat + file edit permission (required for
use_figma to modify the design)
Workflow
Step 0: Verify Figma MCP tools are loaded
Before Step 1, verify that use_figma is callable in this session โ not merely listed in .mcp.json. Newly registered MCP servers require a host restart or MCP reload so tools appear (e.g. Claude Code: restart after claude mcp add โฆ; Cursor: restart Cursor or reload MCP after editing .cursor/mcp.json). Reading .mcp.json is not a substitute for checking the live tool list you have access to right now.
If use_figma is unavailable in the current session, Do NOT proceed to Step 1. Steps 1 (analyze) and 3 (gotcha-survey) spend real Figma API calls and 5โ15 minutes of human survey time before Step 4 would otherwise discover use_figma is missing. Halt immediately and tell the user:
- Confirm
.mcp.json (project or user) registers the Figma MCP entry (e.g. figma under mcpServers).
- Restart the IDE / agent host (or reload MCP) so the newly registered tools load.
- Re-invoke the roundtrip (Claude Code slash command
/canicode-roundtrip, or Cursor: @ canicode-roundtrip with the Figma URL).
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.
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.
Debugging: MCP not available or Step 4 fails
Work through this matrix before concluding a server is broken. Full detail and the symptom โ cause table are in CUSTOMIZATION.md โ Troubleshooting.
- 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.
- 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.
- 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."
- 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 for delivery notes.
Step 1: Analyze the design
If the analyze MCP tool is available, call it with the user's Figma URL:
analyze({ input: "<figma-url>" })
Without canicode MCP โ shell out to the CLI (same JSON shape):
npx canicode analyze "<figma-url>" --json
The response includes:
scores.overall.grade: design grade (S, A+, A, B+, B, C+, C, D, F)
isReadyForCodeGen: recommendation flag โ true suggests the survey can be skipped, but the user always confirms
issues: array of design issues found
summary: human-readable analysis summary
Show the user a brief summary:
Design grade: **{grade}** ({percentage}%) โ {issueCount} issues found.
Step 1.5: Code Connect prerequisite pre-check (soft warn)
The closing step (Step 7) registers a Code Connect mapping for the just-implemented design, which requires three things:
- The user's repo has
@figma/code-connect installed.
figma.config.json is present at the repo root.
- The target Figma component is published in a library (Figma UI: Assets panel โ Publish library).
The first two are repo-side; the third is Figma-side. Pass the Figma URL to canicode doctor --figma-url <url> (added in #532) so all three surface here, before the survey:
npx canicode doctor --figma-url "<the-figma-url-the-user-passed>"
Always quote the URL โ zsh expands ? in ?node-id=... otherwise.
Branch on exit code:
- Exit 0 (no blocking failures) โ silent. Continue to Step 2.
- The Figma publish-status check may render as
โ ๏ธ inconclusive (e.g. FIGMA_TOKEN not configured, network error, URL has no node-id). Inconclusive is not a failure: doctor stays informational, and Step 7d's actual add_code_connect_map call remains the authority. Print the inconclusive line for visibility but do not prompt.
- Exit 1 (any check failed) โ print the doctor's remediation lines verbatim, then prompt:
"Code Connect is not configured in this repo. The roundtrip will still generate code, but the closing mapping step (Step 7) will be skipped. Continue anyway? (Y/n)"
- Y (default) โ proceed to Step 2. Remember the prereq state so Step 7 can short-circuit without re-explaining.
- n โ stop the whole roundtrip cleanly. Tell the user: "Set up Code Connect and re-invoke
/canicode-roundtrip when ready."
Default is Y because many users genuinely just want code generation today and have not chosen to adopt Code Connect yet โ the soft warn informs without blocking. The publish-status check shifting the Figma-side prereq into this step (rather than discovering it after Step 7d's add_code_connect_map fails with "Published component not found") was the #532 motivation.
Step 2: Surface grade as informational banner
Show the grade as a preamble banner โ it is informational only, not a flow gate:
Design scored **{grade}** ({percentage}%).
Then branch on questions (not on isReadyForCodeGen):
If questions is empty (regardless of isReadyForCodeGen):
- Tell the user: "No gotchas surfaced. Continue to code generation?"
- If yes โ proceed to Step 6.
- If no โ stop.
If questions is non-empty AND isReadyForCodeGen is true (high-grade design with optional questions):
- Tell the user: "{N} optional gotcha(s) surfaced. Would you like to review them, or skip to code generation?"
- review โ proceed to Step 3.
- skip โ proceed to Step 6.
If questions is non-empty AND isReadyForCodeGen is false (grade B+ or below):
- Tell the user: "Running gotcha survey to surface implementation pitfalls."
- Proceed to Step 3.
Step 3: Run gotcha survey and collect answers
If the gotcha-survey MCP tool is available, call it:
gotcha-survey({ input: "<figma-url>" })
Without canicode MCP โ shell out to the CLI (same JSON shape):
npx canicode gotcha-survey "<figma-url>" --json
Step 3 โ preamble: match the user's language
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.
Step 3 โ grouped survey (groupedQuestions)
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. Per ADR-016, do not re-implement grouping.
Pacing โ one batch per message (#545): Render exactly one batch per assistant message and wait for the user's reply before rendering the next. A safe / opt-in multi-instance batch is still one batch โ render the shared prompt once and wait. Do not dump multiple batches in a single message and ask "Reply with answers numbered 1โN"; that defeats the paced Q&A UX. The total-batch count and the skip remaining affordance are surfaced once before batch 1 (and re-surfaced every 3rd batch per the appendix); they are not a license to bulk-render. The only exception is skip remaining โ when the user invokes it, mark all unanswered batches as skipped and proceed straight to Step 4.
Step 4: Apply gotcha answers to Figma design
Inline vs file staging (#531)
When the apply commands themselves are short (~โค 200 lines / ~10 KB), assemble the use_figma code string inline in the same call โ read the helper artifact, concatenate with the apply commands, and pass directly. Three tool calls (Write apply.js + Read combined โ use_figma) for a 2โ3 gotcha apply is overhead with no debug benefit beyond what use_figma's own error message already provides. Only stage to /tmp/canicode-apply.js when the apply payload is large enough to bloat the model's reply (e.g. dozens of replicas, definition-write fan-out, or any single batch nearing the ~50KB use_figma ceiling). The helpers artifact may still be a separate Read; the staging tradeoff is about the apply payload, not the helpers.
Mandatory preflight โ prepend one of the bundled helpers before any CanICodeRoundtrip.* call
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.
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:
- 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).
- 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.
- 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.
- 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.
Artifact paths:
- Claude Code / default
canicode init: .claude/skills/canicode-roundtrip/helpers-installer.js, helpers-bootstrap.js, and helpers.js (fallback).
- Cursor after
canicode init --cursor-skills: .cursor/skills/canicode-roundtrip/helpers-installer.js, helpers-bootstrap.js, and helpers.js (fallback).
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.
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:
return { ok: typeof CanICodeRoundtrip !== 'undefined' };
See 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).
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.
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.
Input shape from canicode
Every gotcha-survey question (and every entry in analyzeResult.issues[]) carries these pre-computed fields:
| Field | Type | Meaning |
|---|
applyStrategy | "property-mod" | "structural-mod" | "annotation" | "auto-fix" | Which strategy branch to enter (A/B/C/D). |
targetProperty | string | string[] | (absent) | Figma Plugin-API property to write. Array when multiple properties move together (e.g. no-auto-layout โ ["layoutMode", "itemSpacing"]). Absent for structural/annotation rules. |
annotationProperties | Array<{ type: string }> | (absent) | Pre-computed Dev Mode annotation properties hint for the ruleId (+ subType). Pass directly to upsertCanicodeAnnotation. Absent when the rule has no mapping. See the annotation matrix below for the enum + node-type filtering (enforced by the helper's retry path). |
suggestedName | string | (absent) | Naming rules only โ pre-capitalized value to write to node.name (e.g. "Hover"). |
isInstanceChild | boolean | Whether the nodeId targets a node inside an INSTANCE subtree. |
sourceChildId | string | (absent) | Definition node id inside the source component. Use directly with figma.getNodeByIdAsync. |
instanceContext | object | (absent) | Survey questions only. { parentInstanceNodeId, sourceNodeId, sourceComponentId?, sourceComponentName? } for the Step 3 user-facing note. |
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. |
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. |
Instance-child matrix, annotation enum matrix, write tiers, probe, helpers
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 on main. Open it when you need the matrices or helper list โ do not re-derive write rules from memory (ADR-016).
Strategy A: Property Modification โ apply directly
Rules with applyStrategy === "property-mod". Call the bundled helper โ it branches on question.targetProperty (single vs array) and on each value type (scalar, multi-property object, { variable: "token-name" } binding) automatically. Paint properties (fills, strokes) are bound with setBoundVariableForPaint per the Plugin API contract; scalar fields use setBoundVariable.
await CanICodeRoundtrip.applyPropertyMod(question, answerValue, { categories });
Replicas (#356) โ when question.replicaNodeIds is present, the same answer must land on every replica instance. Iterate the merged set so each scene gets its own per-node failure routing (under the ADR-012 default each replica annotates independently; with allowDefinitionWrite: true they share the one definition write because they share the source):
const targets = [question.nodeId, ...(question.replicaNodeIds ?? [])];
for (const nodeId of targets) {
await CanICodeRoundtrip.applyPropertyMod({ ...question, nodeId }, answerValue, { categories });
}
Answer shape guide (LLM judgment โ the user's answer is prose; parse accordingly):
non-semantic-name: string โ the new node name.
irregular-spacing: number for gap (subType gap), or { paddingTop, paddingRight, paddingBottom, paddingLeft } for padding.
fixed-size-in-auto-layout: "FILL" | "HUG" | "FIXED" โ applied to each axis listed in targetProperty.
missing-size-constraint: partial { minWidth, maxWidth } โ include only the keys the answer supplied.
no-auto-layout: { layoutMode, itemSpacing }; optionally extend with padding/alignment from the answer.
Variable binding โ whenever the answer names a design-system token (e.g. the user says the width should be mobile-width, the gap should be space-m, the color should be Brand/Primary), shape the value as { variable: "token-name" } instead of a raw scalar. The helper calls setBoundVariable which bypasses instance-child override restrictions, so minWidth/maxWidth/color fields that raw writes cannot touch on an instance child will bind successfully. Mix shapes per-property โ e.g. { minWidth: { variable: "mobile-width" }, maxWidth: 1440 }.
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.
Strategy B: Structural modification
Instance-child guard and per-rule prompts โ Appendix Strategy B. Decline / guard โ Strategy C annotation.
Strategy B group componentize โ Phase 3 (missing-component:structure-repetition)
When applyStrategy === "structural-mod" AND question.ruleId === "missing-component" AND question.subType === "structure-repetition" AND question.groupMembers is set, the question represents a fingerprint group of N FRAMEs the user can componentize-and-swap in one batch. The group spans both same-parent siblings and cross-parent matches found by the Stage 3 scope-wide pass (#557). Render the per-question prompt with the group size explicitly so the designer knows the scope before answering. Substitute {nodeName} with question.nodeName and {others} with question.groupMembers.length - 1 (the count excluding the first member that becomes the new component); render in the user's session language:
- Korean:
> "{nodeName}" ์ธ์ ๋์ผํ ๊ตฌ์กฐ์ frame์ด {others}๊ฐ ๋ ์์ต๋๋ค (์ด {others + 1}๊ฐ). ๋ชจ๋ ์ปดํฌ๋ํธํ ํ ๊น์? (yes/no)
- English:
> "{nodeName}" and {others} other frame(s) share the same structure ({others + 1} total). Componentize the whole group? (yes/no)
On yes, compute the file-wide existing component name set once (decision C uses this for the suffix), then call the group orchestrator. On no / skip, drop the question without writing anything; the gotcha state is captured in the SKILL's section markdown either way.
const existingComponentNames = new Set(
figma.root
.findAllWithCriteria({ types: ["COMPONENT", "COMPONENT_SET"] })
.map((c) => c.name)
);
const result = await CanICodeRoundtrip.applyGroupComponentize({
question: { ruleId: question.ruleId, groupMembers: question.groupMembers },
existingComponentNames,
categories,
});
Notes:
- Free-form parents are refused per ADR-023 decision A. When the group's parent (or any swap-target's parent) has no Auto Layout, the relevant primitive annotates the source FRAME with a "wrap in Auto Layout first" hint and skips the write. The summary string surfaces the count (
(1 free-form parent)) so the designer sees the partial outcome at a glance.
- Name collision auto-suffixes per ADR-023 decision C (
Card 2, Card 3, โฆ). The orchestrator passes existingComponentNames to the componentize step which resolves the suffix and reports the rename in result.componentizeResult.finalName.
- Per-member opt-out is not yet wired โ the orchestrator treats
groupMembers as canonical. If the designer wants to exclude a specific member, that is currently a manual pre-edit (delete the entry from the question payload before calling) or a follow-up enhancement.
- No replica fan-out. Stage 3 questions never carry
replicaNodeIds (the #356 instance-child dedupe applies to single-node violations, not group-shaped ones). Iterate groupMembers instead.
Code Connect handoff for the new component (Phase 3 delta 5, optional)
When result.outcome === "componentized-and-swapped" AND result.componentizeResult.newComponentId is set, the new component is a candidate for a Code Connect mapping so future roundtrips reuse the just-generated code instead of regenerating markup. This mirrors the Workflow 1 (#509) Step 7 close-out โ same pre-check, same MCP tools, just sharing the closing question.
Per ADR-023 decision E this is silent skip + one-line pointer when prereqs are absent โ Phase 3 does not own onboarding (Workflow 1 / #509 does). The check is the same npx canicode doctor --figma-url <url> already run at Step 1.5 (prereqs.codeConnectReady cached on the session) โ do not re-run doctor here. If prereqs were missing, surface ONE line and move on:
Code Connect ๋ฏธ์ค์ ์ด๋ผ ๋งคํ ๋จ๊ณ๋ ๊ฑด๋๋๋๋ค. Workflow 1 (/canicode-roundtrip <component-url>)๋ก setupํ๋ฉด ๋ค์๋ถํฐ ์๋ ๋งคํ๋ฉ๋๋ค.
(English: > Code Connect not configured โ skipping mapping. Run Workflow 1 (\/canicode-roundtrip `) to set it up; subsequent roundtrips map automatically.`)
When prereqs are ready, ask the satisfaction prompt and, on yes, register the mapping:
const existing = await mcp__figma__get_code_connect_map({
componentId: result.componentizeResult.newComponentId,
});
if (existing) {
return;
}
const suggestions = await mcp__figma__get_code_connect_suggestions({
componentId: result.componentizeResult.newComponentId,
componentName: result.componentizeResult.finalName,
});
await mcp__figma__add_code_connect_map({
componentId: result.componentizeResult.newComponentId,
codePath: chosenSuggestion.path,
codeName: chosenSuggestion.exportName,
});
await mcp__figma__send_code_connect_mappings();
Notes:
- Inherits Step 1.5 prereq check. Do not re-invoke
canicode doctor โ the cached result from Step 1.5 already covers figma.config.json + @figma/code-connect install + Figma publish status. Skipping the re-check keeps the close-out fast and avoids a second Figma round-trip.
- No suggestions match โ still surface the option. When
get_code_connect_suggestions returns empty, ask the user for a manual codePath + codeName (one prompt, optional). Skipping is always valid โ the new component remains unmapped and Workflow 1 can register it later.
- Wraps the Step 4 apply line with one extra outcome marker:
+ Code Connect: Card โ src/components/Card.tsx on success, + Code Connect: skipped (prereq missing) or + Code Connect: skipped (user declined) on the two skip paths. Counts as part of the Phase 3 group's overall result for Step 5 tally โ no new counter, just an annotation appended to the existing line.
Strategy C: Annotation โ record on the design for designer reference
Rules with applyStrategy === "annotation" cannot be auto-fixed via Plugin API. Add the gotcha answer as a Figma annotation so designers see it in Dev Mode. Use the helper โ it handles the D1 mutex, D2 in-place upsert, and D4 category assignment. When question.replicaNodeIds is present (#356), iterate the merged set so every replica instance gets the annotation:
const targets = [question.nodeId, ...(question.replicaNodeIds ?? [])];
for (const nodeId of targets) {
const scene = await figma.getNodeByIdAsync(nodeId);
CanICodeRoundtrip.upsertCanicodeAnnotation(scene, {
ruleId: question.ruleId,
markdown: `**Q:** ${question.question}\n**A:** ${answer}`,
categoryId: categories.gotcha,
properties: question.annotationProperties,
});
}
Notes:
upsertCanicodeAnnotation writes the recommendation directly as the body and appends an italic โ *<ruleId>* footer. The footer is the dedup marker โ reruns replace the existing entry in place. The category badge (canicode:gotcha / canicode:flag / canicode:fallback) above the body already brands the annotation, so the body no longer leads with **[canicode] <ruleId>** (#353). Pre-#353 entries are still recognised on rerun and replaced with the new format.
label and labelMarkdown are mutually exclusive on write, but Figma returns both on readback. Never spread scene.annotations directly; always call CanICodeRoundtrip.upsertCanicodeAnnotation (or CanICodeRoundtrip.stripAnnotations if you truly need the normalized array).
- Prefer annotating the scene instance child so designers see the note where they work; mention in the markdown if the fix belongs on the source component but could not be applied (library/external).
Strategy C opt-out branch โ unmapped-component
When applyStrategy === "annotation" AND question.ruleId === "unmapped-component" AND the user's answer expresses "intentionally unmapped" (LLM judgment on the prose โ e.g. "skip permanently", "do not map", "intentionally unmapped"), call the dedicated opt-out helper instead of the standard upsertCanicodeAnnotation Q/A path:
await CanICodeRoundtrip.applyUnmappedComponentOptOut(
{ nodeId: question.nodeId, ruleId: question.ruleId },
{ categories }
);
The helper writes a fenced canicode-json block with kind: "rule-opt-out" and ruleId: "unmapped-component" under categories.gotcha. The read-side pipeline (Step 5a + ADR-022 rule short-circuit) consumes this on subsequent analyze runs to suppress the rule for this node โ see ADR-022 for the read-side pipeline that consumes this annotation.
Notes:
- No prose body, no per-property intent. The fence's
intent.kind is the discriminator, not Q/A markdown.
- No replica fan-out.
unmapped-component only fires on COMPONENT / COMPONENT_SET nodes (parser-driven main check). These never carry the Iโฆ;โฆ instance-child id format, so question.replicaNodeIds is absent for this rule โ do not iterate it. Writing an opt-out on an instance scene would no-op because the rule looks up the main component id when matching the ack.
- Idempotent on re-apply. The helper goes through
upsertCanicodeAnnotation, so the footer-based dedup replaces an existing entry in place; running Step 4 twice yields one annotation, not two.
- Distinct from a Step 3 skip. Skipping a gotcha (
answer === "skip" / "n/a") drops the question without touching the design; the opt-out path writes a permanent suppression marker that survives across analyze runs. Choose the opt-out only when the user truly means "this component should never be code-connected"; for "skip for now", drop the question.
Strategy D: Auto-fix lower-severity issues from analysis
The gotcha survey covers blocking/risk severity plus missing-info severity from info-collection rules (#406 โ currently missing-prototype, missing-interaction-state). All other lower-severity rules appear in analyzeResult.issues[] without a survey question. Each issue carries the same pre-computed fields (applyStrategy, targetProperty, annotationProperties, suggestedName, isInstanceChild, sourceChildId). The bundled helper handles the loop, the filter (applyStrategy === "auto-fix"), the naming-vs-annotation branch, and the per-issue outcome accumulator in one call:
const outcomes = await CanICodeRoundtrip.applyAutoFixes(analyzeResult.issues, { categories });
outcomes is an array of { outcome, nodeId, nodeName, ruleId, label }. outcome is one of ๐ง (rename succeeded), ๐ (definition write propagated โ only when allowDefinitionWrite: true), ๐ (annotation written, including the fallback path), or โญ๏ธ (issue's applyStrategy was not "auto-fix" so it was skipped). Bump the matching stepFourReport counter for each entry โ ๐ง โ resolved, ๐ โ definitionWritten, ๐ โ annotated, โญ๏ธ โ skipped โ so the Step 5 tally (CanICodeRoundtrip.computeRoundtripTally, #383) consumes the same structured shape as Strategies A/B/C.
suggestedName is already capitalized for direct Plugin-API use (e.g. "Hover", "Default", "Pressed"). The helper writes it through applyWithInstanceFallback so locked / read-only / instance-override nodes annotate cleanly instead of aborting the batch โ see the source at src/core/roundtrip/apply-auto-fix.ts (#386, ADR-016).
Execution order
- Initialize categories โ first batch calls
const categories = await CanICodeRoundtrip.ensureCanicodeCategories(); and keeps the result in scope for every subsequent call in the same script. (Or re-run ensure at the top of each use_figma batch โ it is idempotent by label.)
- 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.
- Present structural modifications (Strategy B) one by one, apply confirmed ones.
- Batch all annotations (Strategy C + declined structural mods) into a single
use_figma call โ use categories.gotcha for the category id.
- 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).
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:
Applied {N} changes to the Figma design:
- โ
{nodeName}: renamed to "hero-section" (non-semantic-name) โ scene/instance override
- ๐ {nodeName}: minWidth applied on source definition (missing-size-constraint) โ propagates to all instances
- โ
{nodeName}: itemSpacing โ 16px (irregular-spacing)
- ๐ {nodeName}: minWidth bound to variable "mobile-width" (missing-size-constraint)
- โญ๏ธ {nodeName}: declined by user, added annotation (deep-nesting)
- ๐ {nodeName}: annotation added to canicode:gotcha (absolute-position-in-auto-layout)
- ๐ง {nodeName}: auto-fixed to "Hover" (non-standard-naming)
- ๐ {nodeName}: annotation added to canicode:flag โ raw color needs token binding (raw-value)
After each emoji line above, mirror a **structured per-item row** so scene-write vs annotation fallback is visible every run (#435):
{ruleId} @ {nodeName}
attempt: scene write (question.targetProperty / binding shape from answer)
result: {emoji outcome} ({short reason โ e.g. silent-ignore ADR-012 โ annotated, override-error โ annotated, tier-2 propagated})
stepFourReport = {
resolved: <count of โ
+ ๐ง + ๐ lines>, // scene writes, auto-fix renames, variable bindings
annotated: <count of ๐ lines>, // including โญ๏ธ declines that fell back to annotation
definitionWritten: <count of ๐ lines>, // only non-zero with allowDefinitionWrite: true
skipped: <count of โญ๏ธ lines + Step 3 skip/n/a> // user-declined questions
}
Hold stepFourReport in scope through Step 5 โ it is the input to CanICodeRoundtrip.computeRoundtripTally below.
Auto-chain acknowledgments after apply (#440)
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.
Step 5: Re-analyze and report what the roundtrip addressed
Step 5a: Harvest canicode-authored annotations as acknowledgments (#371)
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.
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:
const categories = await CanICodeRoundtrip.ensureCanicodeCategories();
const acknowledgments = await CanICodeRoundtrip.readCanicodeAcknowledgments(
targetNodeId ?? figma.root.id,
categories
);
return { events: [], acknowledgments };
readCanicodeAcknowledgments walks node.children recursively, gates on the canicode:gotcha / canicode:flag / canicode:fallback (and legacy canicode:auto-fix) category ids, and extracts the ruleId from the annotation footer (โ *<ruleId>*) or the legacy **[canicode] <ruleId>** prefix. The categoryId guard keeps user-authored notes that happen to end in italic kebab-case from being mistaken for canicode acknowledgments.
Step 5b: Re-analyze with acknowledgments
Pass the harvested array straight into analyze so the engine flags matching issues as acknowledged: true and the density score gives them half weight:
analyze({ input: "<figma-url>", acknowledgments })
Without canicode MCP โ the CLI accepts the same input via --acknowledgments <path> (JSON file containing the array). Write the array to a temp file from the use_figma return, then:
npx canicode analyze "<figma-url>" --json --acknowledgments /tmp/canicode-acks.json
The response now carries:
acknowledgedCount (top level) โ how many issues matched an acknowledgment.
issues[i].acknowledged: true (per matched issue) โ survives into the report and downstream skills.
summary text โ when acknowledgedCount > 0, the Total line reads Total: N (A acknowledged via canicode annotations / N-A unaddressed).
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).
Grade-movement attribution (#547): When the wrap-up shows a grade jump (e.g. C+ โ B+), attribute the move to the resolved bucket (โ
/ ๐ง / ๐) explicitly so the user does not mis-infer that ๐ annotations contributed. Per ADR-012, annotations are zero-score by design โ they carry context into code-gen but never move the grade. When tally.Y > 0 (any ๐ annotated count), include a one-liner near the bucket tally clarifying this. Templates below already include the line; do not omit it.
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:
const tally = CanICodeRoundtrip.computeRoundtripTally({
stepFourReport,
reanalyzeResponse: {
issueCount: response.issueCount,
acknowledgedCount: response.acknowledgedCount,
},
});
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.)
All gotcha issues resolved (V == 0, i.e. re-analyze surfaces no remaining issues):
-
Tell the user (fill in the counts from the tally above):
Roundtrip complete โ N issues addressed:
โ
X resolved (auto-fix or property write succeeded)
๐ Y annotated on Figma (gotcha answers captured for code-gen)
๐ Z definition writes propagated (only when allowDefinitionWrite: true)
โญ๏ธ W skipped (user declined or "skip")
โ
V issues remaining (unresolved gotchas + non-actionable rules)
*(When Y > 0)* ๐ annotations carry context into code-gen but do not change the grade โ that is by design (ADR-012). Any grade movement comes from the โ
/ ๐ง / ๐ buckets above.
Ready for code generation. *(Optional:) Report still shows grade **{grade}** โ informational only.*
-
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:
const nodeIds = ["id1", "id2"];
for (const id of nodeIds) {
const node = await figma.getNodeByIdAsync(id);
if (node && "annotations" in node) {
node.annotations = CanICodeRoundtrip.removeCanicodeAnnotations(
CanICodeRoundtrip.stripAnnotations(node.annotations),
categories,
);
}
}
Some issues remain (V > 0):
-
Show the same breakdown and ask whether to proceed. When V_ack > 0, expand the remaining line into the acknowledged/unaddressed split surfaced by the re-analyze (#371) so the user can see how much of V is "captured for code-gen" vs "still on the user's plate":
Roundtrip complete โ N issues addressed:
โ
X resolved (auto-fix or property write succeeded)
๐ Y annotated on Figma (gotcha answers captured for code-gen)
๐ Z definition writes propagated (only when allowDefinitionWrite: true)
โญ๏ธ W skipped (user declined or "skip")
โ
V issues remaining
โณ V_ack acknowledged via canicode annotations (carried into code-gen)
โณ V_open unaddressed (no annotation โ your follow-up backlog)
*(When Y > 0)* ๐ annotations carry context into code-gen but do not change the grade โ that is by design (ADR-012). Any grade movement comes from the โ
/ ๐ง / ๐ buckets above.
Proceed to code generation with remaining context? *(Optional footnote: report grade **{grade}**.)*
When V_ack == 0 (re-analyze returned acknowledgedCount: 0), keep the single V issues remaining (unresolved gotchas + non-actionable rules) line.
-
If yes โ proceed to Step 6 with remaining gotcha context.
-
If no โ stop and emit the Stop wrap-up below; lead with the delta, not grade.
Wrap-up message rubric (Stop branch)
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.
Stopped โ N issues addressed, V remaining for manual follow-up:
โ
X resolved
๐ Y annotated on Figma (carried into code-gen via canicode-gotchas)
๐ Z definition writes propagated
โญ๏ธ W skipped
โ
V remaining
โณ V_ack acknowledged via canicode annotations
โณ V_open unaddressed
*(When Y > 0)* ๐ annotations carry context into code-gen but do not change the grade โ that is by design (ADR-012).
*(Optional)* Report grade: **{grade}**.
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.
Step 6: Implement with Figma MCP
Follow the figma-implement-design skill workflow to generate code from the Figma design.
If annotations or unresolved gotchas remain from Step 5, provide them as additional context when implementing:
- Gotchas with severity blocking MUST be addressed โ the design cannot be implemented correctly without this information
- Gotchas with severity risk SHOULD be addressed โ they indicate potential issues that will surface later
- Gotchas with severity missing-info from info-collection rules (
purpose === "info-collection", e.g. missing-prototype, missing-interaction-state) are annotation-primary (#406): the answer describes implementation context Figma cannot encode (click target, state variants). Treat them as code-generation context rather than violations to fix โ the rule's score impact is minimal by design
- Reference the specific node IDs from gotcha answers to locate the affected elements in the design
- Pass the Figma URL or
survey.designKey to figma-implement-design so it can grep the matching ## #NNN โ โฆ section in .claude/skills/canicode-gotchas/SKILL.md instead of reading the whole accumulated file
If all issues were resolved in Steps 4-5, no additional gotcha context is needed โ the design speaks for itself.
After figma-implement-design returns, proceed to Step 7 (Code Connect close-out). Step 7 also owns the final wrap-up message โ do not print the post-handoff wrap-up here. The post-handoff wrap-up rubric below is only used when Step 7 is skipped at its entry condition (see Step 7 โ Entry condition).
Wrap-up message rubric (post-handoff, fallback only)
Used only when Step 7 is skipped at its entry condition (e.g., the user invoked the roundtrip on a screen-level node, not a single component). Otherwise the Step 7 wrap-up below replaces this one.
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).
Roundtrip complete โ N issues addressed, code generated:
โ
X resolved
๐ Y annotated on Figma (referenced during code-gen)
๐ Z definition writes propagated
โญ๏ธ W skipped
โ
V issues remaining
โณ V_ack acknowledged via canicode annotations
โณ V_open unaddressed
*(When Y > 0)* ๐ annotations carry context into code-gen but do not change the grade โ that is by design (ADR-012).
*(Optional)* Report grade: **{grade}**.
Code: <files generated / next-step pointer from figma-implement-design>
(Drop the โณ lines when V_ack == 0.)
Step 7: Close out with a Code Connect mapping
Final step of the roundtrip. Registers a Code Connect mapping pointing the Figma component at the just-generated code so future roundtrips on screens containing this component reuse the implementation instead of regenerating markup. Step 7 owns the final wrap-up โ when this step runs (whether it ends in mapped, skipped, or failed), use the wrap-up rubric at the end of this section instead of the Step 6 fallback.
Step 7 โ Entry condition (single-component scope)
v1 only fires Step 7 when the roundtrip was invoked against a single Figma main component. Multi-component mapping for screen-level roundtrips is out of scope (#515 calls this out as v1.5).
To decide: read the analyze response from Step 1 โ the top-level node's type (or equivalent in get_design_context) tells you whether it is COMPONENT / COMPONENT_SET (single-component scope, run Step 7) versus FRAME / SECTION / INSTANCE containing many descendants (screen-level, skip Step 7).
- Single-component (COMPONENT / COMPONENT_SET) โ proceed to Step 7a.
- Screen-level (anything else) โ print one line: "Roundtrip invoked on a screen-level node โ Code Connect mapping is per-component. Re-invoke
/canicode-roundtrip against an individual main component to register a mapping." Then fall back to the Step 6 post-handoff wrap-up rubric (the fallback rubric immediately above this section). Do not print the Step 7 wrap-up.
Step 7a: Re-check prerequisites
Re-run canicode doctor. The pre-check in Step 1.5 may have failed and the user may have set up Code Connect mid-flow, or it may have passed and still pass โ either way, this is the source of truth for the close-out branch.
- Exit 0 โ proceed to 7b.
- Exit 1 โ print the doctor's remediation, then exit cleanly with:
"Roundtrip steps 1โ6 succeeded. Code Connect mapping skipped because the prerequisites above are missing โ set them up and re-invoke /canicode-roundtrip to register the mapping."
This step's failure must not retroactively fail the earlier steps; their output is independently valuable.
Step 7b: Confirm satisfaction with the generated code
figma-implement-design generated the code at <path>. Are you satisfied with this implementation? (y/N)
Default is N โ the inverse of Step 1.5's default โ because registering a mapping makes a permanent claim about which code represents this Figma component. Asking the user to opt in deliberately is the safer posture.
- N or skip โ exit cleanly: "Mapping not registered. You can re-invoke
/canicode-roundtrip later if you want to map a future revision."
- y โ continue to 7c.
If figma-implement-design's output did not surface the generated code path in a structured way, prompt the user for it before proceeding. Do not try to scan for "recently modified files" โ too fragile.
Step 7c: Check existing mapping
Call get_code_connect_map for the Figma component's node-id (from the original input URL).
- If a mapping exists for this component, show the user the current mapping target and ask:
"A Code Connect mapping already exists for this component, pointing at <existing-path>. Update it to <new-path>? (y/N)"
- N โ exit cleanly, leaving the existing mapping intact.
- y โ proceed to 7d.
- If no mapping exists, proceed straight to 7d.
Step 7d: Register the mapping
Call add_code_connect_map with the Figma node-id + generated code path. In single-mapping roundtrips this publishes the mapping synchronously โ the server-side persistence happens at this call.
send_code_connect_mappings is a batch flush primitive intended for sessions that build up multiple mappings before publishing them as a transaction. In the single-mapping flow this skill drives, calling it after add_code_connect_map returns a duplicate / "no pending mappings" error because the mapping is already live. Treat that follow-up call as optional:
- Recommended (single-mapping path): skip
send_code_connect_mappings entirely. add_code_connect_map is the publish point.
- If you call it anyway (e.g. defensive habit, or a future multi-mapping flow batches multiple
add_* first): tolerate the duplicate / already-registered error explicitly. Do not narrate it as a failure to the user โ the mapping is live. Verify with get_code_connect_map if confirmation is needed before the wrap-up line.
On success (i.e. add_code_connect_map returned without error), print:
"Code Connect mapping registered: <figma-component> โ <code-path>. Future roundtrips on screens containing this component will reuse the code."
The success line is unconditional once add_code_connect_map succeeds. Do not gate it on send_code_connect_mappings returning OK.
On failure of add_code_connect_map itself (Figma MCP returns an error), print the error verbatim and tell the user the rest of the roundtrip succeeded โ the mapping can be added later via Figma CLI (figma connect publish) or by re-invoking the roundtrip. The most common cause is the Figma component not being in a published library; #532 tracks shifting that check earlier into Step 1.5.
Wrap-up message rubric (with mapping outcome)
Extend the Step 6 wrap-up block with one final line describing the mapping outcome โ โ
mapped, โญ๏ธ skipped (user), โญ๏ธ skipped (prereq), or โ failed. Keep grade at most one optional footline (#423).
Roundtrip complete โ N issues addressed, code generated, mapping <state>:
โ
X resolved
๐ Y annotated on Figma (referenced during code-gen)
๐ Z definition writes propagated
โญ๏ธ W skipped
โ
V issues remaining
โณ V_ack acknowledged via canicode annotations
โณ V_open unaddressed
*(When Y > 0)* ๐ annotations carry context into code-gen but do not change the grade โ that is by design (ADR-012).
Code: <files generated / next-step pointer from figma-implement-design>
Code Connect: <mapping outcome line>
Edge cases
Full list โ Appendix Edge Cases.