@buoy-design/cli 0.3.32 → 0.3.34

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 (61) hide show
  1. package/dist/commands/check.d.ts.sync-conflict-20260305-170128-6PCZ3ZU.map +1 -0
  2. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.d.ts +26 -0
  3. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.d.ts.map +1 -0
  4. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.js +438 -0
  5. package/dist/commands/check.sync-conflict-20260305-155016-6PCZ3ZU.js.map +1 -0
  6. package/dist/commands/dock.sync-conflict-20260309-191923-6PCZ3ZU.js +1006 -0
  7. package/dist/commands/show.d.ts.map +1 -1
  8. package/dist/commands/show.d.ts.sync-conflict-20260306-165917-6PCZ3ZU.map +1 -0
  9. package/dist/commands/show.js +6 -0
  10. package/dist/commands/show.js.map +1 -1
  11. package/dist/commands/show.sync-conflict-20260305-140755-6PCZ3ZU.js +1735 -0
  12. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.d.ts +11 -0
  13. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.d.ts.map +1 -0
  14. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.js +1735 -0
  15. package/dist/commands/show.sync-conflict-20260309-130326-6PCZ3ZU.js.map +1 -0
  16. package/dist/config/loader.js +1 -1
  17. package/dist/config/loader.js.map +1 -1
  18. package/dist/config/loader.js.sync-conflict-20260309-033512-6PCZ3ZU.map +1 -0
  19. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.d.ts +8 -0
  20. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.d.ts.map +1 -0
  21. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.js +162 -0
  22. package/dist/config/loader.sync-conflict-20260307-175208-6PCZ3ZU.js.map +1 -0
  23. package/dist/config/schema.d.ts.sync-conflict-20260309-154654-6PCZ3ZU.map +1 -0
  24. package/dist/config/schema.sync-conflict-20260309-135703-6PCZ3ZU.js +214 -0
  25. package/dist/detect/frameworks.js.sync-conflict-20260306-123756-6PCZ3ZU.map +1 -0
  26. package/dist/detect/monorepo-patterns.js.sync-conflict-20260309-155400-6PCZ3ZU.map +1 -0
  27. package/dist/hooks/index.d.ts.sync-conflict-20260306-220901-6PCZ3ZU.map +1 -0
  28. package/dist/output/formatters.js.sync-conflict-20260306-134702-6PCZ3ZU.map +1 -0
  29. package/dist/output/formatters.sync-conflict-20260306-180804-6PCZ3ZU.js +867 -0
  30. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.d.ts +29 -0
  31. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.d.ts.map +1 -0
  32. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.js +867 -0
  33. package/dist/output/formatters.sync-conflict-20260307-131418-5SN2GZG.js.map +1 -0
  34. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.d.ts +29 -0
  35. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.d.ts.map +1 -0
  36. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.js +867 -0
  37. package/dist/output/formatters.sync-conflict-20260307-143122-5SN2GZG.js.map +1 -0
  38. package/dist/output/index.sync-conflict-20260309-222859-6PCZ3ZU.js +5 -0
  39. package/dist/output/reporters.d.sync-conflict-20260309-193820-6PCZ3ZU.ts +38 -0
  40. package/dist/output/reporters.d.ts.sync-conflict-20260306-193811-6PCZ3ZU.map +1 -0
  41. package/dist/output/reporters.sync-conflict-20260309-030558-6PCZ3ZU.js +182 -0
  42. package/dist/output/reports.d.ts.sync-conflict-20260307-172149-6PCZ3ZU.map +1 -0
  43. package/dist/output/reports.js.sync-conflict-20260305-161643-6PCZ3ZU.map +1 -0
  44. package/dist/output/reports.sync-conflict-20260305-211951-6PCZ3ZU.js +393 -0
  45. package/dist/output/visuals.d.ts +53 -0
  46. package/dist/output/visuals.d.ts.map +1 -0
  47. package/dist/output/visuals.js +194 -0
  48. package/dist/output/visuals.js.map +1 -0
  49. package/dist/services/drift-analysis.d.sync-conflict-20260306-151016-6PCZ3ZU.ts +194 -0
  50. package/dist/services/drift-analysis.d.ts.sync-conflict-20260307-175904-6PCZ3ZU.map +1 -0
  51. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.d.ts +194 -0
  52. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.d.ts.map +1 -0
  53. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.js +1022 -0
  54. package/dist/services/drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.js.map +1 -0
  55. package/dist/services/skill-export.d.ts.sync-conflict-20260309-171021-6PCZ3ZU.map +1 -0
  56. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.d.ts +109 -0
  57. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.d.ts.map +1 -0
  58. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.js +737 -0
  59. package/dist/services/skill-export.sync-conflict-20260309-144621-6PCZ3ZU.js.map +1 -0
  60. package/package.json +14 -14
  61. package/LICENSE +0 -21
