canicode 0.10.2 → 0.10.4
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 +36 -31
- package/dist/cli/index.js +401 -22
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +261 -3
- package/dist/index.js +102 -10
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +200 -24
- package/dist/mcp/server.js.map +1 -1
- package/package.json +3 -2
- package/skills/canicode-gotchas/SKILL.md +66 -28
- package/skills/canicode-roundtrip/SKILL.md +290 -86
- package/skills/canicode-roundtrip/helpers.js +270 -10
|
@@ -29,28 +29,39 @@ var CanICodeRoundtrip = (function (exports) {
|
|
|
29
29
|
byLabel.set(label, created.id);
|
|
30
30
|
return created.id;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
const result = {
|
|
33
33
|
gotcha: await ensure("canicode:gotcha", "blue"),
|
|
34
|
-
|
|
34
|
+
flag: await ensure("canicode:flag", "green"),
|
|
35
35
|
fallback: await ensure("canicode:fallback", "yellow")
|
|
36
36
|
};
|
|
37
|
+
const legacyAutoFix = byLabel.get("canicode:auto-fix");
|
|
38
|
+
if (legacyAutoFix) result.legacyAutoFix = legacyAutoFix;
|
|
39
|
+
return result;
|
|
37
40
|
}
|
|
38
41
|
function upsertCanicodeAnnotation(node, input) {
|
|
39
42
|
if (!node || !("annotations" in node)) return false;
|
|
40
43
|
const { ruleId, markdown, categoryId, properties } = input;
|
|
41
|
-
const
|
|
42
|
-
const
|
|
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}
|
|
43
52
|
|
|
44
|
-
${
|
|
53
|
+
${footer}`;
|
|
45
54
|
const existing = stripAnnotations(node.annotations);
|
|
46
55
|
const entry = { labelMarkdown: body };
|
|
47
56
|
if (categoryId) entry.categoryId = categoryId;
|
|
48
57
|
if (properties && properties.length > 0) entry.properties = properties;
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
);
|
|
54
65
|
if (idx >= 0) existing[idx] = entry;
|
|
55
66
|
else existing.push(entry);
|
|
56
67
|
try {
|
|
@@ -251,9 +262,258 @@ ${markdown}`;
|
|
|
251
262
|
);
|
|
252
263
|
}
|
|
253
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;
|
|
254
508
|
exports.applyPropertyMod = applyPropertyMod;
|
|
255
509
|
exports.applyWithInstanceFallback = applyWithInstanceFallback;
|
|
510
|
+
exports.computeRoundtripTally = computeRoundtripTally;
|
|
256
511
|
exports.ensureCanicodeCategories = ensureCanicodeCategories;
|
|
512
|
+
exports.extractAcknowledgmentsFromNode = extractAcknowledgmentsFromNode;
|
|
513
|
+
exports.isCanicodeAnnotation = isCanicodeAnnotation;
|
|
514
|
+
exports.probeDefinitionWritability = probeDefinitionWritability;
|
|
515
|
+
exports.readCanicodeAcknowledgments = readCanicodeAcknowledgments;
|
|
516
|
+
exports.removeCanicodeAnnotations = removeCanicodeAnnotations;
|
|
257
517
|
exports.resolveVariableByName = resolveVariableByName;
|
|
258
518
|
exports.stripAnnotations = stripAnnotations;
|
|
259
519
|
exports.upsertCanicodeAnnotation = upsertCanicodeAnnotation;
|