@buoy-design/core 0.1.0

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.
Files changed (57) hide show
  1. package/dist/analysis/index.d.ts +2 -0
  2. package/dist/analysis/index.d.ts.map +1 -0
  3. package/dist/analysis/index.js +2 -0
  4. package/dist/analysis/index.js.map +1 -0
  5. package/dist/analysis/semantic-diff.d.ts +100 -0
  6. package/dist/analysis/semantic-diff.d.ts.map +1 -0
  7. package/dist/analysis/semantic-diff.js +716 -0
  8. package/dist/analysis/semantic-diff.js.map +1 -0
  9. package/dist/analysis/semantic-diff.test.d.ts +2 -0
  10. package/dist/analysis/semantic-diff.test.d.ts.map +1 -0
  11. package/dist/analysis/semantic-diff.test.js +188 -0
  12. package/dist/analysis/semantic-diff.test.js.map +1 -0
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +7 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/models/component.d.ts +638 -0
  18. package/dist/models/component.d.ts.map +1 -0
  19. package/dist/models/component.js +116 -0
  20. package/dist/models/component.js.map +1 -0
  21. package/dist/models/component.test.d.ts +2 -0
  22. package/dist/models/component.test.d.ts.map +1 -0
  23. package/dist/models/component.test.js +55 -0
  24. package/dist/models/component.test.js.map +1 -0
  25. package/dist/models/drift.d.ts +692 -0
  26. package/dist/models/drift.d.ts.map +1 -0
  27. package/dist/models/drift.js +152 -0
  28. package/dist/models/drift.js.map +1 -0
  29. package/dist/models/drift.test.d.ts +2 -0
  30. package/dist/models/drift.test.d.ts.map +1 -0
  31. package/dist/models/drift.test.js +38 -0
  32. package/dist/models/drift.test.js.map +1 -0
  33. package/dist/models/index.d.ts +9 -0
  34. package/dist/models/index.d.ts.map +1 -0
  35. package/dist/models/index.js +9 -0
  36. package/dist/models/index.js.map +1 -0
  37. package/dist/models/intent.d.ts +226 -0
  38. package/dist/models/intent.d.ts.map +1 -0
  39. package/dist/models/intent.js +84 -0
  40. package/dist/models/intent.js.map +1 -0
  41. package/dist/models/token.d.ts +740 -0
  42. package/dist/models/token.d.ts.map +1 -0
  43. package/dist/models/token.js +164 -0
  44. package/dist/models/token.js.map +1 -0
  45. package/dist/models/token.test.d.ts +2 -0
  46. package/dist/models/token.test.d.ts.map +1 -0
  47. package/dist/models/token.test.js +168 -0
  48. package/dist/models/token.test.js.map +1 -0
  49. package/dist/plugins/index.d.ts +2 -0
  50. package/dist/plugins/index.d.ts.map +1 -0
  51. package/dist/plugins/index.js +2 -0
  52. package/dist/plugins/index.js.map +1 -0
  53. package/dist/plugins/types.d.ts +60 -0
  54. package/dist/plugins/types.d.ts.map +1 -0
  55. package/dist/plugins/types.js +2 -0
  56. package/dist/plugins/types.js.map +1 -0
  57. package/package.json +49 -0