@@ -0,0 +1,1022 @@
1
+ // apps/cli/src/services/drift-analysis.ts
2
+ /**
3
+ * DriftAnalysisService - Consolidated drift detection workflow
4
+ *
5
+ * Handles the common pattern of:
6
+ * 1. Scanning components via ScanOrchestrator
7
+ * 2. Running SemanticDiffEngine analysis
8
+ * 3. Applying ignore rules from config
9
+ * 4. Filtering against ignore list
10
+ */
11
+ import { ScanOrchestrator } from "../scan/orchestrator.js";
12
+ import { getSeverityWeight } from "@buoy-design/core";
13
+ import { TailwindScanner, extractStaticClassStrings } from "@buoy-design/scanners";
14
+ import { detectRepeatedPatterns, checkVariantConsistency, checkExampleCompliance, detectTokenUtilities, checkTokenUtilityUsage, } from "@buoy-design/core";
15
+ import { glob } from "glob";
16
+ import { minimatch } from "minimatch";
17
+ import { readFile } from "fs/promises";
18
+ import { resolve } from "path";
19
+ /**
20
+ * Severity order for filtering and sorting (0 = lowest, 2 = highest)
21
+ * Use getSeverityWeight from @buoy-design/core for consistent ordering
22
+ */
23
+ const SEVERITY_ORDER = {
24
+ info: 0,
25
+ warning: 1,
26
+ critical: 2,
27
+ };
28
+ /**
29
+ * Calculate summary counts for drift signals
30
+ */
31
+ export function calculateDriftSummary(drifts) {
32
+ return {
33
+ total: drifts.length,
34
+ critical: drifts.filter((d) => d.severity === "critical").length,
35
+ warning: drifts.filter((d) => d.severity === "warning").length,
36
+ info: drifts.filter((d) => d.severity === "info").length,
37
+ };
38
+ }
39
+ /**
40
+ * Determine if drifts exceed a severity threshold
41
+ */
42
+ export function hasDriftsAboveThreshold(drifts, failOn) {
43
+ if (failOn === "none")
44
+ return false;
45
+ const threshold = SEVERITY_ORDER[failOn] ?? SEVERITY_ORDER.critical;
46
+ return drifts.some((d) => SEVERITY_ORDER[d.severity] >= threshold);
47
+ }
48
+ /**
49
+ * Sort drifts by severity (critical first)
50
+ */
51
+ export function sortDriftsBySeverity(drifts) {
52
+ return [...drifts].sort((a, b) => getSeverityWeight(b.severity) - getSeverityWeight(a.severity));
53
+ }
54
+ /**
55
+ * Apply ignore rules from config to filter out matching drifts.
56
+ * Each rule can filter by type, file (glob), component (regex),
57
+ * token (regex), and/or value (regex). Multiple fields = AND.
58
+ * Multiple rules = OR (any rule can ignore a drift).
59
+ */
60
+ export function applyIgnoreRules(drifts, ignoreRules, onWarning) {
61
+ if (ignoreRules.length === 0)
62
+ return drifts;
63
+ return drifts.filter((d) => {
64
+ // Keep drift if NO rule matches it (rules are OR'd)
65
+ for (const rule of ignoreRules) {
66
+ if (ruleMatches(d, rule, onWarning))
67
+ return false;
68
+ }
69
+ return true;
70
+ });
71
+ }
72
+ /**
73
+ * Apply promote rules — elevate severity of matching drifts.
74
+ * Each rule specifies filter dimensions + `to` (target severity).
75
+ * Multiple rules = OR (first match wins).
76
+ */
77
+ export function applyPromoteRules(drifts, rules, onWarning) {
78
+ if (rules.length === 0)
79
+ return drifts;
80
+ return drifts.map((d) => {
81
+ for (const rule of rules) {
82
+ if (ruleMatches(d, rule, onWarning)) {
83
+ return { ...d, severity: rule.to };
84
+ }
85
+ }
86
+ return d;
87
+ });
88
+ }
89
+ /**
90
+ * Apply enforce rules — always set matching drifts to critical.
91
+ * Delegates to applyPromoteRules with `to: 'critical'`.
92
+ */
93
+ export function applyEnforceRules(drifts, rules, onWarning) {
94
+ if (rules.length === 0)
95
+ return drifts;
96
+ const promoteRules = rules.map((rule) => ({ ...rule, to: "critical" }));
97
+ return applyPromoteRules(drifts, promoteRules, onWarning);
98
+ }
99
+ function ruleMatches(d, rule, onWarning) {
100
+ const { type, severity, file, component, token, value } = rule;
101
+ // Rule with no filter dimensions does nothing
102
+ if (!type && !severity && !file && !component && !token && !value)
103
+ return false;
104
+ // All specified dimensions must match (AND logic)
105
+ if (type && d.type !== type)
106
+ return false;
107
+ if (severity && d.severity !== severity)
108
+ return false;
109
+ if (file) {
110
+ // Strip line numbers from location (e.g., "Button.tsx:42" → "Button.tsx")
111
+ const loc = d.source.location.replace(/:\d+$/, "");
112
+ if (!minimatch(loc, file))
113
+ return false;
114
+ }
115
+ if (component) {
116
+ if (d.source.entityType !== "component")
117
+ return false;
118
+ try {
119
+ if (!new RegExp(component).test(d.source.entityName))
120
+ return false;
121
+ }
122
+ catch {
123
+ onWarning?.(`Invalid regex "${component}" in ignore rule component field, skipping`);
124
+ return false;
125
+ }
126
+ }
127
+ if (token) {
128
+ if (d.source.entityType !== "token")
129
+ return false;
130
+ try {
131
+ if (!new RegExp(token).test(d.source.entityName))
132
+ return false;
133
+ }
134
+ catch {
135
+ onWarning?.(`Invalid regex "${token}" in ignore rule token field, skipping`);
136
+ return false;
137
+ }
138
+ }
139
+ if (value) {
140
+ const actual = d.details.actual != null ? String(d.details.actual) : "";
141
+ try {
142
+ if (!new RegExp(value).test(actual))
143
+ return false;
144
+ }
145
+ catch {
146
+ onWarning?.(`Invalid regex "${value}" in ignore rule value field, skipping`);
147
+ return false;
148
+ }
149
+ }
150
+ return true;
151
+ }
152
+ /**
153
+ * Apply severity filter to drifts
154
+ */
155
+ export function filterBySeverity(drifts, minSeverity) {
156
+ const minLevel = SEVERITY_ORDER[minSeverity] ?? 0;
157
+ return drifts.filter((d) => SEVERITY_ORDER[d.severity] >= minLevel);
158
+ }
159
+ /**
160
+ * Apply type filter to drifts
161
+ */
162
+ export function filterByType(drifts, type) {
163
+ return drifts.filter((d) => d.type === type);
164
+ }
165
+ /**
166
+ * Apply per-type severity overrides from config
167
+ */
168
+ export function applySeverityOverrides(drifts, overrides) {
169
+ if (!overrides || Object.keys(overrides).length === 0)
170
+ return drifts;
171
+ return drifts.map((d) => {
172
+ const override = overrides[d.type];
173
+ return override ? { ...d, severity: override } : d;
174
+ });
175
+ }
176
+ // Entry point file patterns - these components are rendered by the framework
177
+ // router, not imported by other components, so they should never be flagged as unused
178
+ const ENTRY_POINT_PATTERNS = [
179
+ /\/pages?\//, // Next.js pages/ or page.tsx
180
+ /\/app\/.*page\./, // Next.js App Router page.tsx
181
+ /\/app\/.*layout\./, // Next.js App Router layout.tsx
182
+ /\/app\/.*loading\./, // Next.js App Router loading.tsx
183
+ /\/app\/.*error\./, // Next.js App Router error.tsx
184
+ /\/app\/.*not-found\./, // Next.js App Router not-found.tsx
185
+ /\/app\/.*template\./, // Next.js App Router template.tsx
186
+ /\/routes?\//, // Remix/SvelteKit routes
187
+ /\/\+page\./, // SvelteKit +page.svelte
188
+ /\/\+layout\./, // SvelteKit +layout.svelte
189
+ /\/\+error\./, // SvelteKit +error.svelte
190
+ /\/\+server\./, // SvelteKit +server.ts
191
+ /\/views?\//, // Vue views directory
192
+ /\/screens?\//, // React Native screens
193
+ /\.astro$/, // Astro page/layout components are auto-routed
194
+ /\/(app|main|index)\.(tsx|jsx)$/, // App root / main entry / index component files
195
+ /\/_app\./, // Next.js custom App
196
+ /\/_document\./, // Next.js custom Document
197
+ /\/root\./, // Remix root
198
+ /\/entry\./, // Entry files
199
+ ];
200
+ function isEntryPointComponent(component) {
201
+ const source = component.source;
202
+ // Only file-based sources (react, vue, svelte) have a path field
203
+ if (source.type === 'figma' || source.type === 'storybook')
204
+ return false;
205
+ const location = source.path || '';
206
+ return ENTRY_POINT_PATTERNS.some(pattern => pattern.test(location));
207
+ }
208
+ /**
209
+ * DriftAnalysisService - Main entry point for drift detection
210
+ */
211
+ export class DriftAnalysisService {
212
+ config;
213
+ constructor(config) {
214
+ this.config = config;
215
+ }
216
+ /**
217
+ * Run full drift analysis pipeline
218
+ */
219
+ async analyze(options = {}) {
220
+ const { onProgress, includeIgnored, minSeverity, filterType, cache, checkVariants, checkTokenUtilities, checkExamples, } = options;
221
+ // Step 1: Scan components
222
+ onProgress?.("Scanning components...");
223
+ const orchestrator = new ScanOrchestrator(this.config, process.cwd(), {
224
+ cache,
225
+ });
226
+ const { components } = await orchestrator.scanComponents({
227
+ onProgress,
228
+ });
229
+ // Step 2: Scan tokens (before analysis, so suggestions can be generated)
230
+ onProgress?.("Scanning tokens...");
231
+ const { tokens: scannedTokens } = await orchestrator.scanTokens({ onProgress });
232
+ // Step 2.1: Run semantic diff analysis
233
+ onProgress?.("Analyzing drift...");
234
+ const { SemanticDiffEngine } = await import("@buoy-design/core/analysis");
235
+ const engine = new SemanticDiffEngine();
236
+ const diffResult = engine.analyzeComponents(components, {
237
+ checkDeprecated: true,
238
+ checkNaming: true,
239
+ checkDocumentation: true,
240
+ checkAccessibility: true,
241
+ availableTokens: scannedTokens,
242
+ });
243
+ let drifts = applySeverityOverrides(diffResult.drifts, this.config.drift.severity);
244
+ // Step 2.2: Check framework sprawl
245
+ onProgress?.("Checking for framework sprawl...");
246
+ const { ProjectDetector } = await import("../detect/project-detector.js");
247
+ const detector = new ProjectDetector(process.cwd());
248
+ const projectInfo = await detector.detect();
249
+ if (projectInfo.frameworks.length > 0) {
250
+ const sprawlDrift = engine.checkFrameworkSprawl(projectInfo.frameworks.map((f) => ({ name: f.name, version: f.version })));
251
+ if (sprawlDrift) {
252
+ drifts.push(...applySeverityOverrides([sprawlDrift], this.config.drift.severity));
253
+ onProgress?.(`Framework sprawl detected: ${projectInfo.frameworks.map((f) => f.name).join(", ")}`);
254
+ }
255
+ }
256
+ // Step 2.3: Scan for unused components and tokens
257
+ onProgress?.("Scanning for unused components and tokens...");
258
+ const { collectUsages } = await import("@buoy-design/core");
259
+ const componentNames = components.map((c) => c.name);
260
+ const tokenNames = scannedTokens.map((t) => t.name);
261
+ const usageResult = await collectUsages({
262
+ projectRoot: process.cwd(),
263
+ knownComponents: componentNames,
264
+ knownTokens: tokenNames,
265
+ });
266
+ // Build usage count maps
267
+ const componentUsageMap = new Map();
268
+ for (const cu of usageResult.componentUsages) {
269
+ componentUsageMap.set(cu.componentName, (componentUsageMap.get(cu.componentName) || 0) + 1);
270
+ }
271
+ const tokenUsageMap = new Map();
272
+ for (const tu of usageResult.tokenUsages) {
273
+ tokenUsageMap.set(tu.tokenName, (tokenUsageMap.get(tu.tokenName) || 0) + 1);
274
+ }
275
+ // Fix 2: Count barrel file re-exports as usage
276
+ // Components re-exported from barrel files (index.ts) are part of the public API
277
+ await this.scanBarrelReExports(componentUsageMap);
278
+ // Fix 3: Count dynamic imports as usage
279
+ // React.lazy(() => import('./Component')) and next/dynamic patterns
280
+ await this.scanDynamicImports(componentUsageMap);
281
+ // Fix 4: Count Vue/Svelte template component references as usage
282
+ // <MyComponent /> in .vue and .svelte template blocks
283
+ await this.scanTemplateComponentUsage(componentUsageMap, componentNames);
284
+ // Fix 5: Count Vue auto-registration patterns as usage
285
+ // app.component('MyComponent', ...) and Vue.component('MyComponent', ...)
286
+ await this.scanAutoRegistration(componentUsageMap);
287
+ // Fix 6: Count Angular NgModule declarations as usage
288
+ // @NgModule({ declarations: [ButtonComponent, ...] })
289
+ await this.scanNgModuleDeclarations(componentUsageMap);
290
+ // Fix 7: Count Storybook story file imports as usage
291
+ // Button.stories.tsx importing { Button } means Button is documented/tested
292
+ await this.scanStoryFileUsages(componentUsageMap);
293
+ // Fix 8: Count Lit/Web Component registrations as usage
294
+ await this.scanWebComponentRegistrations(componentUsageMap);
295
+ // Fix 9: Treat Nuxt components/ dir as auto-imported
296
+ const componentNameSet = new Set(componentNames);
297
+ await this.scanNuxtAutoImports(componentUsageMap, componentNameSet);
298
+ // Fix 10: Count test file imports as usage
299
+ // Components imported in .test.tsx/.spec.tsx are actively maintained
300
+ await this.scanTestFileUsages(componentUsageMap);
301
+ // Fix 11: Count HOC/wrapper patterns as usage
302
+ // forwardRef(Component), memo(Component), styled(Component), withXxx(Component)
303
+ await this.scanHOCWrapperUsages(componentUsageMap);
304
+ // Fix 12: Component library detection — if package.json exports components,
305
+ // all exported components are the product's public API
306
+ await this.scanPackageExports(componentUsageMap);
307
+ // Fix 13: Detect components passed as values (props, object properties, arguments)
308
+ // e.g. transition={DialogTransition} or { toolbarAccount: AccountPopover }
309
+ await this.scanComponentAsValueUsages(componentUsageMap, componentNames);
310
+ // Fix 1: Exempt entry point components (pages, routes, layouts)
311
+ // These are rendered by the framework router, not imported by other components
312
+ const nonEntryPointComponents = components.filter(c => !isEntryPointComponent(c));
313
+ // Check for unused components (excluding entry points)
314
+ const unusedComponentDrifts = engine.checkUnusedComponents(nonEntryPointComponents, componentUsageMap);
315
+ if (unusedComponentDrifts.length > 0) {
316
+ drifts.push(...applySeverityOverrides(unusedComponentDrifts, this.config.drift.severity));
317
+ onProgress?.(`Found ${unusedComponentDrifts.length} unused components`);
318
+ }
319
+ // Check for unused tokens
320
+ const unusedTokenDrifts = engine.checkUnusedTokens(scannedTokens, tokenUsageMap);
321
+ if (unusedTokenDrifts.length > 0) {
322
+ drifts.push(...applySeverityOverrides(unusedTokenDrifts, this.config.drift.severity));
323
+ onProgress?.(`Found ${unusedTokenDrifts.length} unused tokens`);
324
+ }
325
+ // Step 2.4: Cross-source comparison (orphaned-component, orphaned-token, value-divergence)
326
+ const { classifyComponents, classifyTokens } = await import("./source-classifier.js");
327
+ const canonicalPatterns = this.config.sources.tokens?.canonical ?? [];
328
+ const classifiedComponents = classifyComponents(components);
329
+ const classifiedTokens = classifyTokens(scannedTokens, canonicalPatterns);
330
+ if (classifiedComponents.canonical.length > 0 && classifiedComponents.code.length > 0) {
331
+ onProgress?.(`Comparing ${classifiedComponents.code.length} code components against ${classifiedComponents.canonical.length} design components...`);
332
+ const componentDiff = engine.compareComponents(classifiedComponents.code, classifiedComponents.canonical);
333
+ if (componentDiff.drifts.length > 0) {
334
+ drifts.push(...applySeverityOverrides(componentDiff.drifts, this.config.drift.severity));
335
+ onProgress?.(`Found ${componentDiff.drifts.length} cross-source component issues`);
336
+ }
337
+ }
338
+ if (classifiedTokens.canonical.length > 0 && classifiedTokens.code.length > 0) {
339
+ onProgress?.(`Comparing ${classifiedTokens.code.length} code tokens against ${classifiedTokens.canonical.length} design tokens...`);
340
+ const tokenDiff = engine.compareTokens(classifiedTokens.code, classifiedTokens.canonical);
341
+ if (tokenDiff.drifts.length > 0) {
342
+ drifts.push(...applySeverityOverrides(tokenDiff.drifts, this.config.drift.severity));
343
+ onProgress?.(`Found ${tokenDiff.drifts.length} cross-source token issues`);
344
+ }
345
+ }
346
+ // Step 2.5: Run Tailwind arbitrary value detection
347
+ if (this.config.sources.tailwind?.enabled) {
348
+ onProgress?.("Scanning for Tailwind arbitrary values...");
349
+ const tailwindScanner = new TailwindScanner({
350
+ projectRoot: process.cwd(),
351
+ include: this.config.sources.tailwind.files,
352
+ exclude: this.config.sources.tailwind.exclude,
353
+ detectArbitraryValues: true,
354
+ });
355
+ const tailwindResult = await tailwindScanner.scan();
356
+ if (tailwindResult.drifts.length > 0) {
357
+ drifts = [
358
+ ...drifts,
359
+ ...applySeverityOverrides(tailwindResult.drifts, this.config.drift.severity),
360
+ ];
361
+ onProgress?.(`Found ${tailwindResult.drifts.length} Tailwind arbitrary value issues`);
362
+ }
363
+ }
364
+ // Step 2.6: Repeated pattern detection (always-on, opt-out via config)
365
+ const repeatedPatternConfig = (this.config.drift?.types?.["repeated-pattern"] ?? {});
366
+ if (repeatedPatternConfig.enabled !== false) {
367
+ onProgress?.("Detecting repeated patterns...");
368
+ const patternDrifts = await this.detectRepeatedPatterns(repeatedPatternConfig);
369
+ drifts.push(...patternDrifts);
370
+ if (patternDrifts.length > 0) {
371
+ onProgress?.(`Found ${patternDrifts.length} repeated pattern issues`);
372
+ }
373
+ }
374
+ // Step 2.7: Phase 4.1 - Cross-Variant Consistency Checking
375
+ const variantCheckEnabled = checkVariants ?? this.config.drift?.types?.["value-divergence"]?.checkVariants;
376
+ if (variantCheckEnabled) {
377
+ onProgress?.("Checking variant consistency...");
378
+ const variantDrifts = checkVariantConsistency(components);
379
+ if (variantDrifts.length > 0) {
380
+ drifts.push(...applySeverityOverrides(variantDrifts, this.config.drift.severity));
381
+ onProgress?.(`Found ${variantDrifts.length} variant inconsistencies`);
382
+ }
383
+ }
384
+ // Step 2.8: Phase 4.2 - Token Utility Function Detection
385
+ const tokenUtilityCheckEnabled = checkTokenUtilities ?? this.config.drift?.types?.["hardcoded-value"]?.checkUtilities;
386
+ if (tokenUtilityCheckEnabled) {
387
+ onProgress?.("Detecting token utility functions...");
388
+ const utilityAnalysis = detectTokenUtilities(components);
389
+ if (utilityAnalysis.availableUtilities.length > 0) {
390
+ onProgress?.(`Found ${utilityAnalysis.availableUtilities.length} token utilities: ${utilityAnalysis.availableUtilities.map((u) => u.name).join(", ")}`);
391
+ const utilityDrifts = checkTokenUtilityUsage(components, utilityAnalysis);
392
+ if (utilityDrifts.length > 0) {
393
+ drifts.push(...applySeverityOverrides(utilityDrifts, this.config.drift.severity));
394
+ onProgress?.(`Found ${utilityDrifts.length} hardcoded values that could use utilities`);
395
+ }
396
+ }
397
+ }
398
+ // Step 2.9: Phase 4.3 - Example Code vs Production Code Analysis
399
+ const exampleCheckEnabled = checkExamples ?? this.config.drift?.types?.["missing-documentation"]?.checkExamples;
400
+ if (exampleCheckEnabled) {
401
+ onProgress?.("Analyzing example code compliance...");
402
+ const exampleDrifts = checkExampleCompliance(components);
403
+ if (exampleDrifts.length > 0) {
404
+ drifts.push(...applySeverityOverrides(exampleDrifts, this.config.drift.severity));
405
+ onProgress?.(`Found ${exampleDrifts.length} example/documentation issues`);
406
+ }
407
+ }
408
+ // Step 3: Apply severity filter (before other filters for efficiency)
409
+ if (minSeverity) {
410
+ drifts = filterBySeverity(drifts, minSeverity);
411
+ }
412
+ // Step 4: Apply type filter
413
+ if (filterType) {
414
+ drifts = filterByType(drifts, filterType);
415
+ }
416
+ // Step 5: Apply promote rules from config
417
+ if (this.config.drift.promote && this.config.drift.promote.length > 0) {
418
+ drifts = applyPromoteRules(drifts, this.config.drift.promote, (msg) => {
419
+ onProgress?.(`Warning: ${msg}`);
420
+ });
421
+ }
422
+ // Step 6: Apply enforce rules from config
423
+ if (this.config.drift.enforce && this.config.drift.enforce.length > 0) {
424
+ drifts = applyEnforceRules(drifts, this.config.drift.enforce, (msg) => {
425
+ onProgress?.(`Warning: ${msg}`);
426
+ });
427
+ }
428
+ // Step 7: Apply ignore rules from config
429
+ drifts = applyIgnoreRules(drifts, this.config.drift.ignore, (msg) => {
430
+ onProgress?.(`Warning: ${msg}`);
431
+ });
432
+ // Step 8: Apply ignore list filtering
433
+ let ignoredCount = 0;
434
+ if (!includeIgnored) {
435
+ const { loadIgnoreList, filterIgnored } = await import("../commands/ignore.js");
436
+ const ignoreList = await loadIgnoreList();
437
+ const filtered = filterIgnored(drifts, ignoreList);
438
+ drifts = filtered.newDrifts;
439
+ ignoredCount = filtered.ignoredCount;
440
+ if (ignoredCount > 0) {
441
+ onProgress?.(`Filtered out ${ignoredCount} ignored drift signals.`);
442
+ }
443
+ }
444
+ return {
445
+ drifts,
446
+ components,
447
+ ignoredCount,
448
+ summary: calculateDriftSummary(drifts),
449
+ };
450
+ }
451
+ /**
452
+ * Scan barrel files (index.ts) for re-exports and count re-exported components as used.
453
+ * Components re-exported from barrel files are part of the public API.
454
+ */
455
+ async scanBarrelReExports(componentUsageMap) {
456
+ const cwd = process.cwd();
457
+ const barrelFiles = await glob('**/index.{ts,tsx,js,jsx}', {
458
+ cwd,
459
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
460
+ nodir: true,
461
+ maxDepth: 6,
462
+ });
463
+ for (const barrelFile of barrelFiles.slice(0, 200)) {
464
+ try {
465
+ const content = await readFile(resolve(cwd, barrelFile), 'utf-8');
466
+ // Check for named re-exports: export { Button, Card } from './components'
467
+ const namedPattern = /export\s*\{\s*([^}]+)\s*\}\s*from/g;
468
+ let match;
469
+ while ((match = namedPattern.exec(content)) !== null) {
470
+ const names = match[1].split(',').map(n => {
471
+ // Handle "default as Name" and "Name as Alias"
472
+ const parts = n.trim().split(/\s+as\s+/);
473
+ return (parts[1] || parts[0] || '').trim();
474
+ }).filter(n => n && /^[A-Z]/.test(n)); // Only PascalCase (component names)
475
+ for (const name of names) {
476
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
477
+ }
478
+ }
479
+ // Check for default re-exports: export { default as Button } from './Button'
480
+ const defaultReExportPattern = /export\s*\{\s*default\s+as\s+([A-Z][a-zA-Z0-9]*)\s*\}\s*from/g;
481
+ while ((match = defaultReExportPattern.exec(content)) !== null) {
482
+ const name = match[1];
483
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
484
+ }
485
+ // Check for wildcard re-exports: export * from './Button'
486
+ // If the module name looks like a component name, count it
487
+ const wildcardPattern = /export\s*\*\s*from\s*['"]\.\/([^'"]+)['"]/g;
488
+ while ((match = wildcardPattern.exec(content)) !== null) {
489
+ const moduleName = match[1];
490
+ const segments = moduleName.split('/');
491
+ const last = segments[segments.length - 1] || '';
492
+ if (/^[A-Z]/.test(last)) {
493
+ componentUsageMap.set(last, (componentUsageMap.get(last) || 0) + 1);
494
+ }
495
+ }
496
+ }
497
+ catch {
498
+ // ignore unreadable files
499
+ }
500
+ }
501
+ }
502
+ /**
503
+ * Scan source files for dynamic imports and count imported components as used.
504
+ * Handles React.lazy(() => import('./Component')) and next/dynamic patterns.
505
+ */
506
+ async scanDynamicImports(componentUsageMap) {
507
+ const cwd = process.cwd();
508
+ const sourceFiles = await glob('**/*.{tsx,jsx,ts,js}', {
509
+ cwd,
510
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.test.*', '**/*.spec.*'],
511
+ nodir: true,
512
+ maxDepth: 6,
513
+ });
514
+ for (const file of sourceFiles.slice(0, 200)) {
515
+ try {
516
+ const content = await readFile(resolve(cwd, file), 'utf-8');
517
+ if (!content.includes('import('))
518
+ continue; // Quick check before regex
519
+ const dynamicPattern = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
520
+ let match;
521
+ while ((match = dynamicPattern.exec(content)) !== null) {
522
+ const modulePath = match[1];
523
+ const segments = modulePath.split('/');
524
+ const last = (segments[segments.length - 1] || '').replace(/\.(tsx?|jsx?)$/, '');
525
+ if (/^[A-Z]/.test(last)) {
526
+ componentUsageMap.set(last, (componentUsageMap.get(last) || 0) + 1);
527
+ }
528
+ }
529
+ }
530
+ catch {
531
+ // ignore unreadable files
532
+ }
533
+ }
534
+ }
535
+ /**
536
+ * Scan Vue, Svelte, and Angular template blocks for component usage.
537
+ * Vue SFCs reference components in <template> blocks as <MyComponent />,
538
+ * Angular templates use <app-my-component> selectors,
539
+ * which aren't detected by JS import scanning.
540
+ */
541
+ async scanTemplateComponentUsage(componentUsageMap, knownComponents) {
542
+ if (knownComponents.length === 0)
543
+ return;
544
+ const cwd = process.cwd();
545
+ const templateFiles = await glob('**/*.{vue,svelte,html}', {
546
+ cwd,
547
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
548
+ nodir: true,
549
+ maxDepth: 8,
550
+ });
551
+ const knownSet = new Set(knownComponents);
552
+ // Build a kebab-case lookup for Vue/Angular (MyComponent -> my-component)
553
+ const kebabMap = new Map();
554
+ for (const name of knownComponents) {
555
+ const kebab = name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
556
+ kebabMap.set(kebab, name);
557
+ // Angular selectors often use app- prefix: ButtonComponent -> app-button
558
+ const angularSelector = `app-${kebab.replace(/-component$/, '')}`;
559
+ kebabMap.set(angularSelector, name);
560
+ }
561
+ for (const file of templateFiles.slice(0, 300)) {
562
+ try {
563
+ const content = await readFile(resolve(cwd, file), 'utf-8');
564
+ // Match PascalCase component tags: <MyComponent or <MyComponent>
565
+ const pascalPattern = /<([A-Z][a-zA-Z0-9]*)\s*/g;
566
+ let match;
567
+ while ((match = pascalPattern.exec(content)) !== null) {
568
+ const name = match[1];
569
+ if (knownSet.has(name)) {
570
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
571
+ }
572
+ }
573
+ // Match kebab-case component tags in Vue: <my-component or <my-component>
574
+ const kebabPattern = /<([a-z][a-z0-9]*(?:-[a-z0-9]+)+)[\s>/]/g;
575
+ while ((match = kebabPattern.exec(content)) !== null) {
576
+ const kebab = match[1];
577
+ const pascal = kebabMap.get(kebab);
578
+ if (pascal) {
579
+ componentUsageMap.set(pascal, (componentUsageMap.get(pascal) || 0) + 1);
580
+ }
581
+ }
582
+ }
583
+ catch {
584
+ // ignore unreadable files
585
+ }
586
+ }
587
+ }
588
+ /**
589
+ * Scan for Vue auto-registration patterns.
590
+ * Detects app.component('Name', ...) and Vue.component('Name', ...) calls.
591
+ */
592
+ async scanAutoRegistration(componentUsageMap) {
593
+ const cwd = process.cwd();
594
+ const sourceFiles = await glob('**/*.{ts,js,tsx,jsx}', {
595
+ cwd,
596
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**'],
597
+ nodir: true,
598
+ maxDepth: 4,
599
+ });
600
+ for (const file of sourceFiles.slice(0, 100)) {
601
+ try {
602
+ const content = await readFile(resolve(cwd, file), 'utf-8');
603
+ if (!content.includes('.component('))
604
+ continue;
605
+ // Match: app.component('Name', ...) or Vue.component('Name', ...)
606
+ const pattern = /\.component\(\s*['"]([A-Z][a-zA-Z0-9]*)['"]/g;
607
+ let match;
608
+ while ((match = pattern.exec(content)) !== null) {
609
+ const name = match[1];
610
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
611
+ }
612
+ }
613
+ catch {
614
+ // ignore unreadable files
615
+ }
616
+ }
617
+ }
618
+ /**
619
+ * Scan Angular NgModule files for declared components.
620
+ * Components in declarations: [...] are registered and should count as used.
621
+ */
622
+ async scanNgModuleDeclarations(componentUsageMap) {
623
+ const cwd = process.cwd();
624
+ const moduleFiles = await glob('**/*.module.ts', {
625
+ cwd,
626
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
627
+ nodir: true,
628
+ maxDepth: 8,
629
+ });
630
+ for (const file of moduleFiles.slice(0, 50)) {
631
+ try {
632
+ const content = await readFile(resolve(cwd, file), 'utf-8');
633
+ if (!content.includes('declarations'))
634
+ continue;
635
+ // Match declarations: [Component1, Component2, ...]
636
+ const declMatch = content.match(/declarations\s*:\s*\[([\s\S]*?)\]/);
637
+ if (declMatch) {
638
+ const names = declMatch[1].match(/\b([A-Z][a-zA-Z]+(?:Component|Directive|Pipe))\b/g);
639
+ if (names) {
640
+ for (const name of names) {
641
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
642
+ }
643
+ }
644
+ }
645
+ // Also match imports: [...] and exports: [...] arrays
646
+ for (const key of ['imports', 'exports']) {
647
+ const match = content.match(new RegExp(`${key}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
648
+ if (match) {
649
+ const names = match[1].match(/\b([A-Z][a-zA-Z]+(?:Component|Module))\b/g);
650
+ if (names) {
651
+ for (const name of names) {
652
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
653
+ }
654
+ }
655
+ }
656
+ }
657
+ }
658
+ catch {
659
+ // ignore unreadable files
660
+ }
661
+ }
662
+ }
663
+ /**
664
+ * Scan Storybook story files for component imports.
665
+ * A component referenced in a .stories file is documented/tested and should count as used.
666
+ */
667
+ async scanStoryFileUsages(componentUsageMap) {
668
+ const cwd = process.cwd();
669
+ const storyFiles = await glob('**/*.stories.{ts,tsx,js,jsx}', {
670
+ cwd,
671
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
672
+ nodir: true,
673
+ maxDepth: 8,
674
+ });
675
+ for (const file of storyFiles.slice(0, 200)) {
676
+ try {
677
+ const content = await readFile(resolve(cwd, file), 'utf-8');
678
+ // Match named imports: import { Button, Card } from '...'
679
+ const importPattern = /import\s*\{\s*([^}]+)\s*\}\s*from/g;
680
+ let match;
681
+ while ((match = importPattern.exec(content)) !== null) {
682
+ const names = match[1].split(',').map(n => {
683
+ const parts = n.trim().split(/\s+as\s+/);
684
+ return (parts[0] || '').trim();
685
+ }).filter(n => n && /^[A-Z]/.test(n));
686
+ for (const name of names) {
687
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
688
+ }
689
+ }
690
+ // Match default imports: import Button from '...'
691
+ const defaultImportPattern = /import\s+([A-Z][a-zA-Z0-9]*)\s+from/g;
692
+ while ((match = defaultImportPattern.exec(content)) !== null) {
693
+ const name = match[1];
694
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
695
+ }
696
+ // Match CSF3 component meta: component: Button or component: () => Button
697
+ const metaPattern = /component\s*:\s*([A-Z][a-zA-Z0-9]*)/g;
698
+ while ((match = metaPattern.exec(content)) !== null) {
699
+ const name = match[1];
700
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
701
+ }
702
+ }
703
+ catch {
704
+ // ignore unreadable files
705
+ }
706
+ }
707
+ }
708
+ /**
709
+ * Scan for Lit and Web Component registration patterns.
710
+ * customElements.define('my-button', MyButton) and @customElement('my-button')
711
+ * mean the component is registered and used by the browser.
712
+ */
713
+ async scanWebComponentRegistrations(componentUsageMap) {
714
+ const cwd = process.cwd();
715
+ const files = await glob('**/*.{ts,js}', {
716
+ cwd,
717
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.d.ts', '**/*.spec.*', '**/*.test.*'],
718
+ nodir: true,
719
+ maxDepth: 8,
720
+ });
721
+ for (const file of files.slice(0, 500)) {
722
+ try {
723
+ const content = await readFile(resolve(cwd, file), 'utf-8');
724
+ // Match customElements.define('tag-name', ClassName)
725
+ const definePattern = /customElements\.define\s*\(\s*['"][^'"]+['"]\s*,\s*([A-Z][a-zA-Z0-9]*)/g;
726
+ let match;
727
+ while ((match = definePattern.exec(content)) !== null) {
728
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
729
+ }
730
+ // Match @customElement('tag-name') decorator
731
+ const decoratorPattern = /@customElement\s*\(\s*['"][^'"]+['"]\s*\)/g;
732
+ if (decoratorPattern.test(content)) {
733
+ // The class following this decorator is registered
734
+ const classPattern = /class\s+([A-Z][a-zA-Z0-9]*)\s+extends/g;
735
+ while ((match = classPattern.exec(content)) !== null) {
736
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
737
+ }
738
+ }
739
+ }
740
+ catch {
741
+ // ignore unreadable files
742
+ }
743
+ }
744
+ }
745
+ /**
746
+ * In Nuxt 3, components in the components/ directory are auto-imported globally.
747
+ * If we detect Nuxt (nuxt.config.ts exists), mark all components in components/ as used.
748
+ */
749
+ async scanNuxtAutoImports(componentUsageMap, componentNames) {
750
+ const cwd = process.cwd();
751
+ // Check for Nuxt config
752
+ const nuxtConfigs = await glob('nuxt.config.{ts,js,mjs}', { cwd, nodir: true });
753
+ if (nuxtConfigs.length === 0)
754
+ return;
755
+ // In Nuxt, all components in components/ are auto-imported
756
+ for (const name of componentNames) {
757
+ // Mark as used (they're available globally via auto-import)
758
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
759
+ }
760
+ }
761
+ /**
762
+ * Scan test files for component imports.
763
+ * Components imported in .test.tsx/.spec.tsx are actively maintained/tested.
764
+ */
765
+ async scanTestFileUsages(componentUsageMap) {
766
+ const cwd = process.cwd();
767
+ const testFiles = await glob('**/*.{test,spec}.{ts,tsx,js,jsx}', {
768
+ cwd,
769
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
770
+ nodir: true,
771
+ maxDepth: 8,
772
+ });
773
+ for (const file of testFiles.slice(0, 300)) {
774
+ try {
775
+ const content = await readFile(resolve(cwd, file), 'utf-8');
776
+ // Match named imports: import { Button, Card } from '...'
777
+ const importPattern = /import\s*\{\s*([^}]+)\s*\}\s*from/g;
778
+ let match;
779
+ while ((match = importPattern.exec(content)) !== null) {
780
+ const names = match[1].split(',').map(n => {
781
+ const parts = n.trim().split(/\s+as\s+/);
782
+ return (parts[0] || '').trim();
783
+ }).filter(n => n && /^[A-Z]/.test(n));
784
+ for (const name of names) {
785
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
786
+ }
787
+ }
788
+ // Match default imports: import Button from '...'
789
+ const defaultImportPattern = /import\s+([A-Z][a-zA-Z0-9]*)\s+from/g;
790
+ while ((match = defaultImportPattern.exec(content)) !== null) {
791
+ const name = match[1];
792
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
793
+ }
794
+ }
795
+ catch {
796
+ // ignore unreadable files
797
+ }
798
+ }
799
+ }
800
+ /**
801
+ * Scan for HOC and wrapper patterns that count as component usage.
802
+ * Detects forwardRef(Component), memo(Component), styled(Component),
803
+ * withXxx(Component), Object.assign(Component, { ... }), and
804
+ * compound component patterns like Component.Sub = SubComponent.
805
+ */
806
+ async scanHOCWrapperUsages(componentUsageMap) {
807
+ const cwd = process.cwd();
808
+ const sourceFiles = await glob('**/*.{ts,tsx,js,jsx}', {
809
+ cwd,
810
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.d.ts'],
811
+ nodir: true,
812
+ maxDepth: 8,
813
+ });
814
+ for (const file of sourceFiles.slice(0, 500)) {
815
+ try {
816
+ const content = await readFile(resolve(cwd, file), 'utf-8');
817
+ // forwardRef(Component) / React.forwardRef(Component)
818
+ const forwardRefPattern = /(?:React\.)?forwardRef\s*[(<]\s*(?:function\s+)?([A-Z][a-zA-Z0-9]*)/g;
819
+ let match;
820
+ while ((match = forwardRefPattern.exec(content)) !== null) {
821
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
822
+ }
823
+ // Also: const X = forwardRef(...) — the wrapped result is the component
824
+ const forwardRefAssignPattern = /const\s+([A-Z][a-zA-Z0-9]*)\s*=\s*(?:React\.)?forwardRef/g;
825
+ while ((match = forwardRefAssignPattern.exec(content)) !== null) {
826
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
827
+ }
828
+ // memo(Component) / React.memo(Component)
829
+ const memoPattern = /(?:React\.)?memo\(\s*([A-Z][a-zA-Z0-9]*)\s*[,)]/g;
830
+ while ((match = memoPattern.exec(content)) !== null) {
831
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
832
+ }
833
+ // styled(Component) / styled.div / emotion patterns
834
+ const styledPattern = /styled\(\s*([A-Z][a-zA-Z0-9]*)\s*\)/g;
835
+ while ((match = styledPattern.exec(content)) !== null) {
836
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
837
+ }
838
+ // withXxx(Component) — HOC patterns
839
+ const hocPattern = /with[A-Z][a-zA-Z]*\(\s*([A-Z][a-zA-Z0-9]*)\s*[,)]/g;
840
+ while ((match = hocPattern.exec(content)) !== null) {
841
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
842
+ }
843
+ // Object.assign(Component, { Sub1, Sub2 }) — compound component pattern
844
+ const assignPattern = /Object\.assign\(\s*([A-Z][a-zA-Z0-9]*)\s*,\s*\{([^}]+)\}/g;
845
+ while ((match = assignPattern.exec(content)) !== null) {
846
+ // The base component is used
847
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
848
+ // Each assigned sub-component is used
849
+ const subs = match[2].match(/\b([A-Z][a-zA-Z0-9]*)\b/g);
850
+ if (subs) {
851
+ for (const sub of subs) {
852
+ componentUsageMap.set(sub, (componentUsageMap.get(sub) || 0) + 1);
853
+ }
854
+ }
855
+ }
856
+ // Component.Sub = SubComponent — compound component property assignment
857
+ const compoundPattern = /([A-Z][a-zA-Z0-9]*)\.([A-Z][a-zA-Z0-9]*)\s*=\s*([A-Z][a-zA-Z0-9]*)/g;
858
+ while ((match = compoundPattern.exec(content)) !== null) {
859
+ // Both the parent and the assigned component are used
860
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
861
+ componentUsageMap.set(match[3], (componentUsageMap.get(match[3]) || 0) + 1);
862
+ }
863
+ // React.createElement(Component, ...) — non-JSX rendering
864
+ const createElementPattern = /React\.createElement\(\s*([A-Z][a-zA-Z0-9]*)/g;
865
+ while ((match = createElementPattern.exec(content)) !== null) {
866
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
867
+ }
868
+ }
869
+ catch {
870
+ // ignore unreadable files
871
+ }
872
+ }
873
+ }
874
+ /**
875
+ * Detect component libraries and mark all exported components as used.
876
+ * If package.json has a "main" or "exports" field pointing to a barrel file,
877
+ * all components re-exported from that barrel are the product's public API.
878
+ */
879
+ async scanPackageExports(componentUsageMap) {
880
+ const cwd = process.cwd();
881
+ try {
882
+ const pkgContent = await readFile(resolve(cwd, 'package.json'), 'utf-8');
883
+ const pkg = JSON.parse(pkgContent);
884
+ // Detect if this is a component library by checking for:
885
+ // - exports field with ./ entries
886
+ // - main/module pointing to index file
887
+ // - "react" or "vue" in peerDependencies (library pattern)
888
+ const hasPeerReact = pkg.peerDependencies?.react || pkg.peerDependencies?.vue;
889
+ const hasExports = pkg.exports && typeof pkg.exports === 'object';
890
+ const mainEntry = pkg.main || pkg.module || '';
891
+ const isLibraryPattern = hasPeerReact && (hasExports || mainEntry);
892
+ if (!isLibraryPattern)
893
+ return;
894
+ // Scan the root barrel file(s) for exports — these are the public API
895
+ const rootBarrels = await glob('src/index.{ts,tsx,js,jsx}', { cwd, nodir: true });
896
+ for (const barrel of rootBarrels) {
897
+ try {
898
+ const content = await readFile(resolve(cwd, barrel), 'utf-8');
899
+ // Named exports: export { Button, Card } from '...'
900
+ const namedPattern = /export\s*\{\s*([^}]+)\s*\}\s*from/g;
901
+ let match;
902
+ while ((match = namedPattern.exec(content)) !== null) {
903
+ const names = match[1].split(',').map(n => {
904
+ const parts = n.trim().split(/\s+as\s+/);
905
+ return (parts[1] || parts[0] || '').trim();
906
+ }).filter(n => n && /^[A-Z]/.test(n));
907
+ for (const name of names) {
908
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
909
+ }
910
+ }
911
+ // Default re-exports: export { default as Button } from '...'
912
+ const defaultPattern = /export\s*\{\s*default\s+as\s+([A-Z][a-zA-Z0-9]*)\s*\}\s*from/g;
913
+ while ((match = defaultPattern.exec(content)) !== null) {
914
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
915
+ }
916
+ // Wildcard re-exports: export * from './Button'
917
+ const wildcardPattern = /export\s*\*\s*from\s*['"]\.\/([^'"]+)['"]/g;
918
+ while ((match = wildcardPattern.exec(content)) !== null) {
919
+ const moduleName = match[1];
920
+ const segments = moduleName.split('/');
921
+ const last = segments[segments.length - 1] || '';
922
+ if (/^[A-Z]/.test(last)) {
923
+ componentUsageMap.set(last, (componentUsageMap.get(last) || 0) + 1);
924
+ }
925
+ }
926
+ // Direct exports: export const Button = ... or export function Button
927
+ const directExportPattern = /export\s+(?:const|function|class)\s+([A-Z][a-zA-Z0-9]*)/g;
928
+ while ((match = directExportPattern.exec(content)) !== null) {
929
+ componentUsageMap.set(match[1], (componentUsageMap.get(match[1]) || 0) + 1);
930
+ }
931
+ }
932
+ catch {
933
+ // ignore
934
+ }
935
+ }
936
+ }
937
+ catch {
938
+ // No package.json or not parseable — not a library
939
+ }
940
+ }
941
+ /**
942
+ * Scan for components passed as values — JSX prop values, object property values,
943
+ * and array elements. Catches patterns like:
944
+ * transition={DialogTransition}
945
+ * { toolbarAccount: AccountPopover }
946
+ * [DialogTransition, BackdropTransition]
947
+ */
948
+ async scanComponentAsValueUsages(componentUsageMap, knownComponents) {
949
+ if (knownComponents.length === 0)
950
+ return;
951
+ const cwd = process.cwd();
952
+ const sourceFiles = await glob('**/*.{tsx,jsx,ts,js}', {
953
+ cwd,
954
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.d.ts'],
955
+ nodir: true,
956
+ maxDepth: 8,
957
+ });
958
+ // Build a Set for O(1) lookup
959
+ const knownSet = new Set(knownComponents);
960
+ for (const file of sourceFiles.slice(0, 500)) {
961
+ try {
962
+ const content = await readFile(resolve(cwd, file), 'utf-8');
963
+ // Match component names used as values in these contexts:
964
+ // 1. JSX prop value: ={ComponentName} or ={ComponentName}
965
+ // 2. Object property: : ComponentName, or : ComponentName}
966
+ // 3. Ternary with component: ? ComponentName : or : ComponentName}
967
+ // 4. Array element: [ComponentName, or , ComponentName]
968
+ //
969
+ // We look for PascalCase identifiers preceded by value-assignment contexts
970
+ const valuePattern = /(?:[=:?]\s*|,\s*|\[\s*)([A-Z][a-zA-Z0-9]*)\b/g;
971
+ let match;
972
+ while ((match = valuePattern.exec(content)) !== null) {
973
+ const name = match[1];
974
+ if (knownSet.has(name)) {
975
+ componentUsageMap.set(name, (componentUsageMap.get(name) || 0) + 1);
976
+ }
977
+ }
978
+ }
979
+ catch {
980
+ // ignore unreadable files
981
+ }
982
+ }
983
+ }
984
+ /**
985
+ * Detect repeated class patterns across source files
986
+ */
987
+ async detectRepeatedPatterns(config) {
988
+ const occurrences = [];
989
+ const cwd = process.cwd();
990
+ // Find all source files
991
+ const patterns = ["**/*.tsx", "**/*.jsx", "**/*.vue", "**/*.svelte"];
992
+ const ignore = ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"];
993
+ const files = await glob(patterns, { cwd, ignore, absolute: true });
994
+ for (const file of files) {
995
+ try {
996
+ const content = await readFile(file, "utf-8");
997
+ const relativePath = file.replace(cwd + "/", "");
998
+ // Extract static class strings using existing extractor
999
+ const classStrings = extractStaticClassStrings(content);
1000
+ for (const cs of classStrings) {
1001
+ // Combine all classes into a single string
1002
+ const allClasses = cs.classes.join(" ");
1003
+ if (allClasses.trim()) {
1004
+ occurrences.push({
1005
+ classes: allClasses,
1006
+ file: relativePath,
1007
+ line: cs.line,
1008
+ });
1009
+ }
1010
+ }
1011
+ }
1012
+ catch {
1013
+ // Skip files that can't be read
1014
+ }
1015
+ }
1016
+ return detectRepeatedPatterns(occurrences, {
1017
+ minOccurrences: config.minOccurrences ?? 2,
1018
+ matching: config.matching ?? "exact",
1019
+ });
1020
+ }
1021
+ }
1022
+ //# sourceMappingURL=drift-analysis.sync-conflict-20260309-133811-6PCZ3ZU.js.map