canicode 0.10.3 → 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.
@@ -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 isUnwritable = node === null || node.remote === true;
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;