@@ -0,0 +1,716 @@
1
+ import { createDriftId, normalizeComponentName, normalizeTokenName, tokensMatch, } from '../models/index.js';
2
+ export class SemanticDiffEngine {
3
+ /**
4
+ * Check for framework sprawl - multiple UI frameworks in one codebase
5
+ */
6
+ checkFrameworkSprawl(frameworks) {
7
+ // Only count UI/component frameworks, not backend frameworks
8
+ const uiFrameworkNames = [
9
+ 'react', 'vue', 'svelte', 'angular', 'solid', 'preact', 'lit', 'stencil',
10
+ 'nextjs', 'nuxt', 'astro', 'remix', 'sveltekit', 'gatsby', 'react-native', 'expo', 'flutter'
11
+ ];
12
+ const uiFrameworks = frameworks.filter(f => uiFrameworkNames.includes(f.name));
13
+ if (uiFrameworks.length <= 1) {
14
+ return null; // No sprawl
15
+ }
16
+ const frameworkNames = uiFrameworks.map(f => f.name);
17
+ const primaryFramework = uiFrameworks[0];
18
+ return {
19
+ id: createDriftId('framework-sprawl', 'project', frameworkNames.join('-')),
20
+ type: 'framework-sprawl',
21
+ severity: 'warning',
22
+ source: {
23
+ entityType: 'component',
24
+ entityId: 'project',
25
+ entityName: 'Project Architecture',
26
+ location: 'package.json',
27
+ },
28
+ message: `Framework sprawl detected: ${uiFrameworks.length} UI frameworks in use (${frameworkNames.join(', ')})`,
29
+ details: {
30
+ expected: `Single framework (${primaryFramework.name})`,
31
+ actual: `${uiFrameworks.length} frameworks`,
32
+ frameworks: uiFrameworks.map(f => ({ name: f.name, version: f.version })),
33
+ 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',
37
+ ],
38
+ },
39
+ detectedAt: new Date(),
40
+ };
41
+ }
42
+ /**
43
+ * Compare components from different sources (e.g., React vs Figma)
44
+ */
45
+ compareComponents(sourceComponents, targetComponents, options = {}) {
46
+ const matches = [];
47
+ const matchedSourceIds = new Set();
48
+ const matchedTargetIds = new Set();
49
+ // Phase 1: Exact name matches
50
+ 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'));
55
+ matchedSourceIds.add(source.id);
56
+ matchedTargetIds.add(exactMatch.id);
57
+ }
58
+ }
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;
65
+ if (bestMatch && bestMatch.confidence >= minConfidence) {
66
+ matches.push(bestMatch);
67
+ matchedSourceIds.add(source.id);
68
+ matchedTargetIds.add(bestMatch.target.id);
69
+ }
70
+ }
71
+ // 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)));
73
+ return {
74
+ matches,
75
+ orphanedSource: sourceComponents.filter(c => !matchedSourceIds.has(c.id)),
76
+ orphanedTarget: targetComponents.filter(c => !matchedTargetIds.has(c.id)),
77
+ drifts,
78
+ };
79
+ }
80
+ /**
81
+ * Compare tokens between sources
82
+ */
83
+ compareTokens(sourceTokens, targetTokens) {
84
+ const matches = [];
85
+ const drifts = [];
86
+ const matchedSourceIds = new Set();
87
+ const matchedTargetIds = new Set();
88
+ for (const source of sourceTokens) {
89
+ const sourceName = normalizeTokenName(source.name);
90
+ const target = targetTokens.find(t => normalizeTokenName(t.name) === sourceName);
91
+ if (!target)
92
+ continue;
93
+ matchedSourceIds.add(source.id);
94
+ matchedTargetIds.add(target.id);
95
+ matches.push({ source, target });
96
+ // Check for value divergence
97
+ if (!tokensMatch(source.value, target.value)) {
98
+ drifts.push({
99
+ id: createDriftId('value-divergence', source.id, target.id),
100
+ type: 'value-divergence',
101
+ severity: 'warning',
102
+ source: this.tokenToDriftSource(source),
103
+ target: this.tokenToDriftSource(target),
104
+ message: `Token "${source.name}" has different values between sources`,
105
+ details: {
106
+ expected: source.value,
107
+ actual: target.value,
108
+ suggestions: ['Align token values between design and code'],
109
+ },
110
+ detectedAt: new Date(),
111
+ });
112
+ }
113
+ }
114
+ // Orphaned tokens
115
+ const orphanedSource = sourceTokens.filter(t => !matchedSourceIds.has(t.id));
116
+ const orphanedTarget = targetTokens.filter(t => !matchedTargetIds.has(t.id));
117
+ for (const token of orphanedSource) {
118
+ drifts.push({
119
+ id: createDriftId('orphaned-token', token.id),
120
+ type: 'orphaned-token',
121
+ severity: 'info',
122
+ source: this.tokenToDriftSource(token),
123
+ message: `Token "${token.name}" exists in ${token.source.type} but not in design`,
124
+ details: {
125
+ suggestions: ['Add token to design system or remove if unused'],
126
+ },
127
+ detectedAt: new Date(),
128
+ });
129
+ }
130
+ for (const token of orphanedTarget) {
131
+ drifts.push({
132
+ id: createDriftId('orphaned-token', token.id),
133
+ type: 'orphaned-token',
134
+ severity: 'info',
135
+ source: this.tokenToDriftSource(token),
136
+ message: `Token "${token.name}" exists in design but not implemented`,
137
+ details: {
138
+ suggestions: ['Implement token in code or mark as planned'],
139
+ },
140
+ detectedAt: new Date(),
141
+ });
142
+ }
143
+ return { matches, orphanedSource, orphanedTarget, drifts };
144
+ }
145
+ /**
146
+ * Analyze a single set of components for internal drift
147
+ */
148
+ analyzeComponents(components, options = {}) {
149
+ const drifts = [];
150
+ // First pass: collect patterns across all components
151
+ const namingPatterns = this.detectNamingPatterns(components);
152
+ const propTypeMap = this.buildPropTypeMap(components);
153
+ const propNamingMap = this.buildPropNamingMap(components);
154
+ for (const component of components) {
155
+ // Check for deprecation
156
+ if (options.checkDeprecated && component.metadata.deprecated) {
157
+ drifts.push({
158
+ id: createDriftId('deprecated-pattern', component.id),
159
+ type: 'deprecated-pattern',
160
+ severity: 'warning',
161
+ source: this.componentToDriftSource(component),
162
+ message: `Component "${component.name}" is marked as deprecated`,
163
+ details: {
164
+ suggestions: [
165
+ component.metadata.deprecationReason || 'Migrate to recommended alternative',
166
+ ],
167
+ },
168
+ detectedAt: new Date(),
169
+ });
170
+ }
171
+ // Check naming consistency (against project's own patterns, not arbitrary rules)
172
+ if (options.checkNaming) {
173
+ const namingIssue = this.checkNamingConsistency(component.name, namingPatterns);
174
+ if (namingIssue) {
175
+ drifts.push({
176
+ id: createDriftId('naming-inconsistency', component.id),
177
+ type: 'naming-inconsistency',
178
+ severity: 'info',
179
+ source: this.componentToDriftSource(component),
180
+ message: namingIssue.message,
181
+ details: {
182
+ suggestions: [namingIssue.suggestion],
183
+ },
184
+ detectedAt: new Date(),
185
+ });
186
+ }
187
+ }
188
+ // Check for prop type inconsistencies across components
189
+ for (const prop of component.props) {
190
+ const typeConflict = this.checkPropTypeConsistency(prop, propTypeMap);
191
+ if (typeConflict) {
192
+ drifts.push({
193
+ id: createDriftId('semantic-mismatch', component.id, prop.name),
194
+ type: 'semantic-mismatch',
195
+ severity: 'warning',
196
+ source: this.componentToDriftSource(component),
197
+ message: `Prop "${prop.name}" in "${component.name}" uses type "${prop.type}" but other components use "${typeConflict.dominantType}"`,
198
+ details: {
199
+ expected: typeConflict.dominantType,
200
+ actual: prop.type,
201
+ usedIn: typeConflict.examples,
202
+ suggestions: ['Standardize prop types across components for consistency'],
203
+ },
204
+ detectedAt: new Date(),
205
+ });
206
+ }
207
+ }
208
+ // Check for inconsistent prop naming patterns (onClick vs handleClick)
209
+ const propNamingIssues = this.checkPropNamingConsistency(component, propNamingMap);
210
+ for (const issue of propNamingIssues) {
211
+ drifts.push({
212
+ id: createDriftId('naming-inconsistency', component.id, issue.propName),
213
+ type: 'naming-inconsistency',
214
+ severity: 'info',
215
+ source: this.componentToDriftSource(component),
216
+ message: issue.message,
217
+ details: {
218
+ suggestions: [issue.suggestion],
219
+ },
220
+ detectedAt: new Date(),
221
+ });
222
+ }
223
+ // Check for accessibility issues
224
+ if (options.checkAccessibility) {
225
+ const a11yIssues = this.checkAccessibility(component);
226
+ for (const issue of a11yIssues) {
227
+ drifts.push({
228
+ id: createDriftId('accessibility-conflict', component.id),
229
+ type: 'accessibility-conflict',
230
+ severity: 'critical',
231
+ source: this.componentToDriftSource(component),
232
+ message: `Component "${component.name}" has accessibility issues: ${issue}`,
233
+ details: {
234
+ suggestions: ['Fix accessibility issue to ensure inclusive design'],
235
+ },
236
+ detectedAt: new Date(),
237
+ });
238
+ }
239
+ }
240
+ // Check for hardcoded values that should be tokens
241
+ if (component.metadata.hardcodedValues && component.metadata.hardcodedValues.length > 0) {
242
+ 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;
245
+ // Group by type for cleaner messaging
246
+ if (colorCount > 0) {
247
+ const colorValues = hardcoded.filter(h => h.type === 'color');
248
+ drifts.push({
249
+ id: createDriftId('hardcoded-value', component.id, 'color'),
250
+ type: 'hardcoded-value',
251
+ severity: 'warning',
252
+ source: this.componentToDriftSource(component),
253
+ message: `Component "${component.name}" has ${colorCount} hardcoded color${colorCount > 1 ? 's' : ''}: ${colorValues.map(h => h.value).join(', ')}`,
254
+ 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})`),
257
+ },
258
+ detectedAt: new Date(),
259
+ });
260
+ }
261
+ if (spacingCount > 0) {
262
+ const spacingValues = hardcoded.filter(h => h.type === 'spacing' || h.type === 'fontSize');
263
+ drifts.push({
264
+ id: createDriftId('hardcoded-value', component.id, 'spacing'),
265
+ type: 'hardcoded-value',
266
+ severity: 'info',
267
+ source: this.componentToDriftSource(component),
268
+ message: `Component "${component.name}" has ${spacingCount} hardcoded size value${spacingCount > 1 ? 's' : ''}: ${spacingValues.map(h => h.value).join(', ')}`,
269
+ details: {
270
+ suggestions: ['Consider using spacing tokens for consistency'],
271
+ affectedFiles: spacingValues.map(h => `${h.property}: ${h.value} (${h.location})`),
272
+ },
273
+ detectedAt: new Date(),
274
+ });
275
+ }
276
+ }
277
+ }
278
+ // Cross-component checks
279
+ // Check for potential duplicate components
280
+ const duplicates = this.detectPotentialDuplicates(components);
281
+ for (const dup of duplicates) {
282
+ drifts.push({
283
+ id: createDriftId('naming-inconsistency', dup.components[0].id, 'duplicate'),
284
+ type: 'naming-inconsistency',
285
+ severity: 'warning',
286
+ source: this.componentToDriftSource(dup.components[0]),
287
+ message: `Potential duplicate components: ${dup.components.map(c => c.name).join(', ')}`,
288
+ details: {
289
+ suggestions: ['Consider consolidating these components or clarifying their distinct purposes'],
290
+ relatedComponents: dup.components.map(c => c.name),
291
+ },
292
+ detectedAt: new Date(),
293
+ });
294
+ }
295
+ return { drifts };
296
+ }
297
+ /**
298
+ * Detect the dominant naming patterns in the codebase
299
+ */
300
+ detectNamingPatterns(components) {
301
+ const patterns = {
302
+ PascalCase: 0,
303
+ camelCase: 0,
304
+ 'kebab-case': 0,
305
+ snake_case: 0,
306
+ other: 0,
307
+ };
308
+ for (const comp of components) {
309
+ const pattern = this.identifyNamingPattern(comp.name);
310
+ patterns[pattern]++;
311
+ }
312
+ // Find dominant pattern (must be > 60% to be considered dominant)
313
+ const total = components.length;
314
+ let dominant = null;
315
+ let dominantCount = 0;
316
+ for (const [pattern, count] of Object.entries(patterns)) {
317
+ if (count > dominantCount && count / total > 0.6) {
318
+ dominant = pattern;
319
+ dominantCount = count;
320
+ }
321
+ }
322
+ return { patterns, dominant, total };
323
+ }
324
+ identifyNamingPattern(name) {
325
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(name))
326
+ return 'PascalCase';
327
+ if (/^[a-z][a-zA-Z0-9]*$/.test(name))
328
+ return 'camelCase';
329
+ if (/^[a-z][a-z0-9-]*$/.test(name))
330
+ return 'kebab-case';
331
+ if (/^[a-z][a-z0-9_]*$/.test(name))
332
+ return 'snake_case';
333
+ return 'other';
334
+ }
335
+ checkNamingConsistency(name, patterns) {
336
+ if (!patterns.dominant)
337
+ return null; // No clear pattern, don't flag
338
+ const thisPattern = this.identifyNamingPattern(name);
339
+ if (thisPattern === patterns.dominant)
340
+ return null;
341
+ // 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
343
+ if (patterns.patterns[patterns.dominant] < outlierThreshold)
344
+ return null;
345
+ return {
346
+ message: `Component "${name}" uses ${thisPattern} but ${Math.round((patterns.patterns[patterns.dominant] / patterns.total) * 100)}% of components use ${patterns.dominant}`,
347
+ suggestion: `Consider renaming to match project convention (${patterns.dominant})`,
348
+ };
349
+ }
350
+ /**
351
+ * Build a map of prop names to their types across all components
352
+ */
353
+ buildPropTypeMap(components) {
354
+ const map = new Map();
355
+ for (const comp of components) {
356
+ for (const prop of comp.props) {
357
+ const normalizedName = prop.name.toLowerCase();
358
+ if (!map.has(normalizedName)) {
359
+ map.set(normalizedName, { types: new Map(), total: 0 });
360
+ }
361
+ const usage = map.get(normalizedName);
362
+ const typeCount = usage.types.get(prop.type) || { count: 0, examples: [] };
363
+ typeCount.count++;
364
+ if (typeCount.examples.length < 3) {
365
+ typeCount.examples.push(comp.name);
366
+ }
367
+ usage.types.set(prop.type, typeCount);
368
+ usage.total++;
369
+ }
370
+ }
371
+ return map;
372
+ }
373
+ checkPropTypeConsistency(prop, propTypeMap) {
374
+ const usage = propTypeMap.get(prop.name.toLowerCase());
375
+ if (!usage || usage.total < 3)
376
+ return null; // Not enough data
377
+ // Find dominant type
378
+ let dominantType = '';
379
+ let dominantCount = 0;
380
+ for (const [type, data] of usage.types) {
381
+ if (data.count > dominantCount) {
382
+ dominantType = type;
383
+ dominantCount = data.count;
384
+ }
385
+ }
386
+ // Only flag if this prop's type differs and dominant is > 70%
387
+ if (prop.type === dominantType)
388
+ return null;
389
+ if (dominantCount / usage.total < 0.7)
390
+ return null;
391
+ const examples = usage.types.get(dominantType)?.examples || [];
392
+ return { dominantType, examples };
393
+ }
394
+ /**
395
+ * Build a map of semantic prop purposes to their naming patterns
396
+ */
397
+ buildPropNamingMap(components) {
398
+ const map = new Map();
399
+ // Group props by semantic purpose
400
+ const clickHandlers = [];
401
+ const changeHandlers = [];
402
+ for (const comp of components) {
403
+ for (const prop of comp.props) {
404
+ const lower = prop.name.toLowerCase();
405
+ if (lower.includes('click') || lower.includes('press')) {
406
+ clickHandlers.push(prop.name);
407
+ }
408
+ if (lower.includes('change')) {
409
+ changeHandlers.push(prop.name);
410
+ }
411
+ }
412
+ }
413
+ map.set('click', clickHandlers);
414
+ map.set('change', changeHandlers);
415
+ return map;
416
+ }
417
+ checkPropNamingConsistency(component, propNamingMap) {
418
+ const issues = [];
419
+ for (const prop of component.props) {
420
+ const lower = prop.name.toLowerCase();
421
+ // Check click handler naming
422
+ if (lower.includes('click') || lower.includes('press')) {
423
+ const allClickHandlers = propNamingMap.get('click') || [];
424
+ if (allClickHandlers.length >= 5) {
425
+ const dominant = this.findDominantPropPattern(allClickHandlers);
426
+ if (dominant && !prop.name.startsWith(dominant.prefix)) {
427
+ const dominantPct = Math.round((dominant.count / allClickHandlers.length) * 100);
428
+ if (dominantPct >= 70) {
429
+ issues.push({
430
+ propName: prop.name,
431
+ 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`,
433
+ });
434
+ }
435
+ }
436
+ }
437
+ }
438
+ }
439
+ return issues;
440
+ }
441
+ findDominantPropPattern(propNames) {
442
+ const prefixes = {};
443
+ 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;
448
+ }
449
+ let dominant = null;
450
+ for (const [prefix, count] of Object.entries(prefixes)) {
451
+ if (!dominant || count > dominant.count) {
452
+ dominant = { prefix, count };
453
+ }
454
+ }
455
+ return dominant;
456
+ }
457
+ /**
458
+ * Detect potential duplicate components based on similar names
459
+ */
460
+ detectPotentialDuplicates(components) {
461
+ const duplicates = [];
462
+ const processed = new Set();
463
+ for (const comp of components) {
464
+ if (processed.has(comp.id))
465
+ 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 => {
472
+ if (c.id === comp.id)
473
+ 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;
479
+ });
480
+ if (similar.length > 0) {
481
+ const group = [comp, ...similar];
482
+ group.forEach(c => processed.add(c.id));
483
+ duplicates.push({ components: group });
484
+ }
485
+ }
486
+ return duplicates;
487
+ }
488
+ createMatch(source, target, matchType) {
489
+ return {
490
+ source,
491
+ target,
492
+ confidence: matchType === 'exact' ? 1 : 0,
493
+ matchType,
494
+ differences: this.findDifferences(source, target),
495
+ };
496
+ }
497
+ findBestMatch(source, candidates) {
498
+ let bestMatch = null;
499
+ let bestScore = 0;
500
+ for (const candidate of candidates) {
501
+ const score = this.calculateSimilarity(source, candidate);
502
+ if (score > bestScore) {
503
+ bestScore = score;
504
+ const matchType = score > 0.9 ? 'similar' : 'partial';
505
+ bestMatch = {
506
+ source,
507
+ target: candidate,
508
+ confidence: score,
509
+ matchType,
510
+ differences: this.findDifferences(source, candidate),
511
+ };
512
+ }
513
+ }
514
+ return bestMatch;
515
+ }
516
+ calculateSimilarity(a, b) {
517
+ let score = 0;
518
+ const weights = { name: 0.4, props: 0.3, variants: 0.2, dependencies: 0.1 };
519
+ // Name similarity (Levenshtein-based)
520
+ 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);
539
+ return score;
540
+ }
541
+ 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];
573
+ }
574
+ findDifferences(source, target) {
575
+ const differences = [];
576
+ // 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]));
579
+ for (const [name, prop] of sourceProps) {
580
+ const targetProp = targetProps.get(name);
581
+ if (!targetProp) {
582
+ differences.push({
583
+ field: `props.${prop.name}`,
584
+ sourceValue: prop,
585
+ targetValue: undefined,
586
+ severity: prop.required ? 'warning' : 'info',
587
+ });
588
+ }
589
+ else if (prop.type !== targetProp.type) {
590
+ differences.push({
591
+ field: `props.${prop.name}.type`,
592
+ sourceValue: prop.type,
593
+ targetValue: targetProp.type,
594
+ severity: 'warning',
595
+ });
596
+ }
597
+ }
598
+ for (const [name, prop] of targetProps) {
599
+ if (!sourceProps.has(name)) {
600
+ differences.push({
601
+ field: `props.${prop.name}`,
602
+ sourceValue: undefined,
603
+ targetValue: prop,
604
+ severity: 'info',
605
+ });
606
+ }
607
+ }
608
+ return differences;
609
+ }
610
+ generateComponentDrifts(matches, orphanedSource, orphanedTarget) {
611
+ const drifts = [];
612
+ // Drifts from matches with significant differences
613
+ for (const match of matches) {
614
+ const significantDiffs = match.differences.filter(d => d.severity === 'warning' || d.severity === 'critical');
615
+ if (significantDiffs.length > 0) {
616
+ drifts.push({
617
+ id: createDriftId('semantic-mismatch', match.source.id, match.target.id),
618
+ type: 'semantic-mismatch',
619
+ severity: this.getHighestSeverity(match.differences),
620
+ source: this.componentToDriftSource(match.source),
621
+ target: this.componentToDriftSource(match.target),
622
+ message: `Component "${match.source.name}" has ${significantDiffs.length} differences between sources`,
623
+ details: {
624
+ diff: JSON.stringify(match.differences, null, 2),
625
+ suggestions: ['Review component definitions for consistency'],
626
+ },
627
+ detectedAt: new Date(),
628
+ });
629
+ }
630
+ }
631
+ // Orphaned source components
632
+ for (const comp of orphanedSource) {
633
+ drifts.push({
634
+ id: createDriftId('orphaned-component', comp.id),
635
+ type: 'orphaned-component',
636
+ severity: 'warning',
637
+ source: this.componentToDriftSource(comp),
638
+ message: `Component "${comp.name}" exists in ${comp.source.type} but has no match in design`,
639
+ details: {
640
+ suggestions: ['Add component to Figma or document as intentional deviation'],
641
+ },
642
+ detectedAt: new Date(),
643
+ });
644
+ }
645
+ // Orphaned target components
646
+ for (const comp of orphanedTarget) {
647
+ drifts.push({
648
+ id: createDriftId('orphaned-component', comp.id),
649
+ type: 'orphaned-component',
650
+ severity: 'info',
651
+ source: this.componentToDriftSource(comp),
652
+ message: `Component "${comp.name}" exists in design but not implemented`,
653
+ details: {
654
+ suggestions: ['Implement component or mark as planned'],
655
+ },
656
+ detectedAt: new Date(),
657
+ });
658
+ }
659
+ return drifts;
660
+ }
661
+ componentToDriftSource(comp) {
662
+ let location = '';
663
+ if (comp.source.type === 'react') {
664
+ location = `${comp.source.path}:${comp.source.line || 0}`;
665
+ }
666
+ else if (comp.source.type === 'figma') {
667
+ location = comp.source.url || comp.source.nodeId;
668
+ }
669
+ else if (comp.source.type === 'storybook') {
670
+ location = comp.source.url || comp.source.storyId;
671
+ }
672
+ return {
673
+ entityType: 'component',
674
+ entityId: comp.id,
675
+ entityName: comp.name,
676
+ location,
677
+ };
678
+ }
679
+ tokenToDriftSource(token) {
680
+ let location = '';
681
+ if (token.source.type === 'json' || token.source.type === 'css' || token.source.type === 'scss') {
682
+ location = token.source.path;
683
+ }
684
+ else if (token.source.type === 'figma') {
685
+ location = token.source.fileKey;
686
+ }
687
+ return {
688
+ entityType: 'token',
689
+ entityId: token.id,
690
+ entityName: token.name,
691
+ location,
692
+ };
693
+ }
694
+ 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';
700
+ }
701
+ checkAccessibility(component) {
702
+ const issues = [];
703
+ // 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()));
706
+ 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) {
710
+ issues.push(...component.metadata.accessibility.issues);
711
+ }
712
+ }
713
+ return issues;
714
+ }
715
+ }
716
+ //# sourceMappingURL=semantic-diff.js.map