canicode 0.10.4 → 0.11.0
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 +9 -2
- package/dist/cli/index.js +559 -141
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +254 -19
- package/dist/index.js +256 -73
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +242 -81
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +39 -3
- package/package.json +1 -1
- package/skills/canicode-gotchas/SKILL.md +17 -14
- package/skills/canicode-roundtrip/SKILL.md +12 -11
- package/skills/canicode-roundtrip/helpers.js +1 -1
- package/skills/cursor/canicode/SKILL.md +76 -0
- package/skills/cursor/canicode-gotchas/SKILL.md +199 -0
- package/skills/cursor/canicode-roundtrip/SKILL.md +618 -0
- package/skills/cursor/canicode-roundtrip/helpers.js +523 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
var CanICodeRoundtrip = (function (exports) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// src/core/roundtrip/annotations.ts
|
|
5
|
+
function stripAnnotations(annotations) {
|
|
6
|
+
const input = annotations ?? [];
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const a of input) {
|
|
9
|
+
const hasLM = typeof a.labelMarkdown === "string" && a.labelMarkdown.length > 0;
|
|
10
|
+
const hasLabel = typeof a.label === "string" && a.label.length > 0;
|
|
11
|
+
if (!hasLM && !hasLabel) continue;
|
|
12
|
+
const base = hasLM ? { labelMarkdown: a.labelMarkdown } : { label: a.label };
|
|
13
|
+
if (a.categoryId) base.categoryId = a.categoryId;
|
|
14
|
+
if (Array.isArray(a.properties) && a.properties.length > 0) {
|
|
15
|
+
base.properties = a.properties;
|
|
16
|
+
}
|
|
17
|
+
out.push(base);
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
async function ensureCanicodeCategories() {
|
|
22
|
+
const api = figma.annotations;
|
|
23
|
+
const existing = await api.getAnnotationCategoriesAsync();
|
|
24
|
+
const byLabel = new Map(existing.map((c) => [c.label, c.id]));
|
|
25
|
+
async function ensure(label, color) {
|
|
26
|
+
const cached = byLabel.get(label);
|
|
27
|
+
if (cached) return cached;
|
|
28
|
+
const created = await api.addAnnotationCategoryAsync({ label, color });
|
|
29
|
+
byLabel.set(label, created.id);
|
|
30
|
+
return created.id;
|
|
31
|
+
}
|
|
32
|
+
const result = {
|
|
33
|
+
gotcha: await ensure("canicode:gotcha", "blue"),
|
|
34
|
+
flag: await ensure("canicode:flag", "green"),
|
|
35
|
+
fallback: await ensure("canicode:fallback", "yellow")
|
|
36
|
+
};
|
|
37
|
+
const legacyAutoFix = byLabel.get("canicode:auto-fix");
|
|
38
|
+
if (legacyAutoFix) result.legacyAutoFix = legacyAutoFix;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function upsertCanicodeAnnotation(node, input) {
|
|
42
|
+
if (!node || !("annotations" in node)) return false;
|
|
43
|
+
const { ruleId, markdown, categoryId, properties } = input;
|
|
44
|
+
const legacyPrefix = `**[canicode] ${ruleId}**`;
|
|
45
|
+
const footer = `\u2014 *${ruleId}*`;
|
|
46
|
+
let bodyText = markdown;
|
|
47
|
+
if (bodyText.startsWith(legacyPrefix)) {
|
|
48
|
+
bodyText = bodyText.slice(legacyPrefix.length).replace(/^\s*\n+/, "");
|
|
49
|
+
}
|
|
50
|
+
const trimmed = bodyText.replace(/\s+$/, "");
|
|
51
|
+
const body = trimmed.endsWith(footer) ? trimmed : `${trimmed}
|
|
52
|
+
|
|
53
|
+
${footer}`;
|
|
54
|
+
const existing = stripAnnotations(node.annotations);
|
|
55
|
+
const entry = { labelMarkdown: body };
|
|
56
|
+
if (categoryId) entry.categoryId = categoryId;
|
|
57
|
+
if (properties && properties.length > 0) entry.properties = properties;
|
|
58
|
+
const matchesRuleId = (text) => {
|
|
59
|
+
if (typeof text !== "string") return false;
|
|
60
|
+
return text.startsWith(legacyPrefix) || text.includes(footer);
|
|
61
|
+
};
|
|
62
|
+
const idx = existing.findIndex(
|
|
63
|
+
(a) => matchesRuleId(a.labelMarkdown) || matchesRuleId(a.label)
|
|
64
|
+
);
|
|
65
|
+
if (idx >= 0) existing[idx] = entry;
|
|
66
|
+
else existing.push(entry);
|
|
67
|
+
try {
|
|
68
|
+
node.annotations = existing;
|
|
69
|
+
return true;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
const msg = String(e?.message ?? e);
|
|
72
|
+
const isNodeTypeReject = /invalid property .+ for a .+ node/i.test(msg);
|
|
73
|
+
if (!entry.properties || !isNodeTypeReject) throw e;
|
|
74
|
+
delete entry.properties;
|
|
75
|
+
if (idx >= 0) existing[idx] = entry;
|
|
76
|
+
node.annotations = existing;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/core/roundtrip/apply-with-instance-fallback.ts
|
|
82
|
+
var DEFINITION_WRITE_SKIPPED_EVENT = "cic_roundtrip_definition_write_skipped";
|
|
83
|
+
function resolveSourceComponentName(definition, question) {
|
|
84
|
+
if (definition && typeof definition.name === "string" && definition.name) {
|
|
85
|
+
return definition.name;
|
|
86
|
+
}
|
|
87
|
+
const ic = question.instanceContext;
|
|
88
|
+
if (ic && typeof ic.sourceComponentName === "string" && ic.sourceComponentName) {
|
|
89
|
+
return ic.sourceComponentName;
|
|
90
|
+
}
|
|
91
|
+
return "the source component";
|
|
92
|
+
}
|
|
93
|
+
async function routeToDefinitionOrAnnotate(definition, writeFn, ctx) {
|
|
94
|
+
if (definition && !ctx.allowDefinitionWrite && ctx.reason !== "non-override-error") {
|
|
95
|
+
const componentName = resolveSourceComponentName(definition, ctx.question);
|
|
96
|
+
if (ctx.categories) {
|
|
97
|
+
upsertCanicodeAnnotation(ctx.scene, {
|
|
98
|
+
ruleId: ctx.question.ruleId,
|
|
99
|
+
markdown: `The fix below could not be applied on this instance child \u2014 the property silently ignored the write or the override was rejected. Apply it on the source component **${componentName}** so every instance picks it up. Re-run with \`allowDefinitionWrite: true\` to let canicode propagate automatically.`,
|
|
100
|
+
categoryId: ctx.categories.fallback
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
ctx.telemetry?.(DEFINITION_WRITE_SKIPPED_EVENT, {
|
|
104
|
+
ruleId: ctx.question.ruleId,
|
|
105
|
+
reason: ctx.reason
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
icon: "\u{1F4DD}",
|
|
109
|
+
label: "definition write skipped (opt-in disabled)"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (!definition) {
|
|
113
|
+
if (ctx.categories) {
|
|
114
|
+
const markdown = ctx.reason === "silent-ignore" ? "write accepted but value unchanged; no definition available" : ctx.reason === "override-error" ? `could not apply automatically: ${ctx.errorMessage ?? ""}` : `could not apply automatically: ${ctx.errorMessage ?? ""}`;
|
|
115
|
+
upsertCanicodeAnnotation(ctx.scene, {
|
|
116
|
+
ruleId: ctx.question.ruleId,
|
|
117
|
+
markdown,
|
|
118
|
+
categoryId: ctx.categories.fallback
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return ctx.reason === "silent-ignore" ? { icon: "\u{1F4DD}", label: "silent-ignore, annotated" } : { icon: "\u{1F4DD}", label: `error: ${ctx.errorMessage ?? ""}` };
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await writeFn(definition);
|
|
125
|
+
return {
|
|
126
|
+
icon: "\u{1F310}",
|
|
127
|
+
label: ctx.reason === "silent-ignore" ? "source definition (silent-ignore fallback)" : "source definition"
|
|
128
|
+
};
|
|
129
|
+
} catch (defErr) {
|
|
130
|
+
const defMsg = String(defErr?.message ?? defErr);
|
|
131
|
+
const isRemoteReadOnly = definition.remote === true || /read-only/i.test(defMsg);
|
|
132
|
+
if (ctx.categories) {
|
|
133
|
+
upsertCanicodeAnnotation(ctx.scene, {
|
|
134
|
+
ruleId: ctx.question.ruleId,
|
|
135
|
+
markdown: isRemoteReadOnly ? "source component lives in an external library and is read-only from this file \u2014 apply the fix in the library file itself." : `could not apply at source definition: ${defMsg}`,
|
|
136
|
+
categoryId: ctx.categories.fallback
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
icon: "\u{1F4DD}",
|
|
141
|
+
label: isRemoteReadOnly ? "external library (read-only)" : `definition error: ${defMsg}`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function applyWithInstanceFallback(question, writeFn, context = {}) {
|
|
146
|
+
const { categories, allowDefinitionWrite = false, telemetry } = context;
|
|
147
|
+
const scene = await figma.getNodeByIdAsync(question.nodeId);
|
|
148
|
+
if (!scene) return { icon: "\u{1F4DD}", label: "missing node" };
|
|
149
|
+
const definition = question.sourceChildId ? await figma.getNodeByIdAsync(question.sourceChildId) : null;
|
|
150
|
+
try {
|
|
151
|
+
const changed = await writeFn(scene);
|
|
152
|
+
if (changed === false) {
|
|
153
|
+
return routeToDefinitionOrAnnotate(definition, writeFn, {
|
|
154
|
+
question,
|
|
155
|
+
scene,
|
|
156
|
+
categories,
|
|
157
|
+
reason: "silent-ignore",
|
|
158
|
+
allowDefinitionWrite,
|
|
159
|
+
telemetry
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return { icon: "\u2705", label: "instance/scene" };
|
|
163
|
+
} catch (e) {
|
|
164
|
+
const msg = String(e?.message ?? e);
|
|
165
|
+
const looksLikeInstanceOverride = /cannot be overridden/i.test(msg) || /override/i.test(msg);
|
|
166
|
+
if (!looksLikeInstanceOverride) {
|
|
167
|
+
return routeToDefinitionOrAnnotate(null, writeFn, {
|
|
168
|
+
question,
|
|
169
|
+
scene,
|
|
170
|
+
categories,
|
|
171
|
+
reason: "non-override-error",
|
|
172
|
+
errorMessage: msg,
|
|
173
|
+
allowDefinitionWrite,
|
|
174
|
+
telemetry
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return routeToDefinitionOrAnnotate(definition, writeFn, {
|
|
178
|
+
question,
|
|
179
|
+
scene,
|
|
180
|
+
categories,
|
|
181
|
+
reason: "override-error",
|
|
182
|
+
errorMessage: msg,
|
|
183
|
+
allowDefinitionWrite,
|
|
184
|
+
telemetry
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/core/roundtrip/apply-property-mod.ts
|
|
190
|
+
async function resolveVariableByName(name) {
|
|
191
|
+
const locals = await figma.variables.getLocalVariablesAsync();
|
|
192
|
+
return locals.find((v) => v.name === name) ?? null;
|
|
193
|
+
}
|
|
194
|
+
function parseValue(raw) {
|
|
195
|
+
if (raw && typeof raw === "object" && "variable" in raw) {
|
|
196
|
+
const v = raw;
|
|
197
|
+
const parsed = { kind: "binding", name: v.variable };
|
|
198
|
+
if ("fallback" in v) parsed.fallback = v.fallback;
|
|
199
|
+
return parsed;
|
|
200
|
+
}
|
|
201
|
+
if (raw && typeof raw === "object" && "fallback" in raw) {
|
|
202
|
+
return { kind: "scalar", scalar: raw.fallback };
|
|
203
|
+
}
|
|
204
|
+
return { kind: "scalar", scalar: raw };
|
|
205
|
+
}
|
|
206
|
+
function isPaintProp(prop) {
|
|
207
|
+
return prop === "fills" || prop === "strokes";
|
|
208
|
+
}
|
|
209
|
+
function applyPropertyBinding(target, prop, variable) {
|
|
210
|
+
if (isPaintProp(prop)) {
|
|
211
|
+
const current = target[prop];
|
|
212
|
+
if (current === figma.mixed || !Array.isArray(current)) return false;
|
|
213
|
+
const paints = current;
|
|
214
|
+
const bound = paints.map(
|
|
215
|
+
(paint) => figma.variables.setBoundVariableForPaint(paint, "color", variable)
|
|
216
|
+
);
|
|
217
|
+
target[prop] = bound;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
target.setBoundVariable(prop, variable);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
function applyPropertyScalar(target, prop, scalar) {
|
|
224
|
+
const rec = target;
|
|
225
|
+
const before = rec[prop];
|
|
226
|
+
rec[prop] = scalar;
|
|
227
|
+
if (rec[prop] === before && before !== scalar) return false;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
async function applyPropertyMod(question, answerValue, context = {}) {
|
|
231
|
+
const props = Array.isArray(question.targetProperty) ? question.targetProperty : question.targetProperty !== void 0 ? [question.targetProperty] : [];
|
|
232
|
+
return applyWithInstanceFallback(
|
|
233
|
+
question,
|
|
234
|
+
async (target) => {
|
|
235
|
+
if (!target) return void 0;
|
|
236
|
+
let changed = void 0;
|
|
237
|
+
for (const prop of props) {
|
|
238
|
+
if (!(prop in target)) continue;
|
|
239
|
+
const perProp = answerValue && typeof answerValue === "object" && !("variable" in answerValue) && !Array.isArray(answerValue) ? answerValue[prop] : answerValue;
|
|
240
|
+
const parsed = parseValue(perProp);
|
|
241
|
+
if (parsed.kind === "binding") {
|
|
242
|
+
const variable = await resolveVariableByName(parsed.name);
|
|
243
|
+
if (variable) {
|
|
244
|
+
applyPropertyBinding(target, prop, variable);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (parsed.fallback !== void 0) {
|
|
248
|
+
if (!applyPropertyScalar(target, prop, parsed.fallback)) {
|
|
249
|
+
changed = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (parsed.scalar === void 0) continue;
|
|
255
|
+
if (!applyPropertyScalar(target, prop, parsed.scalar)) {
|
|
256
|
+
changed = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return changed;
|
|
260
|
+
},
|
|
261
|
+
context
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/core/roundtrip/probe-definition-writability.ts
|
|
266
|
+
async function probeDefinitionWritability(questions) {
|
|
267
|
+
const verdict = /* @__PURE__ */ new Map();
|
|
268
|
+
const unwritableNames = [];
|
|
269
|
+
const seenName = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const q of questions) {
|
|
271
|
+
const id = q.sourceChildId;
|
|
272
|
+
if (!id) continue;
|
|
273
|
+
if (verdict.has(id)) continue;
|
|
274
|
+
const node = await figma.getNodeByIdAsync(id);
|
|
275
|
+
const writability = resolveWritability(node);
|
|
276
|
+
const isUnwritable = writability.isUnwritable;
|
|
277
|
+
verdict.set(id, isUnwritable ? "unwritable" : "writable");
|
|
278
|
+
if (isUnwritable) {
|
|
279
|
+
const name = typeof writability.componentName === "string" && writability.componentName || typeof node?.name === "string" && node.name || q.instanceContext?.sourceComponentName || id;
|
|
280
|
+
if (!seenName.has(name)) {
|
|
281
|
+
seenName.add(name);
|
|
282
|
+
unwritableNames.push(name);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const totalCount = verdict.size;
|
|
287
|
+
let unwritableCount = 0;
|
|
288
|
+
for (const v of verdict.values()) if (v === "unwritable") unwritableCount++;
|
|
289
|
+
return {
|
|
290
|
+
totalCount,
|
|
291
|
+
unwritableCount,
|
|
292
|
+
unwritableSourceNames: unwritableNames,
|
|
293
|
+
allUnwritable: totalCount > 0 && unwritableCount === totalCount,
|
|
294
|
+
partiallyUnwritable: unwritableCount > 0 && unwritableCount < totalCount
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function resolveWritability(node) {
|
|
298
|
+
if (node === null) return { isUnwritable: true };
|
|
299
|
+
if ("remote" in node && typeof node.remote === "boolean") {
|
|
300
|
+
return { isUnwritable: node.remote === true };
|
|
301
|
+
}
|
|
302
|
+
const containing = findContainingComponent(node);
|
|
303
|
+
if (!containing) {
|
|
304
|
+
return { isUnwritable: false };
|
|
305
|
+
}
|
|
306
|
+
const isUnwritable = "remote" in containing && containing.remote === true;
|
|
307
|
+
return {
|
|
308
|
+
isUnwritable,
|
|
309
|
+
...isUnwritable && typeof containing.name === "string" ? { componentName: containing.name } : {}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function findContainingComponent(node) {
|
|
313
|
+
let cur = node;
|
|
314
|
+
for (let i = 0; i < 100 && cur; i++) {
|
|
315
|
+
if (cur.type === "COMPONENT" || cur.type === "COMPONENT_SET") return cur;
|
|
316
|
+
cur = cur.parent ?? null;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/core/roundtrip/read-acknowledgments.ts
|
|
322
|
+
var FOOTER_RE = /—\s+\*([A-Za-z0-9-]+)\*\s*$/;
|
|
323
|
+
var LEGACY_PREFIX_RE = /^\*\*\[canicode\]\s+([A-Za-z0-9-]+)\*\*/;
|
|
324
|
+
function extractAcknowledgmentsFromNode(node, canicodeCategoryIds) {
|
|
325
|
+
if (!node || !("annotations" in node)) return [];
|
|
326
|
+
const annotations = node.annotations ?? [];
|
|
327
|
+
if (annotations.length === 0) return [];
|
|
328
|
+
const out = [];
|
|
329
|
+
for (const a of annotations) {
|
|
330
|
+
const text = (typeof a.labelMarkdown === "string" && a.labelMarkdown.length > 0 ? a.labelMarkdown : "") || (typeof a.label === "string" && a.label.length > 0 ? a.label : "");
|
|
331
|
+
if (!text) continue;
|
|
332
|
+
if (canicodeCategoryIds) {
|
|
333
|
+
if (!a.categoryId || !canicodeCategoryIds.has(a.categoryId)) continue;
|
|
334
|
+
}
|
|
335
|
+
const ruleId = extractRuleId(text);
|
|
336
|
+
if (!ruleId) continue;
|
|
337
|
+
out.push({ nodeId: node.id, ruleId });
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
function extractRuleId(text) {
|
|
342
|
+
const footer = FOOTER_RE.exec(text);
|
|
343
|
+
if (footer) return footer[1] ?? null;
|
|
344
|
+
const legacy = LEGACY_PREFIX_RE.exec(text);
|
|
345
|
+
if (legacy) return legacy[1] ?? null;
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
async function readCanicodeAcknowledgments(rootNodeId, categories) {
|
|
349
|
+
const root = await figma.getNodeByIdAsync(rootNodeId);
|
|
350
|
+
if (!root) return [];
|
|
351
|
+
const canicodeCategoryIds = categories ? new Set(
|
|
352
|
+
[
|
|
353
|
+
categories.gotcha,
|
|
354
|
+
categories.flag,
|
|
355
|
+
categories.fallback,
|
|
356
|
+
categories.legacyAutoFix
|
|
357
|
+
].filter((id) => typeof id === "string" && id.length > 0)
|
|
358
|
+
) : void 0;
|
|
359
|
+
const out = [];
|
|
360
|
+
walk(root, canicodeCategoryIds, out);
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
function walk(node, canicodeCategoryIds, out) {
|
|
364
|
+
try {
|
|
365
|
+
const local = extractAcknowledgmentsFromNode(node, canicodeCategoryIds);
|
|
366
|
+
for (const a of local) out.push(a);
|
|
367
|
+
} catch {
|
|
368
|
+
}
|
|
369
|
+
const children = node.children;
|
|
370
|
+
if (Array.isArray(children)) {
|
|
371
|
+
for (const child of children) {
|
|
372
|
+
if (child && typeof child === "object") walk(child, canicodeCategoryIds, out);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/core/roundtrip/compute-roundtrip-tally.ts
|
|
378
|
+
function computeRoundtripTally(args) {
|
|
379
|
+
const { stepFourReport, reanalyzeResponse } = args;
|
|
380
|
+
const { resolved, annotated, definitionWritten, skipped } = stepFourReport;
|
|
381
|
+
const { issueCount, acknowledgedCount } = reanalyzeResponse;
|
|
382
|
+
if (acknowledgedCount > issueCount) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`computeRoundtripTally: reanalyzeResponse.acknowledgedCount (${acknowledgedCount}) cannot exceed issueCount (${issueCount}). Acknowledged issues are a subset of remaining issues.`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
X: resolved,
|
|
389
|
+
Y: annotated,
|
|
390
|
+
Z: definitionWritten,
|
|
391
|
+
W: skipped,
|
|
392
|
+
N: resolved + annotated + definitionWritten + skipped,
|
|
393
|
+
V: issueCount,
|
|
394
|
+
V_ack: acknowledgedCount,
|
|
395
|
+
V_open: issueCount - acknowledgedCount
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/core/roundtrip/apply-auto-fix.ts
|
|
400
|
+
function pickNodeName(issue, resolved) {
|
|
401
|
+
if (resolved && typeof resolved.name === "string" && resolved.name.length > 0) {
|
|
402
|
+
return resolved.name;
|
|
403
|
+
}
|
|
404
|
+
if (typeof issue.nodePath === "string" && issue.nodePath.length > 0) {
|
|
405
|
+
const segments = issue.nodePath.split(/\s*[›>/]\s*/);
|
|
406
|
+
const tail = segments[segments.length - 1];
|
|
407
|
+
if (tail && tail.length > 0) return tail;
|
|
408
|
+
}
|
|
409
|
+
return issue.nodeId;
|
|
410
|
+
}
|
|
411
|
+
function mapInstanceFallbackIcon(result) {
|
|
412
|
+
if (result.icon === "\u2705") return "\u{1F527}";
|
|
413
|
+
return result.icon;
|
|
414
|
+
}
|
|
415
|
+
async function applyAutoFix(issue, context) {
|
|
416
|
+
const { categories } = context;
|
|
417
|
+
const ruleId = issue.ruleId;
|
|
418
|
+
if (issue.targetProperty === "name" && typeof issue.suggestedName === "string") {
|
|
419
|
+
const suggestedName = issue.suggestedName;
|
|
420
|
+
const question = {
|
|
421
|
+
nodeId: issue.nodeId,
|
|
422
|
+
ruleId,
|
|
423
|
+
...issue.sourceChildId ? { sourceChildId: issue.sourceChildId } : {}
|
|
424
|
+
};
|
|
425
|
+
const result = await applyWithInstanceFallback(
|
|
426
|
+
question,
|
|
427
|
+
(target) => {
|
|
428
|
+
if (target) {
|
|
429
|
+
target.name = suggestedName;
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
categories,
|
|
434
|
+
...context.allowDefinitionWrite !== void 0 ? { allowDefinitionWrite: context.allowDefinitionWrite } : {},
|
|
435
|
+
...context.telemetry !== void 0 ? { telemetry: context.telemetry } : {}
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
const sceneAfter = await figma.getNodeByIdAsync(issue.nodeId);
|
|
439
|
+
return {
|
|
440
|
+
outcome: mapInstanceFallbackIcon(result),
|
|
441
|
+
nodeId: issue.nodeId,
|
|
442
|
+
nodeName: pickNodeName(issue, sceneAfter),
|
|
443
|
+
ruleId,
|
|
444
|
+
label: result.label
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const scene = await figma.getNodeByIdAsync(issue.nodeId);
|
|
448
|
+
const markdown = issue.message ?? `Auto-flagged: ${ruleId}`;
|
|
449
|
+
if (scene) {
|
|
450
|
+
upsertCanicodeAnnotation(scene, {
|
|
451
|
+
ruleId,
|
|
452
|
+
markdown,
|
|
453
|
+
categoryId: categories.flag,
|
|
454
|
+
...issue.annotationProperties && issue.annotationProperties.length > 0 ? { properties: issue.annotationProperties } : {}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
outcome: "\u{1F4DD}",
|
|
459
|
+
nodeId: issue.nodeId,
|
|
460
|
+
nodeName: pickNodeName(issue, scene),
|
|
461
|
+
ruleId,
|
|
462
|
+
label: scene ? `annotation added to canicode:flag \u2014 ${ruleId}` : `missing node (annotation skipped) \u2014 ${ruleId}`
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async function applyAutoFixes(issues, context) {
|
|
466
|
+
const out = [];
|
|
467
|
+
for (const issue of issues) {
|
|
468
|
+
if (issue.applyStrategy !== "auto-fix") {
|
|
469
|
+
out.push({
|
|
470
|
+
outcome: "\u23ED\uFE0F",
|
|
471
|
+
nodeId: issue.nodeId,
|
|
472
|
+
nodeName: pickNodeName(issue, null),
|
|
473
|
+
ruleId: issue.ruleId,
|
|
474
|
+
label: `skipped \u2014 applyStrategy is ${issue.applyStrategy ?? "absent"}`
|
|
475
|
+
});
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
out.push(await applyAutoFix(issue, context));
|
|
479
|
+
}
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/core/roundtrip/remove-canicode-annotations.ts
|
|
484
|
+
var LEGACY_CANICODE_PREFIX = "**[canicode]";
|
|
485
|
+
function isCanicodeAnnotation(annotation, categories) {
|
|
486
|
+
const canicodeIds = new Set(
|
|
487
|
+
[
|
|
488
|
+
categories.gotcha,
|
|
489
|
+
categories.flag,
|
|
490
|
+
categories.fallback,
|
|
491
|
+
categories.legacyAutoFix
|
|
492
|
+
].filter((id) => Boolean(id))
|
|
493
|
+
);
|
|
494
|
+
if (annotation.categoryId && canicodeIds.has(annotation.categoryId)) {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
if (annotation.labelMarkdown?.startsWith(LEGACY_CANICODE_PREFIX)) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
function removeCanicodeAnnotations(annotations, categories) {
|
|
503
|
+
return annotations.filter((a) => !isCanicodeAnnotation(a, categories));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
exports.applyAutoFix = applyAutoFix;
|
|
507
|
+
exports.applyAutoFixes = applyAutoFixes;
|
|
508
|
+
exports.applyPropertyMod = applyPropertyMod;
|
|
509
|
+
exports.applyWithInstanceFallback = applyWithInstanceFallback;
|
|
510
|
+
exports.computeRoundtripTally = computeRoundtripTally;
|
|
511
|
+
exports.ensureCanicodeCategories = ensureCanicodeCategories;
|
|
512
|
+
exports.extractAcknowledgmentsFromNode = extractAcknowledgmentsFromNode;
|
|
513
|
+
exports.isCanicodeAnnotation = isCanicodeAnnotation;
|
|
514
|
+
exports.probeDefinitionWritability = probeDefinitionWritability;
|
|
515
|
+
exports.readCanicodeAcknowledgments = readCanicodeAcknowledgments;
|
|
516
|
+
exports.removeCanicodeAnnotations = removeCanicodeAnnotations;
|
|
517
|
+
exports.resolveVariableByName = resolveVariableByName;
|
|
518
|
+
exports.stripAnnotations = stripAnnotations;
|
|
519
|
+
exports.upsertCanicodeAnnotation = upsertCanicodeAnnotation;
|
|
520
|
+
|
|
521
|
+
return exports;
|
|
522
|
+
|
|
523
|
+
})({});
|