canicode 0.10.3 → 0.10.5
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 +35 -30
- package/dist/cli/index.js +358 -24
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +257 -3
- package/dist/index.js +96 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +157 -26
- package/dist/mcp/server.js.map +1 -1
- package/package.json +2 -1
- package/skills/canicode-gotchas/SKILL.md +66 -28
- package/skills/canicode-roundtrip/SKILL.md +180 -79
- package/skills/canicode-roundtrip/helpers.js +218 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canicode",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
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",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"lint": "tsc --noEmit",
|
|
28
28
|
"build:plugin": "bash scripts/build-plugin.sh",
|
|
29
29
|
"sync-docs": "tsx scripts/sync-rule-docs.ts",
|
|
30
|
+
"check:skill-determinism": "tsx scripts/check-skill-determinism.ts",
|
|
30
31
|
"clean": "rm -rf dist skills"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
@@ -9,7 +9,7 @@ Run a gotcha survey on a Figma design to identify implementation pitfalls, colle
|
|
|
9
9
|
|
|
10
10
|
## Prerequisites
|
|
11
11
|
|
|
12
|
-
- **canicode MCP server** (preferred): `claude mcp add canicode
|
|
12
|
+
- **canicode MCP server** (preferred): `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp` — long-form flags only; the short-form `-y -p` collides with `claude mcp add`'s parser (#366). The MCP server reads `FIGMA_TOKEN` from `~/.canicode/config.json` or the host environment, so do **not** pass `-e FIGMA_TOKEN=…` here (#364).
|
|
13
13
|
- **Without canicode MCP** (fallback): the `canicode gotcha-survey --json` CLI produces the same response shape — no MCP installation required.
|
|
14
14
|
- **FIGMA_TOKEN** configured for live Figma URLs
|
|
15
15
|
|
|
@@ -43,21 +43,50 @@ If `isReadyForCodeGen` is `true` or `questions` is empty:
|
|
|
43
43
|
|
|
44
44
|
### Step 3: Present questions to the user
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
The survey response carries a pre-computed `groupedQuestions.groups[].batches[]` shape so the SKILL never has to sort, partition, or maintain a batchable-rule whitelist in prose. The sort key, `_no-source` sentinel, and batchable-rule list all live in `core/gotcha/group-and-batch-questions.ts` with vitest coverage (per ADR-016). Iterate over it:
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
**[{severity}] {ruleId}** — node: {nodeName}
|
|
48
|
+
For every `batch` in `groupedQuestions.groups.flatMap((g) => g.batches)`:
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
- **Single-question batch (`batch.questions.length === 1`)** — render the standard prompt for `batch.questions[0]`:
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
```
|
|
53
|
+
**[{severity}] {ruleId}** — node: {nodeName}
|
|
54
|
+
|
|
55
|
+
{question}
|
|
56
|
+
|
|
57
|
+
> Hint: {hint}
|
|
58
|
+
> Example: {example}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **Batch of N ≥ 2 with `batch.batchable === true`** (#369) — render one shared prompt covering every member:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
**[{severity}] {ruleId}** — {batch.questions.length} instances:
|
|
65
|
+
- {nodeName₁}
|
|
66
|
+
- {nodeName₂}
|
|
67
|
+
- …
|
|
68
|
+
|
|
69
|
+
{sharedQuestionPrompt}
|
|
70
|
+
|
|
71
|
+
Reply with one answer to apply to all {batch.questions.length}, or **split** to answer each individually.
|
|
72
|
+
|
|
73
|
+
> Hint: {hint}
|
|
74
|
+
> Example: {example}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
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).
|
|
56
78
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-
|
|
79
|
+
- **Any batch with `batch.batchable === false`** is always rendered as a single-question prompt — the helper guarantees `questions.length === 1` for those (identity-typed answers like `non-semantic-name`, structural-mod rules).
|
|
80
|
+
|
|
81
|
+
Wait for the user's answer before moving to the next batch. The user may:
|
|
82
|
+
- Answer the question / batch directly
|
|
83
|
+
- Say **split** (batch only) to fall back to per-question prompting for that batch
|
|
84
|
+
- Say **skip** to skip the question / the entire batch
|
|
85
|
+
- Say **n/a** if the question / the entire batch is not applicable
|
|
86
|
+
|
|
87
|
+
When applying the batched answer, expand back to per-question records in Step 4 — the gotcha section format stores one record per `nodeId`.
|
|
88
|
+
|
|
89
|
+
> The `groupedQuestions.groups[].instanceContext` field exists for the `canicode-roundtrip` SKILL's "Instance note" hoist (#370). This skill ignores it — every record gets its own `Instance context` bullet in Step 4 anyway.
|
|
61
90
|
|
|
62
91
|
### Step 4: Upsert the gotcha section
|
|
63
92
|
|
|
@@ -69,26 +98,35 @@ After collecting all answers, **upsert** this design's section into the `# Colle
|
|
|
69
98
|
|
|
70
99
|
This file goes in the **user's project** (current working directory), NOT in the canicode repo. The Workflow region above **must never be modified** — only the `# Collected Gotchas` region below is touched.
|
|
71
100
|
|
|
72
|
-
#### Step 4a:
|
|
101
|
+
#### Step 4a: Use the `designKey` from the survey response
|
|
102
|
+
|
|
103
|
+
`designKey` uniquely identifies the design so re-running on the same URL replaces the existing section in place. The survey response carries it on `survey.designKey` — read it directly. Do **not** parse the input URL in prose.
|
|
104
|
+
|
|
105
|
+
The `core/contracts/design-key.ts` helper (`computeDesignKey`) handles every shape with vitest coverage so the SKILL stays ADR-016-compliant:
|
|
73
106
|
|
|
74
|
-
|
|
107
|
+
- **Figma URL** → `<fileKey>#<nodeId>` with `-` → `:` normalization on the nodeId. Example: `https://figma.com/design/abc123XYZ/My-File?node-id=42-100&t=ref` → `designKey = "abc123XYZ#42:100"`. Trailing query parameters (`?t=...`, `?mode=...`) are dropped.
|
|
108
|
+
- **Figma URL without `node-id`** → just `<fileKey>` (file-level key).
|
|
109
|
+
- **Fixture path / JSON file** → absolute path.
|
|
75
110
|
|
|
76
|
-
|
|
77
|
-
- **Fixture path** — use the absolute path, e.g. `/Users/me/project/fixtures/simple.json`.
|
|
111
|
+
#### Step 4b: Upsert via the canicode CLI
|
|
78
112
|
|
|
79
|
-
|
|
113
|
+
File-state detection (4-way: missing / valid / missing-heading / clobbered) and section walking (find existing `## #NNN — ...` by `Design key` substring, otherwise compute the next monotonic zero-padded NNN) are deterministic markdown operations and live in `core/gotcha/upsert-gotcha-section.ts` with vitest coverage — the SKILL never re-implements them in prose (per ADR-016).
|
|
114
|
+
|
|
115
|
+
Render the per-design section markdown using the **Output Template** below with the literal string `{{SECTION_NUMBER}}` in the header (the CLI substitutes the right NNN for you — preserves it on replace, computes the next monotonic value on append). Then invoke:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx canicode upsert-gotcha-section \
|
|
119
|
+
--file .claude/skills/canicode-gotchas/SKILL.md \
|
|
120
|
+
--design-key "<designKey from Step 4a>" \
|
|
121
|
+
--section - # then pipe the rendered section markdown through stdin
|
|
122
|
+
```
|
|
80
123
|
|
|
81
|
-
|
|
124
|
+
The CLI prints a JSON result `{ state, action, sectionNumber, wrote, userMessage }`:
|
|
82
125
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- **File has YAML frontmatter but no `# Collected Gotchas` heading** (an older workflow install, or a user-edited workflow that dropped the trailing heading) → preserve everything above unchanged and append a new `# Collected Gotchas` heading at the bottom, then proceed to step 3.
|
|
88
|
-
- **File missing the YAML frontmatter** (a pre-#340 single-design clobber — the old overwrite rewrote the frontmatter's `description` to the per-design variant, so a well-formed canicode frontmatter is the cleanest discriminator) → **do not attempt to reconstruct the workflow inline**. Tell the user: "Your gotchas SKILL.md looks like the pre-#340 single-design format. Run `canicode init --force` to restore the workflow, then re-run this survey — your answers will land in a clean numbered section." Stop here.
|
|
89
|
-
3. Walk the existing `## #NNN — ...` sections under `# Collected Gotchas` and look for one whose `- **Design key**:` bullet matches the `designKey` from Step 4a. Substring match against the bullet value is sufficient.
|
|
90
|
-
- **Found** → replace that section in place. **Preserve its `#NNN` number** so external references (downstream skills, user notes) remain stable.
|
|
91
|
-
- **Not found** → append a new section at the bottom of the region. `#NNN = (highest existing number) + 1`, zero-padded to three digits. Never reuse a number that appeared earlier and was deleted; numbering is monotonic.
|
|
126
|
+
- `wrote: true` → success. `action` is `"replace"` (preserved `sectionNumber`) or `"append"` (next monotonic `sectionNumber`).
|
|
127
|
+
- `wrote: false` with `state: "missing"` → tell the user: *"Your gotchas SKILL.md is not installed yet. Run `canicode init` first, then re-invoke this skill."* Stop here.
|
|
128
|
+
- `wrote: false` with `state: "clobbered"` → tell the user: *"Your gotchas SKILL.md is missing the canicode YAML frontmatter (pre-#340 single-design clobber). Run `canicode init --force` to restore the workflow, then re-run this survey — your answers will land in a clean numbered section."* Stop here.
|
|
129
|
+
- `wrote: true` with `state: "missing-heading"` → silent recovery. The CLI injected the `# Collected Gotchas` heading and appended the section; the workflow region above is untouched.
|
|
92
130
|
|
|
93
131
|
The Workflow region above must never be touched. Do NOT copy Workflow prose into the per-design section; the section only carries metadata + gotcha answers.
|
|
94
132
|
|
|
@@ -125,7 +163,7 @@ Each per-design section in the `# Collected Gotchas` region has this exact shape
|
|
|
125
163
|
| `designName` | Figma file name or fixture name from the input |
|
|
126
164
|
| `YYYY-MM-DD` | Today's date (the day you are running the survey) |
|
|
127
165
|
| `figmaUrl` | The input URL or fixture path provided by the user |
|
|
128
|
-
| `designKey` |
|
|
166
|
+
| `designKey` | `survey.designKey` from the gotcha-survey response (see Step 4a) |
|
|
129
167
|
| `designGrade` | `designGrade` from gotcha-survey response |
|
|
130
168
|
| `analyzedAt` | Current timestamp (ISO 8601) |
|
|
131
169
|
| `ruleId` | `ruleId` from each question |
|
|
@@ -11,7 +11,7 @@ Orchestrate the full design-to-code roundtrip: analyze a Figma design for readin
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
13
13
|
- **Figma MCP server** installed (provides `get_design_context`, `get_screenshot`, `use_figma`, and other Figma tools) — REQUIRED, there is no CLI fallback for `use_figma`
|
|
14
|
-
- **canicode MCP server** (preferred): `claude mcp add canicode
|
|
14
|
+
- **canicode MCP server** (preferred): `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp` — long-form flags only; the short-form `-y -p` collides with `claude mcp add`'s parser (#366). The MCP server reads `FIGMA_TOKEN` from `~/.canicode/config.json` or the host environment, so do **not** pass `-e FIGMA_TOKEN=…` here (#364).
|
|
15
15
|
- **Without canicode MCP** (fallback): Steps 1 (analyze) and 3 (gotcha-survey) shell out to `npx canicode <command> --json` — same JSON shape as the MCP tools. Step 4 (apply to Figma) still requires Figma MCP `use_figma`.
|
|
16
16
|
- **FIGMA_TOKEN** configured for live Figma URLs
|
|
17
17
|
- **Figma Full seat + file edit permission** (required for `use_figma` to modify the design)
|
|
@@ -82,45 +82,91 @@ npx canicode gotcha-survey "<figma-url>" --json
|
|
|
82
82
|
|
|
83
83
|
If `questions` is empty, skip to **Step 6**.
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
#### Step 3a: Why the response carries a pre-grouped+batched view
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
The naive "one-question-at-a-time" loop produces two well-known UX failures on real designs:
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
```
|
|
89
|
+
- **Repeated Instance note (#370)** — when 10 consecutive questions share the same `instanceContext.sourceComponentId`, the standard "_Instance note: …source component **X**…_" paragraph prints 10 times. After the first occurrence it adds zero new information and consumes ~2 screens of vertical space.
|
|
90
|
+
- **Repeated identical answer (#369)** — when 7 consecutive questions all carry the same `ruleId` (e.g. `missing-size-constraint`) and the user's reasonable answer would be the same for all of them (e.g. `min-width: 320px, max-width: 1200px`), the user types the same thing 7 times in a row.
|
|
92
91
|
|
|
93
|
-
|
|
92
|
+
`gotcha-survey` already ships the resolution on its `groupedQuestions` field. Sort key (`(sourceComponentId ?? "_no-source", ruleId, nodeName)`), source-component grouping, and the batchable-rule whitelist (`missing-size-constraint`, `irregular-spacing`, `no-auto-layout`, `fixed-size-in-auto-layout`) all live in `core/gotcha/group-and-batch-questions.ts` with vitest coverage. Per ADR-016, do **not** re-implement the sort, partition, or whitelist in prose — iterate over `groupedQuestions.groups[].batches[]` directly.
|
|
94
93
|
|
|
95
|
-
|
|
96
|
-
_Replicas: This question represents **{replicas} instances** of the same source-component child sharing the same rule. Your single answer will be applied to all of them in Step 4 (one annotation/write per instance scene)._
|
|
97
|
-
```
|
|
94
|
+
#### Step 3b: Prompt each group, then each batch within it
|
|
98
95
|
|
|
99
|
-
|
|
96
|
+
For each `group` in `response.groupedQuestions.groups`:
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
- **`group.instanceContext === null`** — this is the trailing group of non-instance questions. Skip the header and prompt each batch directly.
|
|
99
|
+
- **`group.instanceContext !== null`** — emit the Instance note **once** as a group header (#370):
|
|
103
100
|
|
|
104
|
-
|
|
101
|
+
```
|
|
102
|
+
─────────────────────────────────────────
|
|
103
|
+
The next {sum of batch.questions.length} questions all target instance children of source component **{instanceContext.sourceComponentName ?? instanceContext.sourceComponentId ?? "unknown"}** (definition node `{instanceContext.sourceNodeId}`). Layout and size fixes may need to apply on the source and propagate to all instances — you will be asked to confirm before any definition-level write.
|
|
104
|
+
─────────────────────────────────────────
|
|
105
|
+
```
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
For each `batch` inside the group:
|
|
108
|
+
|
|
109
|
+
- **`batch.questions.length === 1`** — render the standard single-question block for `batch.questions[0]`:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
**[{severity}] {ruleId}** — node: {nodeName}
|
|
113
|
+
|
|
114
|
+
{question}
|
|
115
|
+
|
|
116
|
+
> Hint: {hint}
|
|
117
|
+
> Example: {example}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**If `question.replicas` is present (#356 dedup)**, prepend one note above the standard block:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
_Replicas: This question represents **{replicas} instances** of the same source-component child sharing the same rule. Your single answer will be applied to all of them in Step 4 (one annotation/write per instance scene)._
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- **`batch.questions.length >= 2 && batch.batchable === true`** (#369) — render one batch prompt covering all members. Use `batch.totalScenes` (already summed across each member's `replicas`) for the Figma-scene fan-out hint:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
**[{severity}] {batch.ruleId}** — {batch.questions.length} instances:
|
|
130
|
+
- {nodeName₁}{ruleSpecificContext₁}
|
|
131
|
+
- {nodeName₂}{ruleSpecificContext₂}
|
|
132
|
+
- …
|
|
133
|
+
|
|
134
|
+
{sharedQuestionPrompt}
|
|
135
|
+
|
|
136
|
+
Reply with one answer to apply to all {batch.questions.length}, or **split** to answer each individually.
|
|
137
|
+
|
|
138
|
+
> Hint: {hint}
|
|
139
|
+
> Example: {example}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Where:
|
|
143
|
+
- `sharedQuestionPrompt` is the rule's `question` text with the per-node noun replaced by the rule's plural noun (e.g. "These layers all use FILL sizing without min/max constraints. What size boundaries should they share?" instead of repeating "What size boundaries should this layer have?" N times).
|
|
144
|
+
- `ruleSpecificContext` is short and rule-specific: e.g. for `missing-size-constraint` show the current `width`/`height` if the question has them; for `irregular-spacing` show the current `itemSpacing`; otherwise omit.
|
|
145
|
+
- On `split`, fall back to the per-question loop for that batch only — keep the rest of the group's batches as-is.
|
|
146
|
+
|
|
147
|
+
When `batch.totalScenes > batch.questions.length` (at least one member carries replicas), append one note so the user knows their single answer fans out further than the listed nodes:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
_Replicas: your one answer will land on **{batch.totalScenes}** Figma scenes total in Step 4 (some of these {batch.questions.length} questions already represent multiple instances of the same source-component child)._
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **`batch.batchable === false`** is always rendered as a single-question prompt — the helper guarantees `questions.length === 1` for those (identity-typed answers like `non-semantic-name`, structural-mod rules).
|
|
154
|
+
|
|
155
|
+
Wait for the user's answer before moving to the next batch. For each batch, the user may:
|
|
156
|
+
- Answer the question directly (single value covers all batch members)
|
|
157
|
+
- Say **split** (batch only) to fall back to per-question prompting for that batch
|
|
158
|
+
- Say **skip** to skip the question / the entire batch
|
|
159
|
+
- Say **n/a** if the question / the entire batch is not applicable
|
|
109
160
|
|
|
110
|
-
|
|
111
|
-
- Answer the question directly
|
|
112
|
-
- Say "skip" to skip a question
|
|
113
|
-
- Say "n/a" if the question is not applicable
|
|
161
|
+
When applying the batched answer, expand back to per-question records before storing — the gotcha section format and Step 4 apply loop both expect one record per `nodeId`.
|
|
114
162
|
|
|
115
|
-
After all questions are answered, **upsert this design's gotcha section** into `.claude/skills/canicode-gotchas/SKILL.md` in the user's project. Read the existing file, then either replace the section whose `Design key` matches
|
|
163
|
+
After all questions are answered, **upsert this design's gotcha section** into `.claude/skills/canicode-gotchas/SKILL.md` in the user's project. Read the existing file, then either replace the section whose `Design key` matches `survey.designKey` (the canonical identifier the gotcha-survey response carries — see `/canicode-gotchas` Step 4a) or append a new numbered section under `# Collected Gotchas`. Never modify anything above the `# Collected Gotchas` heading — the region above it (frontmatter + workflow prose) is the skill loader contract installed by `canicode init`. See the `/canicode-gotchas` skill's "Upsert the gotcha section" step (Step 4) for the exact section format and matching rule.
|
|
116
164
|
|
|
117
165
|
Then proceed to **Step 4** to apply answers to the Figma design.
|
|
118
166
|
|
|
119
167
|
### Step 4: Apply gotcha answers to Figma design
|
|
120
168
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
For each answered gotcha (skip questions answered with "skip" or "n/a"), branch on the pre-computed `question.applyStrategy`. The routing table, target properties, and instance-child resolution are resolved server-side by `canicode` — do NOT re-derive them from the rule id.
|
|
169
|
+
For each answered gotcha (skip questions answered with "skip" or "n/a"), branch on the pre-computed `question.applyStrategy`. The routing table, target properties, and instance-child resolution are resolved server-side by `canicode` — do NOT re-derive them from the rule id. The `fileKey` is not needed at this step — the bundled helpers operate on `nodeId` directly.
|
|
124
170
|
|
|
125
171
|
Use the **`nodeId` from the answered question**. When `question.isInstanceChild` is `true`, treat layout and size-constraint changes as **high impact**: applying them on the source definition affects **every instance** of that component in the file. Ask for explicit user confirmation before writing to the definition node.
|
|
126
172
|
|
|
@@ -244,6 +290,12 @@ The probe is read-only and idempotent; running it before the picker adds one rou
|
|
|
244
290
|
- `applyPropertyMod(question, answerValue, { categories, allowDefinitionWrite, telemetry })` — Strategy A entry point. Branches on `targetProperty` (single vs array) and answer shape (scalar, per-property object, `{ variable: "name" }` binding). Uses `setBoundVariableForPaint` for `fills` / `strokes` and `setBoundVariable` for scalar fields. Passes the full context through to `applyWithInstanceFallback`.
|
|
245
291
|
- `resolveVariableByName(name)` — local-variable exact-name lookup; returns `null` for remote library variables not imported into this file.
|
|
246
292
|
- `probeDefinitionWritability(questions)` — async pre-flight (#357). Returns `{ totalCount, unwritableCount, unwritableSourceNames, allUnwritable, partiallyUnwritable }`. Use BEFORE the Definition write picker so the picker can drop the opt-in branch when every candidate is in an external library / unresolved (saves the user a wasted "I opted in, why did I get annotations?" decision). Read-only probe, dedupes by `sourceChildId`.
|
|
293
|
+
- `extractAcknowledgmentsFromNode(node, canicodeCategoryIds?)` — synchronous pure helper (#371). Reads one node's annotations and returns `{ nodeId, ruleId }[]` for entries gated by canicode `categoryId` plus a recognisable `— *<ruleId>*` footer (or legacy `**[canicode] <ruleId>**` prefix). When `canicodeCategoryIds` is omitted, footer-text matching alone is sufficient (test mode).
|
|
294
|
+
- `readCanicodeAcknowledgments(rootNodeId, categories?)` — async tree walker (#371). Loads `rootNodeId` via `figma.getNodeByIdAsync`, recurses through `children`, and accumulates one acknowledgment per recognised entry. Used at the top of Step 5a to harvest the side channel that lets the analysis pipeline distinguish "still broken" from "the designer has a plan" — pass the result straight to `analyze({ acknowledgments })`. Errors on individual nodes are swallowed so locked / external nodes don't abort the sweep.
|
|
295
|
+
- `computeRoundtripTally({ stepFourReport, reanalyzeResponse })` — pure helper (#383). Takes the structured Step 4 outcome counts (`{ resolved, annotated, definitionWritten, skipped }`) plus a narrowed re-analyze view (`{ issueCount, acknowledgedCount }`) and returns `{ X, Y, Z, W, N, V, V_ack, V_open }`. Replaces the LLM-side emoji-bullet re-counting in Step 5 — render the returned object directly into the wrap-up templates. Throws when `acknowledgedCount > issueCount` (impossible state).
|
|
296
|
+
- `applyAutoFix(issue, { categories, allowDefinitionWrite?, telemetry? })` — Strategy D entry point (#386). Branches on `targetProperty === "name" && suggestedName` once: renames the node via `applyWithInstanceFallback` (so naming auto-fixes share the same tier-1/2/3 policy as Strategy A) or writes a `categories.flag` annotation carrying `issue.message` and `issue.annotationProperties`. Returns one `AutoFixOutcome` (`{ outcome, nodeId, nodeName, ruleId, label }`) where `outcome` is `🔧` / `🌐` / `📝` so Step 4 can bump the structured `stepFourReport` counters without parsing prose. Replaces the inline JS the SKILL used to carry (per ADR-016).
|
|
297
|
+
- `applyAutoFixes(issues, { categories, allowDefinitionWrite?, telemetry? })` — loop wrapper (#386). Filters `issues` to `applyStrategy === "auto-fix"` (skipped entries surface as `⏭️` outcomes for symmetry) and applies each one in sequence. Returns the full `AutoFixOutcome[]`.
|
|
298
|
+
- `removeCanicodeAnnotations(annotations, categories)` — pure filter. Returns `annotations` with every canicode-authored entry removed (gates on `categories.gotcha` / `flag` / `fallback` / `legacyAutoFix` plus the legacy `**[canicode]` body prefix). Use after `stripAnnotations` in the Step 5 cleanup loop — replaces the inline filter predicate the SKILL used to carry. `isCanicodeAnnotation(annotation, categories)` is the single-entry version, exported for callers that need the predicate alone.
|
|
247
299
|
|
|
248
300
|
Keep each `writeFn` small so a throw does not abort unrelated writes. Experiment 08 findings informed every branch in the bundled helpers, and the batch-level confirmation contract still applies *when opting in*: if the orchestrator passes `allowDefinitionWrite: true`, it must have already collected one confirmation covering every potential definition write in the batch. Under the default, no confirmation is needed — the helper annotates the scene instead of propagating.
|
|
249
301
|
|
|
@@ -259,6 +311,7 @@ await CanICodeRoundtrip.applyPropertyMod(question, answerValue, { categories });
|
|
|
259
311
|
|
|
260
312
|
**Replicas (#356)** — when `question.replicaNodeIds` is present, the same answer must land on every replica instance. Iterate the merged set so each scene gets its own per-node failure routing (under the ADR-012 default each replica annotates independently; with `allowDefinitionWrite: true` they share the one definition write because they share the source):
|
|
261
313
|
|
|
314
|
+
<!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside applyPropertyMod -->
|
|
262
315
|
```javascript
|
|
263
316
|
const targets = [question.nodeId, ...(question.replicaNodeIds ?? [])];
|
|
264
317
|
for (const nodeId of targets) {
|
|
@@ -281,6 +334,23 @@ The name must match **the variable's `name` field exactly** — including any sl
|
|
|
281
334
|
|
|
282
335
|
Rules with `applyStrategy === "structural-mod"`. Show the proposed change and **ask for user confirmation** before applying.
|
|
283
336
|
|
|
337
|
+
> **Instance-child guard (#368).** Strategy B mutations restructure the layer tree — `createComponentFromNode`, `flatten`, wrapper removal, instance-link reconnection. None of these compose safely with the Plugin API's instance-override rules: on a node where `question.isInstanceChild === true`, calling `createComponentFromNode` either throws *"Cannot create a component from a node inside an instance"* or detaches the parent instance entirely (the picked instance is replaced by a one-off frame, severing every existing override and propagation link). Restructuring deep-nested wrappers inside an instance child has the same risk surface — even when the call doesn't throw, the resulting structure cannot ride the source-component's propagation in future updates.
|
|
338
|
+
>
|
|
339
|
+
> Before showing the per-rule prompt below, check `question.isInstanceChild`. If it is true, **do not run the destructive call**. Surface this a/b prompt instead and default to **(a)**:
|
|
340
|
+
>
|
|
341
|
+
> ```
|
|
342
|
+
> **{ruleId}** would normally restructure **{nodeName}** here, but this node lives inside instance **{instanceContext.parentInstanceNodeId}** of source component **{instanceContext.sourceComponentName or sourceComponentId or "unknown"}** (definition node `{instanceContext.sourceNodeId}`). On instance children Plugin API restructuring either fails outright or detaches the parent instance.
|
|
343
|
+
>
|
|
344
|
+
> ❯ a) Annotate the scene with a recommendation to apply the change on the source definition (safe — picks up via canicode-gotchas in code-gen, source designer can act on it later)
|
|
345
|
+
> b) Detach the parent instance and attempt the restructuring on the resulting one-off frame (destructive — every existing instance override is lost and the node no longer rides the source component's propagation)
|
|
346
|
+
> ```
|
|
347
|
+
>
|
|
348
|
+
> On **(a)**, route to Strategy C — call `upsertCanicodeAnnotation(scene, { ruleId: question.ruleId, markdown: "**Q:** … **A:** Apply on source definition `${instanceContext.sourceNodeId}` (`${instanceContext.sourceComponentName ?? "unknown"}`) — instance-child restructuring would detach the parent instance.", categoryId: categories.gotcha })`. Reference `instanceContext.sourceComponentName` and `instanceContext.sourceNodeId` in the body so the source designer can locate the target.
|
|
349
|
+
>
|
|
350
|
+
> On **(b)**, gate behind a second confirmation that explicitly names the side effects ("This will detach instance **{parentInstanceNodeId}** — all overrides on it will be lost and it will stop receiving updates from **{sourceComponentName}**. Type the parent instance name to confirm."). Only then execute the per-rule destructive call below.
|
|
351
|
+
>
|
|
352
|
+
> The same posture as ADR-012's `allowDefinitionWrite: false` default: instance-child structural mutations are off-by-default and require explicit user opt-in *per node*, not per batch — the destructive call here doesn't have a quiet fallback the way Strategy A's `applyWithInstanceFallback` does.
|
|
353
|
+
|
|
284
354
|
**`non-layout-container`** — Convert Group/Section to Auto Layout frame:
|
|
285
355
|
- Prompt: "I'll convert **{nodeName}** to an Auto Layout frame with {direction} layout and {spacing}px gap. Proceed?"
|
|
286
356
|
- If confirmed: `applyPropertyMod(question, { layoutMode: "VERTICAL", itemSpacing: 12 })`.
|
|
@@ -303,12 +373,13 @@ if (scene && scene.type === "FRAME") {
|
|
|
303
373
|
- Prompt: "I'll reconnect **{nodeName}** to its original component. Any overrides will be preserved. Proceed?"
|
|
304
374
|
- Requires finding the original component — if not identifiable, fall back to annotation.
|
|
305
375
|
|
|
306
|
-
If the user **declines** any structural modification, add an annotation instead (same as Strategy C).
|
|
376
|
+
If the user **declines** any structural modification (or the instance-child guard above routes to **(a)**), add an annotation instead (same as Strategy C).
|
|
307
377
|
|
|
308
378
|
#### Strategy C: Annotation — record on the design for designer reference
|
|
309
379
|
|
|
310
380
|
Rules with `applyStrategy === "annotation"` cannot be auto-fixed via Plugin API. Add the gotcha answer as a Figma annotation so designers see it in Dev Mode. Use the helper — it handles the D1 mutex, D2 in-place upsert, and D4 category assignment. When `question.replicaNodeIds` is present (#356), iterate the merged set so every replica instance gets the annotation:
|
|
311
381
|
|
|
382
|
+
<!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside upsertCanicodeAnnotation -->
|
|
312
383
|
```javascript
|
|
313
384
|
const targets = [question.nodeId, ...(question.replicaNodeIds ?? [])];
|
|
314
385
|
for (const nodeId of targets) {
|
|
@@ -331,39 +402,15 @@ Notes:
|
|
|
331
402
|
|
|
332
403
|
#### Strategy D: Auto-fix lower-severity issues from analysis
|
|
333
404
|
|
|
334
|
-
The gotcha survey covers only blocking/risk severity. Lower-severity rules appear in `analyzeResult.issues[]` without a survey question. Each issue carries the same pre-computed fields (`applyStrategy`, `targetProperty`, `annotationProperties`, `suggestedName`, `isInstanceChild`, `sourceChildId`).
|
|
405
|
+
The gotcha survey covers only blocking/risk severity. Lower-severity rules appear in `analyzeResult.issues[]` without a survey question. Each issue carries the same pre-computed fields (`applyStrategy`, `targetProperty`, `annotationProperties`, `suggestedName`, `isInstanceChild`, `sourceChildId`). The bundled helper handles the loop, the filter (`applyStrategy === "auto-fix"`), the naming-vs-annotation branch, and the per-issue outcome accumulator in one call:
|
|
335
406
|
|
|
336
407
|
```javascript
|
|
337
|
-
|
|
338
|
-
if (issue.applyStrategy !== "auto-fix") continue;
|
|
339
|
-
|
|
340
|
-
// Shape an ad-hoc question-like object so the same helpers apply.
|
|
341
|
-
const q = {
|
|
342
|
-
nodeId: issue.nodeId,
|
|
343
|
-
ruleId: issue.ruleId,
|
|
344
|
-
...(issue.sourceChildId ? { sourceChildId: issue.sourceChildId } : {}),
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
if (issue.targetProperty === "name" && issue.suggestedName) {
|
|
348
|
-
// Naming rules — rename to the pre-computed suggestedName.
|
|
349
|
-
await CanICodeRoundtrip.applyWithInstanceFallback(q, async (target) => {
|
|
350
|
-
if (target) target.name = issue.suggestedName;
|
|
351
|
-
}, { categories });
|
|
352
|
-
} else {
|
|
353
|
-
// raw-value, missing-interaction-state, missing-prototype — designer judgment; annotate.
|
|
354
|
-
const scene = await figma.getNodeByIdAsync(issue.nodeId);
|
|
355
|
-
CanICodeRoundtrip.upsertCanicodeAnnotation(scene, {
|
|
356
|
-
ruleId: issue.ruleId,
|
|
357
|
-
markdown: issue.message,
|
|
358
|
-
categoryId: categories.flag,
|
|
359
|
-
// Optional: surface the live value for the affected property in Dev Mode.
|
|
360
|
-
properties: issue.annotationProperties,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
}
|
|
408
|
+
const outcomes = await CanICodeRoundtrip.applyAutoFixes(analyzeResult.issues, { categories });
|
|
364
409
|
```
|
|
365
410
|
|
|
366
|
-
`
|
|
411
|
+
`outcomes` is an array of `{ outcome, nodeId, nodeName, ruleId, label }`. `outcome` is one of `🔧` (rename succeeded), `🌐` (definition write propagated — only when `allowDefinitionWrite: true`), `📝` (annotation written, including the fallback path), or `⏭️` (issue's `applyStrategy` was not `"auto-fix"` so it was skipped). Bump the matching `stepFourReport` counter for each entry — `🔧` → `resolved`, `🌐` → `definitionWritten`, `📝` → `annotated`, `⏭️` → `skipped` — so the Step 5 tally (`CanICodeRoundtrip.computeRoundtripTally`, #383) consumes the same structured shape as Strategies A/B/C.
|
|
412
|
+
|
|
413
|
+
`suggestedName` is already capitalized for direct Plugin-API use (e.g. `"Hover"`, `"Default"`, `"Pressed"`). The helper writes it through `applyWithInstanceFallback` so locked / read-only / instance-override nodes annotate cleanly instead of aborting the batch — see the source at `src/core/roundtrip/apply-auto-fix.ts` (#386, ADR-016).
|
|
367
414
|
|
|
368
415
|
#### Execution order
|
|
369
416
|
|
|
@@ -373,7 +420,7 @@ for (const issue of analyzeResult.issues) {
|
|
|
373
420
|
3. **Batch all annotations** (Strategy C + declined structural mods) into a single `use_figma` call — use `categories.gotcha` for the category id.
|
|
374
421
|
4. **Batch all auto-fixes and annotations for lower-severity issues** (Strategy D) — use `categories.flag` for annotated ones (renamed from `autoFix` per #355 — the category means "flagged for designer attention", not "fixed"), `categories.fallback` is reserved for errors surfaced by `applyWithInstanceFallback` itself.
|
|
375
422
|
|
|
376
|
-
After applying,
|
|
423
|
+
After applying, **emit a structured `stepFourReport`** alongside the human-readable per-question lines. Step 5 reads from this object — it does **not** re-parse the per-question lines (per ADR-016). Increment each counter as Strategy A/B/C/D complete:
|
|
377
424
|
|
|
378
425
|
```
|
|
379
426
|
Applied {N} changes to the Figma design:
|
|
@@ -385,29 +432,73 @@ Applied {N} changes to the Figma design:
|
|
|
385
432
|
- 📝 {nodeName}: annotation added to canicode:gotcha (absolute-position-in-auto-layout)
|
|
386
433
|
- 🔧 {nodeName}: auto-fixed to "Hover" (non-standard-naming)
|
|
387
434
|
- 📝 {nodeName}: annotation added to canicode:flag — raw color needs token binding (raw-value)
|
|
435
|
+
|
|
436
|
+
stepFourReport = {
|
|
437
|
+
resolved: <count of ✅ + 🔧 + 🔗 lines>, // scene writes, auto-fix renames, variable bindings
|
|
438
|
+
annotated: <count of 📝 lines>, // including ⏭️ declines that fell back to annotation
|
|
439
|
+
definitionWritten: <count of 🌐 lines>, // only non-zero with allowDefinitionWrite: true
|
|
440
|
+
skipped: <count of ⏭️ lines + Step 3 skip/n/a> // user-declined questions
|
|
441
|
+
}
|
|
388
442
|
```
|
|
389
443
|
|
|
444
|
+
Hold `stepFourReport` in scope through Step 5 — it is the input to `CanICodeRoundtrip.computeRoundtripTally` below.
|
|
445
|
+
|
|
390
446
|
### Step 5: Re-analyze and report what the roundtrip addressed
|
|
391
447
|
|
|
392
|
-
|
|
448
|
+
#### Step 5a: Harvest canicode-authored annotations as acknowledgments (#371)
|
|
393
449
|
|
|
450
|
+
Before re-running `analyze`, collect every `(nodeId, ruleId)` pair that Step 4 wrote as a Figma annotation. The REST API does not expose annotations, so this side channel is the only way the analysis pipeline learns that a roundtrip-touched issue is "the designer has a plan" rather than "still broken". Without it the grade and issue count look identical to the pre-roundtrip state — `32 → 32` — even when every gotcha has been captured per ADR-012.
|
|
451
|
+
|
|
452
|
+
Run a short `use_figma` batch that walks the same subtree the original `analyze` covered (`targetNodeId` if you used one, else `figma.root.id`), reads canicode-categorised annotations, and serialises the result:
|
|
453
|
+
|
|
454
|
+
```javascript
|
|
455
|
+
// Inside a use_figma batch:
|
|
456
|
+
const categories = await CanICodeRoundtrip.ensureCanicodeCategories();
|
|
457
|
+
const acknowledgments = await CanICodeRoundtrip.readCanicodeAcknowledgments(
|
|
458
|
+
targetNodeId ?? figma.root.id,
|
|
459
|
+
categories
|
|
460
|
+
);
|
|
461
|
+
return { events: [], acknowledgments };
|
|
394
462
|
```
|
|
395
|
-
|
|
463
|
+
|
|
464
|
+
`readCanicodeAcknowledgments` walks `node.children` recursively, gates on the `canicode:gotcha` / `canicode:flag` / `canicode:fallback` (and legacy `canicode:auto-fix`) category ids, and extracts the ruleId from the annotation footer (`— *<ruleId>*`) or the legacy `**[canicode] <ruleId>**` prefix. The categoryId guard keeps user-authored notes that happen to end in italic kebab-case from being mistaken for canicode acknowledgments.
|
|
465
|
+
|
|
466
|
+
#### Step 5b: Re-analyze with acknowledgments
|
|
467
|
+
|
|
468
|
+
Pass the harvested array straight into `analyze` so the engine flags matching issues as `acknowledged: true` and the density score gives them half weight:
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
analyze({ input: "<figma-url>", acknowledgments })
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Without canicode MCP** — the CLI accepts the same input via `--acknowledgments <path>` (JSON file containing the array). Write the array to a temp file from the `use_figma` return, then:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
npx canicode analyze "<figma-url>" --json --acknowledgments /tmp/canicode-acks.json
|
|
396
478
|
```
|
|
397
479
|
|
|
398
|
-
|
|
480
|
+
The response now carries:
|
|
481
|
+
- `acknowledgedCount` (top level) — how many issues matched an acknowledgment.
|
|
482
|
+
- `issues[i].acknowledged: true` (per matched issue) — survives into the report and downstream skills.
|
|
483
|
+
- `summary` text — when `acknowledgedCount > 0`, the Total line reads `Total: N (A acknowledged via canicode annotations / N-A unaddressed)`.
|
|
399
484
|
|
|
400
|
-
|
|
401
|
-
- `X` (✅ resolved): count of ✅ + 🔧 + 🔗 markers from the Step 4 report block you just emitted (scene/instance-child writes, auto-fix renames, and variable bindings all successfully landed the value).
|
|
402
|
-
- `Y` (📝 annotated): count of 📝 markers from Step 4 — gotcha answers captured as Figma annotations for code-gen reference.
|
|
403
|
-
- `Z` (🌐 definition writes): count of 🌐 markers from Step 4 — only non-zero when the orchestrator opted in with `allowDefinitionWrite: true` (helper context option, not a CLI flag).
|
|
404
|
-
- `W` (⏭️ skipped): count of ⏭️ markers from Step 4 plus any Step 3 questions the user answered with `skip` or `n/a`.
|
|
405
|
-
- `V` (remaining): `issues.length` from the re-analyze response — unresolved gotchas plus non-actionable rules still flagged by the design.
|
|
406
|
-
- `N` (addressed) = `X + Y + Z + W`.
|
|
485
|
+
Under ADR-012's annotate-by-default policy, most instance-child gotchas route to 📝 annotations and do **not** move the numeric grade — but the half-weight density now produces a small visible movement when annotations are recognised. The headline for this step remains the **issues-delta** (what the roundtrip captured); grade movement is a secondary signal.
|
|
407
486
|
|
|
408
|
-
|
|
487
|
+
**Tally** — call `CanICodeRoundtrip.computeRoundtripTally` with the structured `stepFourReport` you assembled in Step 4 and the re-analyze response from Step 5b. The helper handles every count derivation (`N = X + Y + Z + W`, `V_open = V - V_ack`) and validates that `acknowledgedCount` cannot exceed `issueCount`. Render the returned `{ X, Y, Z, W, N, V, V_ack, V_open }` straight into the templates below — do **not** re-derive any of these from the Step 4 prose:
|
|
409
488
|
|
|
410
|
-
|
|
489
|
+
```javascript
|
|
490
|
+
const tally = CanICodeRoundtrip.computeRoundtripTally({
|
|
491
|
+
stepFourReport, // the object emitted at the end of Step 4
|
|
492
|
+
reanalyzeResponse: { // narrowed view of the re-analyze response
|
|
493
|
+
issueCount: response.issueCount,
|
|
494
|
+
acknowledgedCount: response.acknowledgedCount,
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
If Step 4 produced no `stepFourReport` (e.g. user skipped every question, or no gotcha survey ran), pass an all-zero object — `tally.N === 0`, `tally.V_open === tally.V`, and the templates below render the breakdown with zeros rather than treating it as an error. (Skipping Step 5a and passing no `acknowledgments` argument is also valid in this case — the response simply has `acknowledgedCount: 0`.)
|
|
500
|
+
|
|
501
|
+
**All gotcha issues resolved** (`V == 0`, i.e. re-analyze surfaces no remaining issues — note this is mostly independent of grade since ADR-012 annotations only move the score by the half-weight reduction enabled in Step 5b):
|
|
411
502
|
- Tell the user (fill in the counts from the tally above):
|
|
412
503
|
|
|
413
504
|
```
|
|
@@ -421,18 +512,16 @@ If Step 4 produced no report block (e.g. user skipped every question, or no gotc
|
|
|
421
512
|
|
|
422
513
|
Grade: {oldGrade} → {newGrade}. Ready for code generation.
|
|
423
514
|
```
|
|
424
|
-
- Clean up canicode annotations on fixed nodes via `use_figma`.
|
|
515
|
+
- Clean up canicode annotations on fixed nodes via `use_figma`. Use the bundled `removeCanicodeAnnotations` helper — it gates on **categoryId** (the durable canicode-side identifier — the body no longer carries a `[canicode]` prefix per #353), includes `legacyAutoFix` if `ensureCanicodeCategories` returned it (pre-#355 `canicode:auto-fix` sweep), and also matches the legacy `**[canicode]` body prefix as a secondary marker for entries on files that have not been re-roundtripped yet. The match logic lives in `src/core/roundtrip/remove-canicode-annotations.ts` with vitest coverage so prose stays ADR-016-compliant:
|
|
516
|
+
<!-- adr-016-ack: fan-out over an explicit small array of node IDs; the deterministic work lives inside removeCanicodeAnnotations -->
|
|
425
517
|
```javascript
|
|
426
|
-
const canicodeIds = new Set(
|
|
427
|
-
[categories.gotcha, categories.flag, categories.fallback, categories.legacyAutoFix].filter(Boolean)
|
|
428
|
-
);
|
|
429
518
|
const nodeIds = ["id1", "id2"]; // nodes that now pass
|
|
430
519
|
for (const id of nodeIds) {
|
|
431
520
|
const node = await figma.getNodeByIdAsync(id);
|
|
432
521
|
if (node && "annotations" in node) {
|
|
433
|
-
node.annotations = CanICodeRoundtrip.
|
|
434
|
-
|
|
435
|
-
|
|
522
|
+
node.annotations = CanICodeRoundtrip.removeCanicodeAnnotations(
|
|
523
|
+
CanICodeRoundtrip.stripAnnotations(node.annotations),
|
|
524
|
+
categories,
|
|
436
525
|
);
|
|
437
526
|
}
|
|
438
527
|
}
|
|
@@ -440,7 +529,7 @@ for (const id of nodeIds) {
|
|
|
440
529
|
- Proceed to **Step 6**.
|
|
441
530
|
|
|
442
531
|
**Some issues remain** (`V > 0`):
|
|
443
|
-
- Show the same breakdown and ask whether to proceed:
|
|
532
|
+
- Show the same breakdown and ask whether to proceed. When `V_ack > 0`, expand the remaining line into the acknowledged/unaddressed split surfaced by the re-analyze (#371) so the user can see how much of `V` is "captured for code-gen" vs "still on the user's plate":
|
|
444
533
|
|
|
445
534
|
```
|
|
446
535
|
Roundtrip complete — N issues addressed:
|
|
@@ -449,10 +538,14 @@ for (const id of nodeIds) {
|
|
|
449
538
|
🌐 Z definition writes propagated (only when allowDefinitionWrite: true)
|
|
450
539
|
⏭️ W skipped (user declined or "skip")
|
|
451
540
|
—
|
|
452
|
-
V issues remaining
|
|
541
|
+
V issues remaining
|
|
542
|
+
↳ V_ack acknowledged via canicode annotations (carried into code-gen)
|
|
543
|
+
↳ V_open unaddressed (no annotation — your follow-up backlog)
|
|
453
544
|
|
|
454
545
|
Grade: {oldGrade} → {newGrade}. Proceed to code generation with remaining context?
|
|
455
546
|
```
|
|
547
|
+
|
|
548
|
+
When `V_ack == 0` (re-analyze returned `acknowledgedCount: 0`), keep the single `V issues remaining (unresolved gotchas + non-actionable rules)` line.
|
|
456
549
|
- If yes → proceed to **Step 6** with remaining gotcha context.
|
|
457
550
|
- If no → stop and emit the **Stop wrap-up** below; do **not** restate the grade as the lead.
|
|
458
551
|
|
|
@@ -466,11 +559,15 @@ Stopped — N issues addressed, V remaining for manual follow-up:
|
|
|
466
559
|
📝 Y annotated on Figma (carried into code-gen via canicode-gotchas)
|
|
467
560
|
🌐 Z definition writes propagated
|
|
468
561
|
⏭️ W skipped
|
|
562
|
+
—
|
|
563
|
+
V remaining
|
|
564
|
+
↳ V_ack acknowledged via canicode annotations
|
|
565
|
+
↳ V_open unaddressed
|
|
469
566
|
|
|
470
567
|
Grade: {oldGrade} → {newGrade}.
|
|
471
568
|
```
|
|
472
569
|
|
|
473
|
-
Anti-pattern (do **not** lead with a grade-only sentence like "Grade: C → C+. Most size-constraint gotchas are now annotations…"). Lead with the delta block; mention grade once, on its own footnote line, plain prose only.
|
|
570
|
+
When `V_ack == 0`, drop the `↳` lines and leave a single `V remaining` row. Anti-pattern (do **not** lead with a grade-only sentence like "Grade: C → C+. Most size-constraint gotchas are now annotations…"). Lead with the delta block; mention grade once, on its own footnote line, plain prose only.
|
|
474
571
|
|
|
475
572
|
### Step 6: Implement with Figma MCP
|
|
476
573
|
|
|
@@ -481,7 +578,7 @@ Follow the **figma-implement-design** skill workflow to generate code from the F
|
|
|
481
578
|
- Gotchas with severity **blocking** MUST be addressed — the design cannot be implemented correctly without this information
|
|
482
579
|
- Gotchas with severity **risk** SHOULD be addressed — they indicate potential issues that will surface later
|
|
483
580
|
- Reference the specific node IDs from gotcha answers to locate the affected elements in the design
|
|
484
|
-
- Pass the Figma URL
|
|
581
|
+
- Pass the Figma URL or `survey.designKey` to `figma-implement-design` so it can grep the matching `## #NNN — …` section in `.claude/skills/canicode-gotchas/SKILL.md` instead of reading the whole accumulated file
|
|
485
582
|
|
|
486
583
|
**If all issues were resolved in Steps 4-5**, no additional gotcha context is needed — the design speaks for itself.
|
|
487
584
|
|
|
@@ -497,11 +594,15 @@ Roundtrip complete — N issues addressed, code generated:
|
|
|
497
594
|
⏭️ W skipped
|
|
498
595
|
—
|
|
499
596
|
V issues remaining
|
|
597
|
+
↳ V_ack acknowledged via canicode annotations
|
|
598
|
+
↳ V_open unaddressed
|
|
500
599
|
|
|
501
600
|
Grade: {oldGrade} → {newGrade}.
|
|
502
601
|
Code: <files generated / next-step pointer from figma-implement-design>
|
|
503
602
|
```
|
|
504
603
|
|
|
604
|
+
(Drop the `↳` lines when `V_ack == 0`.)
|
|
605
|
+
|
|
505
606
|
## Edge Cases
|
|
506
607
|
|
|
507
608
|
- **No canicode MCP server**: Fall back to `npx canicode analyze --json` and `npx canicode gotcha-survey --json` — both CLI commands return the same shape as the MCP tools. The Figma MCP is still required for `use_figma` in Step 4; there is no CLI fallback for Figma design edits.
|