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.
@@ -29,28 +29,39 @@ var CanICodeRoundtrip = (function (exports) {
29
29
  byLabel.set(label, created.id);
30
30
  return created.id;
31
31
  }
32
- return {
32
+ const result = {
33
33
  gotcha: await ensure("canicode:gotcha", "blue"),
34
- autoFix: await ensure("canicode:auto-fix", "green"),
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 prefix = `**[canicode] ${ruleId}**`;
42
- const body = markdown.startsWith(prefix) ? markdown : `${prefix}
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
- ${markdown}`;
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 idx = existing.findIndex((a) => {
50
- const lm = a.labelMarkdown;
51
- const lb = a.label;
52
- return typeof lm === "string" && lm.startsWith(prefix) || typeof lb === "string" && lb.startsWith(prefix);
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;