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
|
@@ -272,10 +272,11 @@ ${footer}`;
|
|
|
272
272
|
if (!id) continue;
|
|
273
273
|
if (verdict.has(id)) continue;
|
|
274
274
|
const node = await figma.getNodeByIdAsync(id);
|
|
275
|
-
const
|
|
275
|
+
const writability = resolveWritability(node);
|
|
276
|
+
const isUnwritable = writability.isUnwritable;
|
|
276
277
|
verdict.set(id, isUnwritable ? "unwritable" : "writable");
|
|
277
278
|
if (isUnwritable) {
|
|
278
|
-
const name = typeof node?.name === "string" && node.name || q.instanceContext?.sourceComponentName || id;
|
|
279
|
+
const name = typeof writability.componentName === "string" && writability.componentName || typeof node?.name === "string" && node.name || q.instanceContext?.sourceComponentName || id;
|
|
279
280
|
if (!seenName.has(name)) {
|
|
280
281
|
seenName.add(name);
|
|
281
282
|
unwritableNames.push(name);
|
|
@@ -293,11 +294,226 @@ ${footer}`;
|
|
|
293
294
|
partiallyUnwritable: unwritableCount > 0 && unwritableCount < totalCount
|
|
294
295
|
};
|
|
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
|
+
}
|
|
296
505
|
|
|
506
|
+
exports.applyAutoFix = applyAutoFix;
|
|
507
|
+
exports.applyAutoFixes = applyAutoFixes;
|
|
297
508
|
exports.applyPropertyMod = applyPropertyMod;
|
|
298
509
|
exports.applyWithInstanceFallback = applyWithInstanceFallback;
|
|
510
|
+
exports.computeRoundtripTally = computeRoundtripTally;
|
|
299
511
|
exports.ensureCanicodeCategories = ensureCanicodeCategories;
|
|
512
|
+
exports.extractAcknowledgmentsFromNode = extractAcknowledgmentsFromNode;
|
|
513
|
+
exports.isCanicodeAnnotation = isCanicodeAnnotation;
|
|
300
514
|
exports.probeDefinitionWritability = probeDefinitionWritability;
|
|
515
|
+
exports.readCanicodeAcknowledgments = readCanicodeAcknowledgments;
|
|
516
|
+
exports.removeCanicodeAnnotations = removeCanicodeAnnotations;
|
|
301
517
|
exports.resolveVariableByName = resolveVariableByName;
|
|
302
518
|
exports.stripAnnotations = stripAnnotations;
|
|
303
519
|
exports.upsertCanicodeAnnotation = upsertCanicodeAnnotation;
|