@buoy-design/core 0.1.0 → 0.1.1

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.
@@ -1,39 +1,173 @@
1
- import { createDriftId, normalizeComponentName, normalizeTokenName, tokensMatch, } from '../models/index.js';
1
+ import { createDriftId, normalizeComponentName, normalizeTokenName, tokensMatch, } from "../models/index.js";
2
+ import { TokenSuggestionService, } from "./token-suggestions.js";
3
+ import { stringSimilarity as calcStringSimilarity } from "./string-utils.js";
4
+ import { MATCHING_CONFIG, NAMING_CONFIG, getOutlierThreshold, } from "./config.js";
5
+ /**
6
+ * Semantic suffixes that represent legitimate separate components.
7
+ * Components with these suffixes are NOT duplicates of their base component.
8
+ * e.g., Button vs ButtonGroup are distinct components, not duplicates.
9
+ */
10
+ const SEMANTIC_COMPONENT_SUFFIXES = [
11
+ // Compound component patterns
12
+ "group",
13
+ "list",
14
+ "item",
15
+ "items",
16
+ "container",
17
+ "wrapper",
18
+ "provider",
19
+ "context",
20
+ // Layout parts
21
+ "header",
22
+ "footer",
23
+ "body",
24
+ "content",
25
+ "section",
26
+ "sidebar",
27
+ "panel",
28
+ // Specific UI patterns
29
+ "trigger",
30
+ "target",
31
+ "overlay",
32
+ "portal",
33
+ "root",
34
+ "slot",
35
+ "action",
36
+ "actions",
37
+ "icon",
38
+ "label",
39
+ "text",
40
+ "title",
41
+ "description",
42
+ "separator",
43
+ "divider",
44
+ // Size/state variants that are distinct components
45
+ "small",
46
+ "large",
47
+ "mini",
48
+ "skeleton",
49
+ "placeholder",
50
+ "loading",
51
+ "error",
52
+ "empty",
53
+ // Form-related
54
+ "input",
55
+ "field",
56
+ "control",
57
+ "message",
58
+ "helper",
59
+ "hint",
60
+ // Navigation
61
+ "link",
62
+ "menu",
63
+ "submenu",
64
+ "tab",
65
+ "tabs",
66
+ // Data display
67
+ "cell",
68
+ "row",
69
+ "column",
70
+ "columns",
71
+ "head",
72
+ "view",
73
+ ];
74
+ /**
75
+ * Version/status suffixes that indicate potential duplicates.
76
+ * Components with ONLY these suffixes are likely duplicates.
77
+ * e.g., Button vs ButtonNew, Card vs CardLegacy
78
+ */
79
+ const VERSION_SUFFIXES_PATTERN = /(New|Old|V\d+|Legacy|Updated|Deprecated|Beta|Alpha|Experimental|Next|Previous|Original|Backup|Copy|Clone|Alt|Alternative|Temp|Temporary|WIP|Draft)$/i;
2
80
  export class SemanticDiffEngine {
81
+ // Caches for O(1) lookups instead of repeated computations
82
+ nameCache = new Map();
83
+ componentMetadataCache = new Map();
84
+ // Delegated services
85
+ tokenSuggestionService = new TokenSuggestionService();
86
+ /**
87
+ * Cached version of normalizeComponentName to avoid repeated string operations
88
+ */
89
+ cachedNormalizeName(name) {
90
+ let normalized = this.nameCache.get(name);
91
+ if (normalized === undefined) {
92
+ normalized = normalizeComponentName(name);
93
+ this.nameCache.set(name, normalized);
94
+ }
95
+ return normalized;
96
+ }
97
+ /**
98
+ * Get pre-computed component metadata for faster similarity calculations
99
+ */
100
+ getComponentMetadata(component) {
101
+ let metadata = this.componentMetadataCache.get(component.id);
102
+ if (!metadata) {
103
+ metadata = {
104
+ props: new Set(component.props.map((p) => p.name.toLowerCase())),
105
+ variants: new Set(component.variants.map((v) => v.name.toLowerCase())),
106
+ dependencies: new Set(component.dependencies.map((d) => d.toLowerCase())),
107
+ };
108
+ this.componentMetadataCache.set(component.id, metadata);
109
+ }
110
+ return metadata;
111
+ }
112
+ /**
113
+ * Clear caches to prevent memory leaks between operations
114
+ */
115
+ clearCaches() {
116
+ this.nameCache.clear();
117
+ this.componentMetadataCache.clear();
118
+ }
3
119
  /**
4
120
  * Check for framework sprawl - multiple UI frameworks in one codebase
5
121
  */
6
122
  checkFrameworkSprawl(frameworks) {
7
123
  // Only count UI/component frameworks, not backend frameworks
8
124
  const uiFrameworkNames = [
9
- 'react', 'vue', 'svelte', 'angular', 'solid', 'preact', 'lit', 'stencil',
10
- 'nextjs', 'nuxt', 'astro', 'remix', 'sveltekit', 'gatsby', 'react-native', 'expo', 'flutter'
125
+ "react",
126
+ "vue",
127
+ "svelte",
128
+ "angular",
129
+ "solid",
130
+ "preact",
131
+ "lit",
132
+ "stencil",
133
+ "nextjs",
134
+ "nuxt",
135
+ "astro",
136
+ "remix",
137
+ "sveltekit",
138
+ "gatsby",
139
+ "react-native",
140
+ "expo",
141
+ "flutter",
11
142
  ];
12
- const uiFrameworks = frameworks.filter(f => uiFrameworkNames.includes(f.name));
143
+ const uiFrameworks = frameworks.filter((f) => uiFrameworkNames.includes(f.name));
13
144
  if (uiFrameworks.length <= 1) {
14
145
  return null; // No sprawl
15
146
  }
16
- const frameworkNames = uiFrameworks.map(f => f.name);
147
+ const frameworkNames = uiFrameworks.map((f) => f.name);
17
148
  const primaryFramework = uiFrameworks[0];
18
149
  return {
19
- id: createDriftId('framework-sprawl', 'project', frameworkNames.join('-')),
20
- type: 'framework-sprawl',
21
- severity: 'warning',
150
+ id: createDriftId("framework-sprawl", "project", frameworkNames.join("-")),
151
+ type: "framework-sprawl",
152
+ severity: "warning",
22
153
  source: {
23
- entityType: 'component',
24
- entityId: 'project',
25
- entityName: 'Project Architecture',
26
- location: 'package.json',
154
+ entityType: "component",
155
+ entityId: "project",
156
+ entityName: "Project Architecture",
157
+ location: "package.json",
27
158
  },
28
- message: `Framework sprawl detected: ${uiFrameworks.length} UI frameworks in use (${frameworkNames.join(', ')})`,
159
+ message: `Framework sprawl detected: ${uiFrameworks.length} UI frameworks in use (${frameworkNames.join(", ")})`,
29
160
  details: {
30
161
  expected: `Single framework (${primaryFramework.name})`,
31
162
  actual: `${uiFrameworks.length} frameworks`,
32
- frameworks: uiFrameworks.map(f => ({ name: f.name, version: f.version })),
163
+ frameworks: uiFrameworks.map((f) => ({
164
+ name: f.name,
165
+ version: f.version,
166
+ })),
33
167
  suggestions: [
34
- 'Consider consolidating to a single UI framework',
35
- 'Document intentional multi-framework usage if required',
36
- 'Create migration plan if frameworks are being deprecated',
168
+ "Consider consolidating to a single UI framework",
169
+ "Document intentional multi-framework usage if required",
170
+ "Create migration plan if frameworks are being deprecated",
37
171
  ],
38
172
  },
39
173
  detectedAt: new Date(),
@@ -41,39 +175,60 @@ export class SemanticDiffEngine {
41
175
  }
42
176
  /**
43
177
  * Compare components from different sources (e.g., React vs Figma)
178
+ * Optimized with Map-based indexing for O(n+m) instead of O(n×m)
44
179
  */
45
180
  compareComponents(sourceComponents, targetComponents, options = {}) {
46
181
  const matches = [];
47
182
  const matchedSourceIds = new Set();
48
183
  const matchedTargetIds = new Set();
49
- // Phase 1: Exact name matches
184
+ // Build target component lookup map for O(1) exact matching - O(m) one-time cost
185
+ const targetNameMap = new Map();
186
+ for (const target of targetComponents) {
187
+ const normalizedName = this.cachedNormalizeName(target.name);
188
+ targetNameMap.set(normalizedName, target);
189
+ }
190
+ // Phase 1: Exact name matches - O(n) instead of O(n × m)
50
191
  for (const source of sourceComponents) {
51
- const sourceName = normalizeComponentName(source.name);
52
- const exactMatch = targetComponents.find(t => normalizeComponentName(t.name) === sourceName);
53
- if (exactMatch) {
54
- matches.push(this.createMatch(source, exactMatch, 'exact'));
192
+ const normalizedName = this.cachedNormalizeName(source.name);
193
+ const exactMatch = targetNameMap.get(normalizedName);
194
+ if (exactMatch && !matchedTargetIds.has(exactMatch.id)) {
195
+ matches.push(this.createMatch(source, exactMatch, "exact"));
55
196
  matchedSourceIds.add(source.id);
56
197
  matchedTargetIds.add(exactMatch.id);
57
198
  }
58
199
  }
59
- // Phase 2: Fuzzy matching for remaining
60
- const unmatchedSource = sourceComponents.filter(c => !matchedSourceIds.has(c.id));
61
- const unmatchedTarget = targetComponents.filter(c => !matchedTargetIds.has(c.id));
62
- for (const source of unmatchedSource) {
63
- const bestMatch = this.findBestMatch(source, unmatchedTarget);
64
- const minConfidence = options.minMatchConfidence || 0.7;
200
+ // Phase 2: Fuzzy matching for remaining - optimized candidate tracking
201
+ const unmatchedTargetMap = new Map();
202
+ for (const target of targetComponents) {
203
+ if (!matchedTargetIds.has(target.id)) {
204
+ unmatchedTargetMap.set(target.id, target);
205
+ }
206
+ }
207
+ const minConfidence = options.minMatchConfidence || MATCHING_CONFIG.minMatchConfidence;
208
+ for (const source of sourceComponents) {
209
+ if (matchedSourceIds.has(source.id))
210
+ continue;
211
+ // Convert to array only for remaining unmatched targets
212
+ const candidates = Array.from(unmatchedTargetMap.values());
213
+ const bestMatch = this.findBestMatch(source, candidates);
65
214
  if (bestMatch && bestMatch.confidence >= minConfidence) {
66
215
  matches.push(bestMatch);
67
216
  matchedSourceIds.add(source.id);
68
217
  matchedTargetIds.add(bestMatch.target.id);
218
+ // Remove matched target to prevent re-matching and reduce candidates
219
+ unmatchedTargetMap.delete(bestMatch.target.id);
69
220
  }
70
221
  }
71
222
  // Phase 3: Generate drift signals
72
- const drifts = this.generateComponentDrifts(matches, sourceComponents.filter(c => !matchedSourceIds.has(c.id)), targetComponents.filter(c => !matchedTargetIds.has(c.id)));
223
+ const orphanedSource = sourceComponents.filter((c) => !matchedSourceIds.has(c.id));
224
+ const orphanedTarget = targetComponents.filter((c) => !matchedTargetIds.has(c.id));
225
+ const drifts = this.generateComponentDrifts(matches, orphanedSource, orphanedTarget);
226
+ // Clear caches to prevent memory leaks
227
+ this.clearCaches();
73
228
  return {
74
229
  matches,
75
- orphanedSource: sourceComponents.filter(c => !matchedSourceIds.has(c.id)),
76
- orphanedTarget: targetComponents.filter(c => !matchedTargetIds.has(c.id)),
230
+ orphanedSource,
231
+ orphanedTarget,
77
232
  drifts,
78
233
  };
79
234
  }
@@ -87,7 +242,7 @@ export class SemanticDiffEngine {
87
242
  const matchedTargetIds = new Set();
88
243
  for (const source of sourceTokens) {
89
244
  const sourceName = normalizeTokenName(source.name);
90
- const target = targetTokens.find(t => normalizeTokenName(t.name) === sourceName);
245
+ const target = targetTokens.find((t) => normalizeTokenName(t.name) === sourceName);
91
246
  if (!target)
92
247
  continue;
93
248
  matchedSourceIds.add(source.id);
@@ -96,46 +251,46 @@ export class SemanticDiffEngine {
96
251
  // Check for value divergence
97
252
  if (!tokensMatch(source.value, target.value)) {
98
253
  drifts.push({
99
- id: createDriftId('value-divergence', source.id, target.id),
100
- type: 'value-divergence',
101
- severity: 'warning',
254
+ id: createDriftId("value-divergence", source.id, target.id),
255
+ type: "value-divergence",
256
+ severity: "warning",
102
257
  source: this.tokenToDriftSource(source),
103
258
  target: this.tokenToDriftSource(target),
104
259
  message: `Token "${source.name}" has different values between sources`,
105
260
  details: {
106
261
  expected: source.value,
107
262
  actual: target.value,
108
- suggestions: ['Align token values between design and code'],
263
+ suggestions: ["Align token values between design and code"],
109
264
  },
110
265
  detectedAt: new Date(),
111
266
  });
112
267
  }
113
268
  }
114
269
  // Orphaned tokens
115
- const orphanedSource = sourceTokens.filter(t => !matchedSourceIds.has(t.id));
116
- const orphanedTarget = targetTokens.filter(t => !matchedTargetIds.has(t.id));
270
+ const orphanedSource = sourceTokens.filter((t) => !matchedSourceIds.has(t.id));
271
+ const orphanedTarget = targetTokens.filter((t) => !matchedTargetIds.has(t.id));
117
272
  for (const token of orphanedSource) {
118
273
  drifts.push({
119
- id: createDriftId('orphaned-token', token.id),
120
- type: 'orphaned-token',
121
- severity: 'info',
274
+ id: createDriftId("orphaned-token", token.id),
275
+ type: "orphaned-token",
276
+ severity: "info",
122
277
  source: this.tokenToDriftSource(token),
123
278
  message: `Token "${token.name}" exists in ${token.source.type} but not in design`,
124
279
  details: {
125
- suggestions: ['Add token to design system or remove if unused'],
280
+ suggestions: ["Add token to design system or remove if unused"],
126
281
  },
127
282
  detectedAt: new Date(),
128
283
  });
129
284
  }
130
285
  for (const token of orphanedTarget) {
131
286
  drifts.push({
132
- id: createDriftId('orphaned-token', token.id),
133
- type: 'orphaned-token',
134
- severity: 'info',
287
+ id: createDriftId("orphaned-token", token.id),
288
+ type: "orphaned-token",
289
+ severity: "info",
135
290
  source: this.tokenToDriftSource(token),
136
291
  message: `Token "${token.name}" exists in design but not implemented`,
137
292
  details: {
138
- suggestions: ['Implement token in code or mark as planned'],
293
+ suggestions: ["Implement token in code or mark as planned"],
139
294
  },
140
295
  detectedAt: new Date(),
141
296
  });
@@ -155,14 +310,15 @@ export class SemanticDiffEngine {
155
310
  // Check for deprecation
156
311
  if (options.checkDeprecated && component.metadata.deprecated) {
157
312
  drifts.push({
158
- id: createDriftId('deprecated-pattern', component.id),
159
- type: 'deprecated-pattern',
160
- severity: 'warning',
313
+ id: createDriftId("deprecated-pattern", component.id),
314
+ type: "deprecated-pattern",
315
+ severity: "warning",
161
316
  source: this.componentToDriftSource(component),
162
317
  message: `Component "${component.name}" is marked as deprecated`,
163
318
  details: {
164
319
  suggestions: [
165
- component.metadata.deprecationReason || 'Migrate to recommended alternative',
320
+ component.metadata.deprecationReason ||
321
+ "Migrate to recommended alternative",
166
322
  ],
167
323
  },
168
324
  detectedAt: new Date(),
@@ -173,9 +329,9 @@ export class SemanticDiffEngine {
173
329
  const namingIssue = this.checkNamingConsistency(component.name, namingPatterns);
174
330
  if (namingIssue) {
175
331
  drifts.push({
176
- id: createDriftId('naming-inconsistency', component.id),
177
- type: 'naming-inconsistency',
178
- severity: 'info',
332
+ id: createDriftId("naming-inconsistency", component.id),
333
+ type: "naming-inconsistency",
334
+ severity: "info",
179
335
  source: this.componentToDriftSource(component),
180
336
  message: namingIssue.message,
181
337
  details: {
@@ -190,16 +346,18 @@ export class SemanticDiffEngine {
190
346
  const typeConflict = this.checkPropTypeConsistency(prop, propTypeMap);
191
347
  if (typeConflict) {
192
348
  drifts.push({
193
- id: createDriftId('semantic-mismatch', component.id, prop.name),
194
- type: 'semantic-mismatch',
195
- severity: 'warning',
349
+ id: createDriftId("semantic-mismatch", component.id, prop.name),
350
+ type: "semantic-mismatch",
351
+ severity: "warning",
196
352
  source: this.componentToDriftSource(component),
197
353
  message: `Prop "${prop.name}" in "${component.name}" uses type "${prop.type}" but other components use "${typeConflict.dominantType}"`,
198
354
  details: {
199
355
  expected: typeConflict.dominantType,
200
356
  actual: prop.type,
201
357
  usedIn: typeConflict.examples,
202
- suggestions: ['Standardize prop types across components for consistency'],
358
+ suggestions: [
359
+ "Standardize prop types across components for consistency",
360
+ ],
203
361
  },
204
362
  detectedAt: new Date(),
205
363
  });
@@ -209,9 +367,9 @@ export class SemanticDiffEngine {
209
367
  const propNamingIssues = this.checkPropNamingConsistency(component, propNamingMap);
210
368
  for (const issue of propNamingIssues) {
211
369
  drifts.push({
212
- id: createDriftId('naming-inconsistency', component.id, issue.propName),
213
- type: 'naming-inconsistency',
214
- severity: 'info',
370
+ id: createDriftId("naming-inconsistency", component.id, issue.propName),
371
+ type: "naming-inconsistency",
372
+ severity: "info",
215
373
  source: this.componentToDriftSource(component),
216
374
  message: issue.message,
217
375
  details: {
@@ -225,50 +383,91 @@ export class SemanticDiffEngine {
225
383
  const a11yIssues = this.checkAccessibility(component);
226
384
  for (const issue of a11yIssues) {
227
385
  drifts.push({
228
- id: createDriftId('accessibility-conflict', component.id),
229
- type: 'accessibility-conflict',
230
- severity: 'critical',
386
+ id: createDriftId("accessibility-conflict", component.id),
387
+ type: "accessibility-conflict",
388
+ severity: "critical",
231
389
  source: this.componentToDriftSource(component),
232
390
  message: `Component "${component.name}" has accessibility issues: ${issue}`,
233
391
  details: {
234
- suggestions: ['Fix accessibility issue to ensure inclusive design'],
392
+ suggestions: [
393
+ "Fix accessibility issue to ensure inclusive design",
394
+ ],
235
395
  },
236
396
  detectedAt: new Date(),
237
397
  });
238
398
  }
239
399
  }
240
400
  // Check for hardcoded values that should be tokens
241
- if (component.metadata.hardcodedValues && component.metadata.hardcodedValues.length > 0) {
401
+ if (component.metadata.hardcodedValues &&
402
+ component.metadata.hardcodedValues.length > 0) {
242
403
  const hardcoded = component.metadata.hardcodedValues;
243
- const colorCount = hardcoded.filter(h => h.type === 'color').length;
244
- const spacingCount = hardcoded.filter(h => h.type === 'spacing' || h.type === 'fontSize').length;
404
+ const colorCount = hardcoded.filter((h) => h.type === "color").length;
405
+ const spacingCount = hardcoded.filter((h) => h.type === "spacing" || h.type === "fontSize").length;
406
+ // Generate token suggestions if available tokens provided
407
+ const tokenSuggestions = options.availableTokens
408
+ ? this.generateTokenSuggestions(hardcoded, options.availableTokens)
409
+ : new Map();
245
410
  // Group by type for cleaner messaging
246
411
  if (colorCount > 0) {
247
- const colorValues = hardcoded.filter(h => h.type === 'color');
412
+ const colorValues = hardcoded.filter((h) => h.type === "color");
413
+ // Build actionable suggestions
414
+ const suggestions = [];
415
+ const tokenReplacements = [];
416
+ for (const cv of colorValues) {
417
+ const suggs = tokenSuggestions.get(cv.value);
418
+ if (suggs && suggs.length > 0) {
419
+ const bestMatch = suggs[0];
420
+ tokenReplacements.push(`${cv.value} → ${bestMatch.suggestedToken} (${Math.round(bestMatch.confidence * 100)}% match)`);
421
+ }
422
+ }
423
+ if (tokenReplacements.length > 0) {
424
+ suggestions.push(`Suggested replacements:\n ${tokenReplacements.join("\n ")}`);
425
+ }
426
+ else {
427
+ suggestions.push("Replace hardcoded colors with design tokens (e.g., var(--primary) or theme.colors.primary)");
428
+ }
248
429
  drifts.push({
249
- id: createDriftId('hardcoded-value', component.id, 'color'),
250
- type: 'hardcoded-value',
251
- severity: 'warning',
430
+ id: createDriftId("hardcoded-value", component.id, "color"),
431
+ type: "hardcoded-value",
432
+ severity: "warning",
252
433
  source: this.componentToDriftSource(component),
253
- message: `Component "${component.name}" has ${colorCount} hardcoded color${colorCount > 1 ? 's' : ''}: ${colorValues.map(h => h.value).join(', ')}`,
434
+ message: `Component "${component.name}" has ${colorCount} hardcoded color${colorCount > 1 ? "s" : ""}: ${colorValues.map((h) => h.value).join(", ")}`,
254
435
  details: {
255
- suggestions: ['Replace hardcoded colors with design tokens (e.g., var(--primary) or theme.colors.primary)'],
256
- affectedFiles: colorValues.map(h => `${h.property}: ${h.value} (${h.location})`),
436
+ suggestions,
437
+ affectedFiles: colorValues.map((h) => `${h.property}: ${h.value} (${h.location})`),
438
+ tokenSuggestions: tokenReplacements.length > 0 ? tokenReplacements : undefined,
257
439
  },
258
440
  detectedAt: new Date(),
259
441
  });
260
442
  }
261
443
  if (spacingCount > 0) {
262
- const spacingValues = hardcoded.filter(h => h.type === 'spacing' || h.type === 'fontSize');
444
+ const spacingValues = hardcoded.filter((h) => h.type === "spacing" || h.type === "fontSize");
445
+ // Build actionable suggestions
446
+ const suggestions = [];
447
+ const tokenReplacements = [];
448
+ for (const sv of spacingValues) {
449
+ const suggs = tokenSuggestions.get(sv.value);
450
+ if (suggs && suggs.length > 0) {
451
+ const bestMatch = suggs[0];
452
+ tokenReplacements.push(`${sv.value} → ${bestMatch.suggestedToken} (${Math.round(bestMatch.confidence * 100)}% match)`);
453
+ }
454
+ }
455
+ if (tokenReplacements.length > 0) {
456
+ suggestions.push(`Suggested replacements:\n ${tokenReplacements.join("\n ")}`);
457
+ }
458
+ else {
459
+ suggestions.push("Consider using spacing tokens for consistency");
460
+ }
263
461
  drifts.push({
264
- id: createDriftId('hardcoded-value', component.id, 'spacing'),
265
- type: 'hardcoded-value',
266
- severity: 'info',
462
+ id: createDriftId("hardcoded-value", component.id, "spacing"),
463
+ type: "hardcoded-value",
464
+ severity: "info",
267
465
  source: this.componentToDriftSource(component),
268
- message: `Component "${component.name}" has ${spacingCount} hardcoded size value${spacingCount > 1 ? 's' : ''}: ${spacingValues.map(h => h.value).join(', ')}`,
466
+ message: `Component "${component.name}" has ${spacingCount} hardcoded size value${spacingCount > 1 ? "s" : ""}: ${spacingValues.map((h) => h.value).join(", ")}`,
269
467
  details: {
270
- suggestions: ['Consider using spacing tokens for consistency'],
271
- affectedFiles: spacingValues.map(h => `${h.property}: ${h.value} (${h.location})`),
468
+ suggestions,
469
+ affectedFiles: spacingValues.map((h) => `${h.property}: ${h.value} (${h.location})`),
470
+ tokenSuggestions: tokenReplacements.length > 0 ? tokenReplacements : undefined,
272
471
  },
273
472
  detectedAt: new Date(),
274
473
  });
@@ -280,14 +479,16 @@ export class SemanticDiffEngine {
280
479
  const duplicates = this.detectPotentialDuplicates(components);
281
480
  for (const dup of duplicates) {
282
481
  drifts.push({
283
- id: createDriftId('naming-inconsistency', dup.components[0].id, 'duplicate'),
284
- type: 'naming-inconsistency',
285
- severity: 'warning',
482
+ id: createDriftId("naming-inconsistency", dup.components[0].id, "duplicate"),
483
+ type: "naming-inconsistency",
484
+ severity: "warning",
286
485
  source: this.componentToDriftSource(dup.components[0]),
287
- message: `Potential duplicate components: ${dup.components.map(c => c.name).join(', ')}`,
486
+ message: `Potential duplicate components: ${dup.components.map((c) => c.name).join(", ")}`,
288
487
  details: {
289
- suggestions: ['Consider consolidating these components or clarifying their distinct purposes'],
290
- relatedComponents: dup.components.map(c => c.name),
488
+ suggestions: [
489
+ "Consider consolidating these components or clarifying their distinct purposes",
490
+ ],
491
+ relatedComponents: dup.components.map((c) => c.name),
291
492
  },
292
493
  detectedAt: new Date(),
293
494
  });
@@ -301,7 +502,7 @@ export class SemanticDiffEngine {
301
502
  const patterns = {
302
503
  PascalCase: 0,
303
504
  camelCase: 0,
304
- 'kebab-case': 0,
505
+ "kebab-case": 0,
305
506
  snake_case: 0,
306
507
  other: 0,
307
508
  };
@@ -309,12 +510,13 @@ export class SemanticDiffEngine {
309
510
  const pattern = this.identifyNamingPattern(comp.name);
310
511
  patterns[pattern]++;
311
512
  }
312
- // Find dominant pattern (must be > 60% to be considered dominant)
513
+ // Find dominant pattern (must exceed threshold to be considered dominant)
313
514
  const total = components.length;
314
515
  let dominant = null;
315
516
  let dominantCount = 0;
316
517
  for (const [pattern, count] of Object.entries(patterns)) {
317
- if (count > dominantCount && count / total > 0.6) {
518
+ if (count > dominantCount &&
519
+ count / total > NAMING_CONFIG.dominantPatternThreshold) {
318
520
  dominant = pattern;
319
521
  dominantCount = count;
320
522
  }
@@ -323,14 +525,14 @@ export class SemanticDiffEngine {
323
525
  }
324
526
  identifyNamingPattern(name) {
325
527
  if (/^[A-Z][a-zA-Z0-9]*$/.test(name))
326
- return 'PascalCase';
528
+ return "PascalCase";
327
529
  if (/^[a-z][a-zA-Z0-9]*$/.test(name))
328
- return 'camelCase';
530
+ return "camelCase";
329
531
  if (/^[a-z][a-z0-9-]*$/.test(name))
330
- return 'kebab-case';
532
+ return "kebab-case";
331
533
  if (/^[a-z][a-z0-9_]*$/.test(name))
332
- return 'snake_case';
333
- return 'other';
534
+ return "snake_case";
535
+ return "other";
334
536
  }
335
537
  checkNamingConsistency(name, patterns) {
336
538
  if (!patterns.dominant)
@@ -339,7 +541,7 @@ export class SemanticDiffEngine {
339
541
  if (thisPattern === patterns.dominant)
340
542
  return null;
341
543
  // Only flag if this is a clear outlier
342
- const outlierThreshold = Math.max(3, patterns.total * 0.1); // At least 3 or 10% use dominant
544
+ const outlierThreshold = getOutlierThreshold(patterns.total);
343
545
  if (patterns.patterns[patterns.dominant] < outlierThreshold)
344
546
  return null;
345
547
  return {
@@ -359,7 +561,10 @@ export class SemanticDiffEngine {
359
561
  map.set(normalizedName, { types: new Map(), total: 0 });
360
562
  }
361
563
  const usage = map.get(normalizedName);
362
- const typeCount = usage.types.get(prop.type) || { count: 0, examples: [] };
564
+ const typeCount = usage.types.get(prop.type) || {
565
+ count: 0,
566
+ examples: [],
567
+ };
363
568
  typeCount.count++;
364
569
  if (typeCount.examples.length < 3) {
365
570
  typeCount.examples.push(comp.name);
@@ -375,7 +580,7 @@ export class SemanticDiffEngine {
375
580
  if (!usage || usage.total < 3)
376
581
  return null; // Not enough data
377
582
  // Find dominant type
378
- let dominantType = '';
583
+ let dominantType = "";
379
584
  let dominantCount = 0;
380
585
  for (const [type, data] of usage.types) {
381
586
  if (data.count > dominantCount) {
@@ -383,10 +588,11 @@ export class SemanticDiffEngine {
383
588
  dominantCount = data.count;
384
589
  }
385
590
  }
386
- // Only flag if this prop's type differs and dominant is > 70%
591
+ // Only flag if this prop's type differs and dominant exceeds threshold
387
592
  if (prop.type === dominantType)
388
593
  return null;
389
- if (dominantCount / usage.total < 0.7)
594
+ if (dominantCount / usage.total <
595
+ NAMING_CONFIG.establishedConventionThreshold)
390
596
  return null;
391
597
  const examples = usage.types.get(dominantType)?.examples || [];
392
598
  return { dominantType, examples };
@@ -402,16 +608,16 @@ export class SemanticDiffEngine {
402
608
  for (const comp of components) {
403
609
  for (const prop of comp.props) {
404
610
  const lower = prop.name.toLowerCase();
405
- if (lower.includes('click') || lower.includes('press')) {
611
+ if (lower.includes("click") || lower.includes("press")) {
406
612
  clickHandlers.push(prop.name);
407
613
  }
408
- if (lower.includes('change')) {
614
+ if (lower.includes("change")) {
409
615
  changeHandlers.push(prop.name);
410
616
  }
411
617
  }
412
618
  }
413
- map.set('click', clickHandlers);
414
- map.set('change', changeHandlers);
619
+ map.set("click", clickHandlers);
620
+ map.set("change", changeHandlers);
415
621
  return map;
416
622
  }
417
623
  checkPropNamingConsistency(component, propNamingMap) {
@@ -419,8 +625,8 @@ export class SemanticDiffEngine {
419
625
  for (const prop of component.props) {
420
626
  const lower = prop.name.toLowerCase();
421
627
  // Check click handler naming
422
- if (lower.includes('click') || lower.includes('press')) {
423
- const allClickHandlers = propNamingMap.get('click') || [];
628
+ if (lower.includes("click") || lower.includes("press")) {
629
+ const allClickHandlers = propNamingMap.get("click") || [];
424
630
  if (allClickHandlers.length >= 5) {
425
631
  const dominant = this.findDominantPropPattern(allClickHandlers);
426
632
  if (dominant && !prop.name.startsWith(dominant.prefix)) {
@@ -429,7 +635,7 @@ export class SemanticDiffEngine {
429
635
  issues.push({
430
636
  propName: prop.name,
431
637
  message: `"${prop.name}" in "${component.name}" - ${dominantPct}% of click handlers use "${dominant.prefix}..." pattern`,
432
- suggestion: `Consider using "${dominant.prefix}${prop.name.replace(/^(on|handle)/i, '')}" for consistency`,
638
+ suggestion: `Consider using "${dominant.prefix}${prop.name.replace(/^(on|handle)/i, "")}" for consistency`,
433
639
  });
434
640
  }
435
641
  }
@@ -441,10 +647,10 @@ export class SemanticDiffEngine {
441
647
  findDominantPropPattern(propNames) {
442
648
  const prefixes = {};
443
649
  for (const name of propNames) {
444
- if (name.startsWith('on'))
445
- prefixes['on'] = (prefixes['on'] || 0) + 1;
446
- else if (name.startsWith('handle'))
447
- prefixes['handle'] = (prefixes['handle'] || 0) + 1;
650
+ if (name.startsWith("on"))
651
+ prefixes["on"] = (prefixes["on"] || 0) + 1;
652
+ else if (name.startsWith("handle"))
653
+ prefixes["handle"] = (prefixes["handle"] || 0) + 1;
448
654
  }
449
655
  let dominant = null;
450
656
  for (const [prefix, count] of Object.entries(prefixes)) {
@@ -455,7 +661,9 @@ export class SemanticDiffEngine {
455
661
  return dominant;
456
662
  }
457
663
  /**
458
- * Detect potential duplicate components based on similar names
664
+ * Detect potential duplicate components based on similar names.
665
+ * Only flags true duplicates like Button vs ButtonNew or Card vs CardLegacy.
666
+ * Does NOT flag compound components like Button vs ButtonGroup or Card vs CardHeader.
459
667
  */
460
668
  detectPotentialDuplicates(components) {
461
669
  const duplicates = [];
@@ -463,33 +671,59 @@ export class SemanticDiffEngine {
463
671
  for (const comp of components) {
464
672
  if (processed.has(comp.id))
465
673
  continue;
466
- // Find components with similar base names
467
- const baseName = comp.name
468
- .replace(/(New|Old|V\d+|Legacy|Updated|Deprecated)$/i, '')
469
- .replace(/\d+$/, '')
470
- .toLowerCase();
471
- const similar = components.filter(c => {
674
+ // Extract base name and check if it has a version suffix
675
+ const { baseName, hasVersionSuffix } = this.extractBaseName(comp.name);
676
+ const similar = components.filter((c) => {
472
677
  if (c.id === comp.id)
473
678
  return false;
474
- const otherBase = c.name
475
- .replace(/(New|Old|V\d+|Legacy|Updated|Deprecated)$/i, '')
476
- .replace(/\d+$/, '')
477
- .toLowerCase();
478
- return baseName === otherBase && baseName.length >= 3;
679
+ const other = this.extractBaseName(c.name);
680
+ // Only match if base names are identical
681
+ if (baseName !== other.baseName || baseName.length < 3) {
682
+ return false;
683
+ }
684
+ // At least one component must have a version suffix to be a duplicate
685
+ // This prevents Button vs ButtonGroup from matching
686
+ // But allows Button vs ButtonNew to match
687
+ return hasVersionSuffix || other.hasVersionSuffix;
479
688
  });
480
689
  if (similar.length > 0) {
481
690
  const group = [comp, ...similar];
482
- group.forEach(c => processed.add(c.id));
691
+ group.forEach((c) => processed.add(c.id));
483
692
  duplicates.push({ components: group });
484
693
  }
485
694
  }
486
695
  return duplicates;
487
696
  }
697
+ /**
698
+ * Extract the base name from a component name, stripping version suffixes.
699
+ * Returns whether the name had a version suffix (indicating potential duplicate).
700
+ */
701
+ extractBaseName(name) {
702
+ const lowerName = name.toLowerCase();
703
+ // Check if the name ends with a semantic suffix (legitimate separate component)
704
+ for (const suffix of SEMANTIC_COMPONENT_SUFFIXES) {
705
+ if (lowerName.endsWith(suffix) &&
706
+ lowerName.length > suffix.length &&
707
+ // Ensure the suffix is at a word boundary (e.g., "ButtonGroup" not "Buttong")
708
+ lowerName[lowerName.length - suffix.length - 1]?.match(/[a-z]/)) {
709
+ // This is a compound component, not a duplicate candidate
710
+ // Return the full name as the base (it's distinct)
711
+ return { baseName: lowerName, hasVersionSuffix: false };
712
+ }
713
+ }
714
+ // Check for version suffixes that indicate duplicates
715
+ const hasVersionSuffix = VERSION_SUFFIXES_PATTERN.test(name);
716
+ const strippedName = name
717
+ .replace(VERSION_SUFFIXES_PATTERN, "")
718
+ .replace(/\d+$/, "") // Strip trailing numbers
719
+ .toLowerCase();
720
+ return { baseName: strippedName, hasVersionSuffix };
721
+ }
488
722
  createMatch(source, target, matchType) {
489
723
  return {
490
724
  source,
491
725
  target,
492
- confidence: matchType === 'exact' ? 1 : 0,
726
+ confidence: matchType === "exact" ? 1 : 0,
493
727
  matchType,
494
728
  differences: this.findDifferences(source, target),
495
729
  };
@@ -501,7 +735,7 @@ export class SemanticDiffEngine {
501
735
  const score = this.calculateSimilarity(source, candidate);
502
736
  if (score > bestScore) {
503
737
  bestScore = score;
504
- const matchType = score > 0.9 ? 'similar' : 'partial';
738
+ const matchType = score > MATCHING_CONFIG.similarMatchThreshold ? "similar" : "partial";
505
739
  bestMatch = {
506
740
  source,
507
741
  target: candidate,
@@ -515,67 +749,39 @@ export class SemanticDiffEngine {
515
749
  }
516
750
  calculateSimilarity(a, b) {
517
751
  let score = 0;
518
- const weights = { name: 0.4, props: 0.3, variants: 0.2, dependencies: 0.1 };
752
+ const weights = MATCHING_CONFIG.similarityWeights;
519
753
  // Name similarity (Levenshtein-based)
520
754
  score += weights.name * this.stringSimilarity(a.name, b.name);
521
- // Props overlap
522
- const aProps = new Set(a.props.map(p => p.name.toLowerCase()));
523
- const bProps = new Set(b.props.map(p => p.name.toLowerCase()));
524
- const propsIntersection = [...aProps].filter(p => bProps.has(p)).length;
525
- const propsUnion = new Set([...aProps, ...bProps]).size;
526
- score += weights.props * (propsUnion > 0 ? propsIntersection / propsUnion : 0);
527
- // Variant overlap
528
- const aVariants = new Set(a.variants.map(v => v.name.toLowerCase()));
529
- const bVariants = new Set(b.variants.map(v => v.name.toLowerCase()));
530
- const variantsIntersection = [...aVariants].filter(v => bVariants.has(v)).length;
531
- const variantsUnion = new Set([...aVariants, ...bVariants]).size;
532
- score += weights.variants * (variantsUnion > 0 ? variantsIntersection / variantsUnion : 0);
533
- // Dependencies overlap
534
- const aDeps = new Set(a.dependencies.map(d => d.toLowerCase()));
535
- const bDeps = new Set(b.dependencies.map(d => d.toLowerCase()));
536
- const depsIntersection = [...aDeps].filter(d => bDeps.has(d)).length;
537
- const depsUnion = new Set([...aDeps, ...bDeps]).size;
538
- score += weights.dependencies * (depsUnion > 0 ? depsIntersection / depsUnion : 0);
755
+ // Use pre-computed metadata for faster overlap calculation
756
+ const aMeta = this.getComponentMetadata(a);
757
+ const bMeta = this.getComponentMetadata(b);
758
+ // Props overlap (using pre-computed Sets)
759
+ const propsIntersection = [...aMeta.props].filter((p) => bMeta.props.has(p)).length;
760
+ const propsUnion = new Set([...aMeta.props, ...bMeta.props]).size;
761
+ score +=
762
+ weights.props * (propsUnion > 0 ? propsIntersection / propsUnion : 0);
763
+ // Variant overlap (using pre-computed Sets)
764
+ const variantsIntersection = [...aMeta.variants].filter((v) => bMeta.variants.has(v)).length;
765
+ const variantsUnion = new Set([...aMeta.variants, ...bMeta.variants]).size;
766
+ score +=
767
+ weights.variants *
768
+ (variantsUnion > 0 ? variantsIntersection / variantsUnion : 0);
769
+ // Dependencies overlap (using pre-computed Sets)
770
+ const depsIntersection = [...aMeta.dependencies].filter((d) => bMeta.dependencies.has(d)).length;
771
+ const depsUnion = new Set([...aMeta.dependencies, ...bMeta.dependencies])
772
+ .size;
773
+ score +=
774
+ weights.dependencies * (depsUnion > 0 ? depsIntersection / depsUnion : 0);
539
775
  return score;
540
776
  }
541
777
  stringSimilarity(a, b) {
542
- const maxLen = Math.max(a.length, b.length);
543
- if (maxLen === 0)
544
- return 1;
545
- const distance = this.levenshteinDistance(a.toLowerCase(), b.toLowerCase());
546
- return 1 - distance / maxLen;
547
- }
548
- levenshteinDistance(a, b) {
549
- // Create matrix with proper initialization
550
- const rows = b.length + 1;
551
- const cols = a.length + 1;
552
- const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
553
- // Initialize first column
554
- for (let i = 0; i <= b.length; i++) {
555
- matrix[i][0] = i;
556
- }
557
- // Initialize first row
558
- for (let j = 0; j <= a.length; j++) {
559
- matrix[0][j] = j;
560
- }
561
- // Fill in the rest of the matrix
562
- for (let i = 1; i <= b.length; i++) {
563
- for (let j = 1; j <= a.length; j++) {
564
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
565
- matrix[i][j] = matrix[i - 1][j - 1];
566
- }
567
- else {
568
- matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
569
- }
570
- }
571
- }
572
- return matrix[b.length][a.length];
778
+ return calcStringSimilarity(a.toLowerCase(), b.toLowerCase());
573
779
  }
574
780
  findDifferences(source, target) {
575
781
  const differences = [];
576
782
  // Compare props
577
- const sourceProps = new Map(source.props.map(p => [p.name.toLowerCase(), p]));
578
- const targetProps = new Map(target.props.map(p => [p.name.toLowerCase(), p]));
783
+ const sourceProps = new Map(source.props.map((p) => [p.name.toLowerCase(), p]));
784
+ const targetProps = new Map(target.props.map((p) => [p.name.toLowerCase(), p]));
579
785
  for (const [name, prop] of sourceProps) {
580
786
  const targetProp = targetProps.get(name);
581
787
  if (!targetProp) {
@@ -583,7 +789,7 @@ export class SemanticDiffEngine {
583
789
  field: `props.${prop.name}`,
584
790
  sourceValue: prop,
585
791
  targetValue: undefined,
586
- severity: prop.required ? 'warning' : 'info',
792
+ severity: prop.required ? "warning" : "info",
587
793
  });
588
794
  }
589
795
  else if (prop.type !== targetProp.type) {
@@ -591,7 +797,7 @@ export class SemanticDiffEngine {
591
797
  field: `props.${prop.name}.type`,
592
798
  sourceValue: prop.type,
593
799
  targetValue: targetProp.type,
594
- severity: 'warning',
800
+ severity: "warning",
595
801
  });
596
802
  }
597
803
  }
@@ -601,7 +807,7 @@ export class SemanticDiffEngine {
601
807
  field: `props.${prop.name}`,
602
808
  sourceValue: undefined,
603
809
  targetValue: prop,
604
- severity: 'info',
810
+ severity: "info",
605
811
  });
606
812
  }
607
813
  }
@@ -611,18 +817,18 @@ export class SemanticDiffEngine {
611
817
  const drifts = [];
612
818
  // Drifts from matches with significant differences
613
819
  for (const match of matches) {
614
- const significantDiffs = match.differences.filter(d => d.severity === 'warning' || d.severity === 'critical');
820
+ const significantDiffs = match.differences.filter((d) => d.severity === "warning" || d.severity === "critical");
615
821
  if (significantDiffs.length > 0) {
616
822
  drifts.push({
617
- id: createDriftId('semantic-mismatch', match.source.id, match.target.id),
618
- type: 'semantic-mismatch',
823
+ id: createDriftId("semantic-mismatch", match.source.id, match.target.id),
824
+ type: "semantic-mismatch",
619
825
  severity: this.getHighestSeverity(match.differences),
620
826
  source: this.componentToDriftSource(match.source),
621
827
  target: this.componentToDriftSource(match.target),
622
828
  message: `Component "${match.source.name}" has ${significantDiffs.length} differences between sources`,
623
829
  details: {
624
830
  diff: JSON.stringify(match.differences, null, 2),
625
- suggestions: ['Review component definitions for consistency'],
831
+ suggestions: ["Review component definitions for consistency"],
626
832
  },
627
833
  detectedAt: new Date(),
628
834
  });
@@ -631,13 +837,15 @@ export class SemanticDiffEngine {
631
837
  // Orphaned source components
632
838
  for (const comp of orphanedSource) {
633
839
  drifts.push({
634
- id: createDriftId('orphaned-component', comp.id),
635
- type: 'orphaned-component',
636
- severity: 'warning',
840
+ id: createDriftId("orphaned-component", comp.id),
841
+ type: "orphaned-component",
842
+ severity: "warning",
637
843
  source: this.componentToDriftSource(comp),
638
844
  message: `Component "${comp.name}" exists in ${comp.source.type} but has no match in design`,
639
845
  details: {
640
- suggestions: ['Add component to Figma or document as intentional deviation'],
846
+ suggestions: [
847
+ "Add component to Figma or document as intentional deviation",
848
+ ],
641
849
  },
642
850
  detectedAt: new Date(),
643
851
  });
@@ -645,13 +853,13 @@ export class SemanticDiffEngine {
645
853
  // Orphaned target components
646
854
  for (const comp of orphanedTarget) {
647
855
  drifts.push({
648
- id: createDriftId('orphaned-component', comp.id),
649
- type: 'orphaned-component',
650
- severity: 'info',
856
+ id: createDriftId("orphaned-component", comp.id),
857
+ type: "orphaned-component",
858
+ severity: "info",
651
859
  source: this.componentToDriftSource(comp),
652
860
  message: `Component "${comp.name}" exists in design but not implemented`,
653
861
  details: {
654
- suggestions: ['Implement component or mark as planned'],
862
+ suggestions: ["Implement component or mark as planned"],
655
863
  },
656
864
  detectedAt: new Date(),
657
865
  });
@@ -659,58 +867,91 @@ export class SemanticDiffEngine {
659
867
  return drifts;
660
868
  }
661
869
  componentToDriftSource(comp) {
662
- let location = '';
663
- if (comp.source.type === 'react') {
870
+ let location = "";
871
+ if (comp.source.type === "react") {
664
872
  location = `${comp.source.path}:${comp.source.line || 0}`;
665
873
  }
666
- else if (comp.source.type === 'figma') {
874
+ else if (comp.source.type === "figma") {
667
875
  location = comp.source.url || comp.source.nodeId;
668
876
  }
669
- else if (comp.source.type === 'storybook') {
877
+ else if (comp.source.type === "storybook") {
670
878
  location = comp.source.url || comp.source.storyId;
671
879
  }
672
880
  return {
673
- entityType: 'component',
881
+ entityType: "component",
674
882
  entityId: comp.id,
675
883
  entityName: comp.name,
676
884
  location,
677
885
  };
678
886
  }
679
887
  tokenToDriftSource(token) {
680
- let location = '';
681
- if (token.source.type === 'json' || token.source.type === 'css' || token.source.type === 'scss') {
888
+ let location = "";
889
+ if (token.source.type === "json" ||
890
+ token.source.type === "css" ||
891
+ token.source.type === "scss") {
682
892
  location = token.source.path;
683
893
  }
684
- else if (token.source.type === 'figma') {
894
+ else if (token.source.type === "figma") {
685
895
  location = token.source.fileKey;
686
896
  }
687
897
  return {
688
- entityType: 'token',
898
+ entityType: "token",
689
899
  entityId: token.id,
690
900
  entityName: token.name,
691
901
  location,
692
902
  };
693
903
  }
694
904
  getHighestSeverity(differences) {
695
- if (differences.some(d => d.severity === 'critical'))
696
- return 'critical';
697
- if (differences.some(d => d.severity === 'warning'))
698
- return 'warning';
699
- return 'info';
905
+ if (differences.some((d) => d.severity === "critical"))
906
+ return "critical";
907
+ if (differences.some((d) => d.severity === "warning"))
908
+ return "warning";
909
+ return "info";
700
910
  }
701
911
  checkAccessibility(component) {
702
912
  const issues = [];
703
913
  // Check if interactive components have required ARIA props
704
- const interactiveComponents = ['Button', 'Link', 'Input', 'Select', 'Checkbox', 'Radio'];
705
- const isInteractive = interactiveComponents.some(ic => component.name.toLowerCase().includes(ic.toLowerCase()));
914
+ const interactiveComponents = [
915
+ "Button",
916
+ "Link",
917
+ "Input",
918
+ "Select",
919
+ "Checkbox",
920
+ "Radio",
921
+ ];
922
+ const isInteractive = interactiveComponents.some((ic) => component.name.toLowerCase().includes(ic.toLowerCase()));
706
923
  if (isInteractive) {
707
- const hasAriaLabel = component.props.some(p => p.name.toLowerCase().includes('arialabel') || p.name.toLowerCase().includes('aria-label'));
708
- const hasChildren = component.props.some(p => p.name.toLowerCase() === 'children');
709
- if (!hasAriaLabel && !hasChildren && component.metadata.accessibility?.issues) {
924
+ const hasAriaLabel = component.props.some((p) => p.name.toLowerCase().includes("arialabel") ||
925
+ p.name.toLowerCase().includes("aria-label"));
926
+ const hasChildren = component.props.some((p) => p.name.toLowerCase() === "children");
927
+ if (!hasAriaLabel &&
928
+ !hasChildren &&
929
+ component.metadata.accessibility?.issues) {
710
930
  issues.push(...component.metadata.accessibility.issues);
711
931
  }
712
932
  }
713
933
  return issues;
714
934
  }
935
+ /**
936
+ * Find token suggestions for a hardcoded color value
937
+ * Delegates to TokenSuggestionService
938
+ */
939
+ findColorTokenSuggestions(hardcodedValue, tokens, maxSuggestions = 3) {
940
+ return this.tokenSuggestionService.findColorTokenSuggestions(hardcodedValue, tokens, maxSuggestions);
941
+ }
942
+ /**
943
+ * Find token suggestions for a hardcoded spacing value
944
+ * Delegates to TokenSuggestionService
945
+ */
946
+ findSpacingTokenSuggestions(hardcodedValue, tokens, maxSuggestions = 3) {
947
+ return this.tokenSuggestionService.findSpacingTokenSuggestions(hardcodedValue, tokens, maxSuggestions);
948
+ }
949
+ /**
950
+ * Generate actionable suggestions for hardcoded values
951
+ * Delegates to TokenSuggestionService
952
+ */
953
+ generateTokenSuggestions(hardcodedValues, tokens) {
954
+ return this.tokenSuggestionService.generateTokenSuggestions(hardcodedValues, tokens);
955
+ }
715
956
  }
716
957
  //# sourceMappingURL=semantic-diff.js.map