@buoy-design/core 0.3.33 → 0.3.35

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.
@@ -0,0 +1,141 @@
1
+ export interface AuditValue {
2
+ category: 'color' | 'spacing' | 'typography' | 'radius';
3
+ value: string;
4
+ file: string;
5
+ line: number;
6
+ }
7
+ export interface CategoryStats {
8
+ uniqueCount: number;
9
+ totalUsages: number;
10
+ mostCommon: Array<{
11
+ value: string;
12
+ count: number;
13
+ }>;
14
+ }
15
+ export interface FileIssue {
16
+ file: string;
17
+ issueCount: number;
18
+ }
19
+ export interface CloseMatch {
20
+ value: string;
21
+ closeTo: string;
22
+ distance: number;
23
+ }
24
+ export interface AuditReport {
25
+ categories: Record<string, CategoryStats>;
26
+ worstFiles: FileIssue[];
27
+ totals: {
28
+ uniqueValues: number;
29
+ totalUsages: number;
30
+ filesAffected: number;
31
+ };
32
+ closeMatches: CloseMatch[];
33
+ score: number;
34
+ }
35
+ /**
36
+ * Metrics gathered from drift analysis for health scoring.
37
+ * All counts come from drift signals and scan results.
38
+ */
39
+ export interface HealthMetrics {
40
+ /** Total components found in codebase */
41
+ componentCount: number;
42
+ /** Total design tokens defined */
43
+ tokenCount: number;
44
+ /** Number of hardcoded-value drift signals */
45
+ hardcodedValueCount: number;
46
+ /** Number of unused-token drift signals */
47
+ unusedTokenCount: number;
48
+ /** Number of naming-inconsistency drift signals */
49
+ namingInconsistencyCount: number;
50
+ /** Number of critical-severity drift signals */
51
+ criticalCount: number;
52
+ /** Whether a utility CSS framework (Tailwind) is detected */
53
+ hasUtilityFramework: boolean;
54
+ /** Whether a design system library (MUI, Chakra, shadcn, etc.) is detected */
55
+ hasDesignSystemLibrary: boolean;
56
+ /** Total drift signals of ALL types (hardcoded-value, naming-inconsistency, repeated-pattern, etc.) */
57
+ totalDriftCount?: number;
58
+ /** Number of unused-component drift signals (dead code) */
59
+ unusedComponentCount?: number;
60
+ /** Number of repeated-pattern drift signals (extract to shared component) */
61
+ repeatedPatternCount?: number;
62
+ /** Number of orphaned-component drift signals (dead code) */
63
+ orphanedComponentCount?: number;
64
+ /** Number of semantic-mismatch drift signals (naming/structure inconsistencies) */
65
+ semanticMismatchCount?: number;
66
+ /** Number of deprecated-pattern drift signals (technical debt) */
67
+ deprecatedPatternCount?: number;
68
+ /** Number of files with >2 hardcoded values (severe maintenance burden) */
69
+ highDensityFileCount?: number;
70
+ /** Number of drift signals from vendored/template components (e.g., shadcn/ui) */
71
+ vendoredDriftCount?: number;
72
+ /** Most common hardcoded color (for suggestions) */
73
+ topHardcodedColor?: {
74
+ value: string;
75
+ count: number;
76
+ };
77
+ /** File with the most drift issues */
78
+ worstFile?: {
79
+ path: string;
80
+ issueCount: number;
81
+ };
82
+ /** Total unique spacing values found */
83
+ uniqueSpacingValues?: number;
84
+ /** Names of detected frameworks/libraries (for framework-aware suggestions) */
85
+ detectedFrameworkNames?: string[];
86
+ }
87
+ export interface HealthPillar {
88
+ name: string;
89
+ score: number;
90
+ maxScore: number;
91
+ description: string;
92
+ }
93
+ export interface HealthScoreResult {
94
+ /** Overall score 0-100, or null if no UI surface detected */
95
+ score: number | null;
96
+ /** Tier label */
97
+ tier: 'Great' | 'Good' | 'OK' | 'Bad' | 'Terrible' | 'N/A';
98
+ /** Individual pillar scores */
99
+ pillars: {
100
+ valueDiscipline: HealthPillar;
101
+ tokenHealth: HealthPillar;
102
+ consistency: HealthPillar;
103
+ criticalIssues: HealthPillar;
104
+ };
105
+ /** Actionable improvement suggestions */
106
+ suggestions: string[];
107
+ /** Raw metrics used for scoring */
108
+ metrics: HealthMetrics;
109
+ }
110
+ /**
111
+ * Generate an audit report from extracted values
112
+ */
113
+ export declare function generateAuditReport(values: AuditValue[]): AuditReport;
114
+ /**
115
+ * Find values that are close to design tokens (likely typos)
116
+ */
117
+ export declare function findCloseMatches(foundValues: string[], designTokens: string[], category: 'color' | 'spacing' | 'typography' | 'radius'): CloseMatch[];
118
+ /**
119
+ * @deprecated Use calculateHealthScorePillar() with proper HealthMetrics instead.
120
+ * This legacy function approximates componentCount from filesAffected which
121
+ * is inaccurate. Kept for backward compatibility with external callers.
122
+ */
123
+ export declare function calculateHealthScore(report: AuditReport): number;
124
+ /**
125
+ * 4-Pillar Health Score System
126
+ *
127
+ * Measures design system health across four dimensions:
128
+ * - Value Discipline (0-60): Hardcoded values per component (density)
129
+ * - Token Health (0-20): Token system existence and adoption
130
+ * - Consistency (0-10): Naming convention adherence
131
+ * - Critical Issues (0-10): Accessibility and critical failures
132
+ *
133
+ * Tiers: 80-100 Great, 60-79 Good, 40-59 OK, 20-39 Bad, 0-19 Terrible
134
+ *
135
+ * Note: A perfect 100 requires maxing all 4 pillars simultaneously.
136
+ * In practice, the highest-scoring real-world apps reach ~95.
137
+ * A score of 95 represents near-perfect design system health.
138
+ */
139
+ export declare function calculateHealthScorePillar(metrics: HealthMetrics): HealthScoreResult;
140
+ export declare function getHealthTier(score: number): 'Great' | 'Good' | 'OK' | 'Bad' | 'Terrible';
141
+ //# sourceMappingURL=audit.sync-conflict-20260306-131031-6PCZ3ZU.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.sync-conflict-20260306-131031-6PCZ3ZU.d.ts","sourceRoot":"","sources":["../../src/analysis/audit.sync-conflict-20260306-131031-6PCZ3ZU.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,498 @@
1
+ // Audit report generation - analyzes codebase for design system health
2
+ /**
3
+ * Generate an audit report from extracted values
4
+ */
5
+ export function generateAuditReport(values) {
6
+ if (values.length === 0) {
7
+ return {
8
+ categories: {},
9
+ worstFiles: [],
10
+ totals: { uniqueValues: 0, totalUsages: 0, filesAffected: 0 },
11
+ closeMatches: [],
12
+ score: 100,
13
+ };
14
+ }
15
+ // Group by category
16
+ const byCategory = new Map();
17
+ const byFile = new Map();
18
+ const allFiles = new Set();
19
+ for (const v of values) {
20
+ // Category stats
21
+ if (!byCategory.has(v.category)) {
22
+ byCategory.set(v.category, new Map());
23
+ }
24
+ const catMap = byCategory.get(v.category);
25
+ catMap.set(v.value, (catMap.get(v.value) || 0) + 1);
26
+ // File stats
27
+ byFile.set(v.file, (byFile.get(v.file) || 0) + 1);
28
+ allFiles.add(v.file);
29
+ }
30
+ // Build category stats
31
+ const categories = {};
32
+ let totalUnique = 0;
33
+ for (const [category, valueMap] of byCategory) {
34
+ const entries = [...valueMap.entries()];
35
+ const uniqueCount = entries.length;
36
+ const totalUsages = entries.reduce((sum, [, count]) => sum + count, 0);
37
+ // Sort by count descending for mostCommon
38
+ const mostCommon = entries
39
+ .map(([value, count]) => ({ value, count }))
40
+ .sort((a, b) => b.count - a.count)
41
+ .slice(0, 10);
42
+ categories[category] = { uniqueCount, totalUsages, mostCommon };
43
+ totalUnique += uniqueCount;
44
+ }
45
+ // Build worst files list
46
+ const worstFiles = [...byFile.entries()]
47
+ .map(([file, issueCount]) => ({ file, issueCount }))
48
+ .sort((a, b) => b.issueCount - a.issueCount)
49
+ .slice(0, 10);
50
+ const report = {
51
+ categories,
52
+ worstFiles,
53
+ totals: {
54
+ uniqueValues: totalUnique,
55
+ totalUsages: values.length,
56
+ filesAffected: allFiles.size,
57
+ },
58
+ closeMatches: [],
59
+ score: 0,
60
+ };
61
+ report.score = calculateHealthScore(report);
62
+ return report;
63
+ }
64
+ /**
65
+ * Find values that are close to design tokens (likely typos)
66
+ */
67
+ export function findCloseMatches(foundValues, designTokens, category) {
68
+ const matches = [];
69
+ const tokenSet = new Set(designTokens.map((t) => t.toLowerCase()));
70
+ for (const value of foundValues) {
71
+ const valueLower = value.toLowerCase();
72
+ // Skip exact matches
73
+ if (tokenSet.has(valueLower)) {
74
+ continue;
75
+ }
76
+ // Find closest token
77
+ let closestToken = null;
78
+ let closestDistance = Infinity;
79
+ for (const token of designTokens) {
80
+ const distance = getDistance(value, token, category);
81
+ if (distance < closestDistance && distance > 0) {
82
+ closestDistance = distance;
83
+ closestToken = token;
84
+ }
85
+ }
86
+ // Only include if close enough (threshold depends on category)
87
+ const threshold = category === 'color' ? 5 : 2;
88
+ if (closestToken && closestDistance <= threshold) {
89
+ matches.push({
90
+ value,
91
+ closeTo: closestToken,
92
+ distance: closestDistance,
93
+ });
94
+ }
95
+ }
96
+ return matches;
97
+ }
98
+ /**
99
+ * Calculate distance between two values
100
+ */
101
+ function getDistance(a, b, category) {
102
+ if (category === 'color') {
103
+ return colorDistance(a, b);
104
+ }
105
+ if (category === 'spacing' || category === 'radius') {
106
+ return numericDistance(a, b);
107
+ }
108
+ // For typography, use simple string comparison
109
+ return a.toLowerCase() === b.toLowerCase() ? 0 : Infinity;
110
+ }
111
+ /**
112
+ * Calculate color distance (simple hex comparison)
113
+ */
114
+ function colorDistance(a, b) {
115
+ const hexA = a.replace('#', '').toLowerCase();
116
+ const hexB = b.replace('#', '').toLowerCase();
117
+ if (hexA.length !== 6 || hexB.length !== 6) {
118
+ return Infinity;
119
+ }
120
+ // Count differing characters
121
+ let diff = 0;
122
+ for (let i = 0; i < 6; i++) {
123
+ if (hexA[i] !== hexB[i]) {
124
+ diff++;
125
+ }
126
+ }
127
+ return diff;
128
+ }
129
+ /**
130
+ * Calculate numeric distance for spacing/radius
131
+ */
132
+ function numericDistance(a, b) {
133
+ const numA = parseFloat(a);
134
+ const numB = parseFloat(b);
135
+ if (isNaN(numA) || isNaN(numB)) {
136
+ return Infinity;
137
+ }
138
+ return Math.abs(numA - numB);
139
+ }
140
+ /**
141
+ * @deprecated Use calculateHealthScorePillar() with proper HealthMetrics instead.
142
+ * This legacy function approximates componentCount from filesAffected which
143
+ * is inaccurate. Kept for backward compatibility with external callers.
144
+ */
145
+ export function calculateHealthScore(report) {
146
+ const metrics = {
147
+ componentCount: report.totals.filesAffected || 1,
148
+ tokenCount: 0,
149
+ hardcodedValueCount: report.totals.uniqueValues,
150
+ unusedTokenCount: 0,
151
+ namingInconsistencyCount: 0,
152
+ criticalCount: 0,
153
+ hasUtilityFramework: false,
154
+ hasDesignSystemLibrary: false,
155
+ };
156
+ return calculateHealthScorePillar(metrics).score ?? 0;
157
+ }
158
+ /**
159
+ * 4-Pillar Health Score System
160
+ *
161
+ * Measures design system health across four dimensions:
162
+ * - Value Discipline (0-60): Hardcoded values per component (density)
163
+ * - Token Health (0-20): Token system existence and adoption
164
+ * - Consistency (0-10): Naming convention adherence
165
+ * - Critical Issues (0-10): Accessibility and critical failures
166
+ *
167
+ * Tiers: 80-100 Great, 60-79 Good, 40-59 OK, 20-39 Bad, 0-19 Terrible
168
+ *
169
+ * Note: A perfect 100 requires maxing all 4 pillars simultaneously.
170
+ * In practice, the highest-scoring real-world apps reach ~95.
171
+ * A score of 95 represents near-perfect design system health.
172
+ */
173
+ export function calculateHealthScorePillar(metrics) {
174
+ const suggestions = [];
175
+ const frameworks = metrics.detectedFrameworkNames ?? [];
176
+ // No UI surface area — can't evaluate design system health
177
+ if (metrics.componentCount === 0 && metrics.tokenCount === 0 && (metrics.totalDriftCount ?? 0) === 0) {
178
+ return {
179
+ score: null,
180
+ tier: 'N/A',
181
+ pillars: {
182
+ valueDiscipline: { name: 'Value Discipline', score: 0, maxScore: 60, description: 'Hardcoded values per component' },
183
+ tokenHealth: { name: 'Token Health', score: 0, maxScore: 20, description: 'Token system adoption' },
184
+ consistency: { name: 'Consistency', score: 0, maxScore: 10, description: 'Naming convention adherence' },
185
+ criticalIssues: { name: 'Critical Issues', score: 0, maxScore: 10, description: 'Accessibility and critical failures' },
186
+ },
187
+ suggestions: ['No UI components or design tokens detected — this repo may not need design system health tracking'],
188
+ metrics,
189
+ };
190
+ }
191
+ // Pillar 1: Value Discipline (0-60)
192
+ // Primary: hardcoded value density
193
+ // Secondary: dead code density (unused/orphaned components, repeated patterns)
194
+ // Tertiary: total drift density as backstop
195
+ const userHardcodedCount = Math.max(0, metrics.hardcodedValueCount - (metrics.vendoredDriftCount ?? 0));
196
+ const hardcodedDensity = userHardcodedCount / Math.max(metrics.componentCount, 1);
197
+ const deadCodeCount = (metrics.unusedComponentCount ?? 0)
198
+ + (metrics.orphanedComponentCount ?? 0)
199
+ + (metrics.repeatedPatternCount ?? 0);
200
+ const deadCodeDensity = deadCodeCount / Math.max(metrics.componentCount, 1);
201
+ const totalDriftDensity = (metrics.totalDriftCount ?? metrics.hardcodedValueCount) / Math.max(metrics.componentCount, 1);
202
+ // Hardcoded density is primary; dead code adds 30% partial penalty; total drift as backstop
203
+ const density = Math.max(hardcodedDensity + deadCodeDensity * 0.3, totalDriftDensity * 0.5);
204
+ // Treat near-zero density as perfect: repos with <0.1 hardcoded values per component
205
+ // are effectively clean (e.g., 193 components with 12 hardcoded values = 0.06/comp)
206
+ const effectiveDensity = density < 0.1 ? 0 : density;
207
+ // Continuous scoring (no rounding) — round only the final total for more granular scores
208
+ const valueDisciplineRaw = 60 * clamp(1 - effectiveDensity / 2, 0, 1);
209
+ const valueDisciplineScore = Math.round(valueDisciplineRaw);
210
+ if (metrics.hardcodedValueCount > 0) {
211
+ const hasTailwind = frameworks.includes('tailwind');
212
+ const hasShadcn = frameworks.includes('shadcn');
213
+ const hasMui = frameworks.includes('mui');
214
+ let suggestion;
215
+ if (density > 1.0) {
216
+ // Severe — high urgency
217
+ suggestion = `${userHardcodedCount} hardcoded values across your components — high density`;
218
+ if (metrics.topHardcodedColor) {
219
+ suggestion += `. Most common: ${metrics.topHardcodedColor.value} (${metrics.topHardcodedColor.count}\u00d7)`;
220
+ }
221
+ if (hasTailwind) {
222
+ suggestion += '. Add values to your Tailwind theme config instead of using arbitrary values';
223
+ }
224
+ else if (hasMui) {
225
+ suggestion += '. Use the `sx` prop or `theme.palette` instead of inline colors';
226
+ }
227
+ else {
228
+ suggestion += '. Create a design token file (e.g., tokens.css with CSS custom properties)';
229
+ }
230
+ }
231
+ else if (density > 0.3) {
232
+ // Moderate
233
+ suggestion = `${userHardcodedCount} hardcoded value${userHardcodedCount === 1 ? '' : 's'} to extract`;
234
+ if (hasTailwind && hasShadcn) {
235
+ suggestion += ' — use `cn()` utility with Tailwind theme classes';
236
+ }
237
+ else if (hasTailwind) {
238
+ suggestion += ' — extend your tailwind.config theme instead of arbitrary values';
239
+ }
240
+ if (metrics.worstFile) {
241
+ suggestion += `. Focus on ${metrics.worstFile.path} (${metrics.worstFile.issueCount} issues)`;
242
+ }
243
+ }
244
+ else {
245
+ // Low — encouraging
246
+ suggestion = `Nearly there — just ${userHardcodedCount} hardcoded value${userHardcodedCount === 1 ? '' : 's'} left to tokenize`;
247
+ if (metrics.worstFile && metrics.worstFile.issueCount > 1) {
248
+ suggestion += `. ${metrics.worstFile.path} has the most (${metrics.worstFile.issueCount})`;
249
+ }
250
+ }
251
+ suggestions.push(suggestion);
252
+ }
253
+ if ((metrics.vendoredDriftCount ?? 0) > 0) {
254
+ suggestions.push(`${metrics.vendoredDriftCount} additional hardcoded values in vendored components (shadcn/ui templates) — these are from the library, not your code`);
255
+ }
256
+ // Dead code suggestions (threshold-based to reduce noise)
257
+ const unusedComponentCount = metrics.unusedComponentCount ?? 0;
258
+ const repeatedPatternCount = metrics.repeatedPatternCount ?? 0;
259
+ if (unusedComponentCount > 10) {
260
+ if (unusedComponentCount > 50) {
261
+ suggestions.push(`${unusedComponentCount} unused components detected — consider a cleanup sprint to remove dead code`);
262
+ }
263
+ else {
264
+ suggestions.push(`${unusedComponentCount} unused components — review and remove dead code`);
265
+ }
266
+ }
267
+ if (repeatedPatternCount > 5) {
268
+ if (repeatedPatternCount > 20) {
269
+ suggestions.push(`${repeatedPatternCount} repeated patterns — significant duplication, extract to a shared component library`);
270
+ }
271
+ else {
272
+ suggestions.push(`${repeatedPatternCount} repeated patterns — extract to shared components`);
273
+ }
274
+ }
275
+ // Pillar 2: Token Health (0-20)
276
+ // Four sub-factors, each 0-5 points:
277
+ // 1. Utility framework (0-5): Has Tailwind, CSS-in-JS, etc.
278
+ // 2. Design system library (0-5): Has MUI, Chakra, Mantine, etc.
279
+ // 3. Token definition coverage (0-5): Has tokens across categories
280
+ // 4. Token usage ratio (0-5): What fraction of defined tokens are used
281
+ // Sub-factor 1: Utility framework (0-5)
282
+ // Scaled: 3 for having the framework, +2 when tokens are also defined
283
+ const utilityPoints = metrics.hasUtilityFramework
284
+ ? (metrics.tokenCount > 0 ? 5 : 3)
285
+ : 0;
286
+ // Sub-factor 2: Design system library (0-5)
287
+ // Scaled: 3 for having the library, +2 when tokens are also defined
288
+ const libraryPoints = metrics.hasDesignSystemLibrary
289
+ ? (metrics.tokenCount > 0 ? 5 : 3)
290
+ : 0;
291
+ // Sub-factor 3: Token definition coverage (0-5)
292
+ // More tokens = more structured. Cap at 20 tokens for full credit.
293
+ // Continuous (not rounded) for granular scoring.
294
+ const tokenCoveragePoints = metrics.tokenCount > 0
295
+ ? 5 * clamp(metrics.tokenCount / 20, 0, 1)
296
+ : 0;
297
+ // Sub-factor 4: Token usage ratio (0-5)
298
+ // What fraction of tokens are actually used? All used = 5, all unused = 0.
299
+ let tokenUsagePoints;
300
+ if (metrics.tokenCount > 0) {
301
+ const usedTokens = metrics.tokenCount - metrics.unusedTokenCount;
302
+ tokenUsagePoints = 5 * clamp(usedTokens / metrics.tokenCount, 0, 1);
303
+ }
304
+ else if (metrics.hasUtilityFramework || metrics.hasDesignSystemLibrary) {
305
+ // No explicit tokens but has a framework/library — give partial credit
306
+ // because the framework handles tokenization internally
307
+ tokenUsagePoints = density < 0.5 ? 5 : density < 1.0 ? 3 : 1;
308
+ }
309
+ else if (density < 0.1) {
310
+ // No explicit system but very few hardcoded values = implied system
311
+ tokenUsagePoints = 3;
312
+ }
313
+ else {
314
+ tokenUsagePoints = 0;
315
+ }
316
+ // Round the combined score for integer totals
317
+ const tokenHealthScore = Math.round(utilityPoints + libraryPoints + tokenCoveragePoints + tokenUsagePoints);
318
+ // Token health suggestions (threshold-based)
319
+ if (metrics.tokenCount > 0 && metrics.unusedTokenCount > 5) {
320
+ const unusedPct = Math.round((metrics.unusedTokenCount / metrics.tokenCount) * 100);
321
+ if (unusedPct > 50) {
322
+ suggestions.push(`${metrics.unusedTokenCount} of ${metrics.tokenCount} tokens unused (${unusedPct}%) — many tokens may be stale, audit your token definitions`);
323
+ }
324
+ else {
325
+ suggestions.push(`${metrics.unusedTokenCount} tokens defined but unused — wire them into components or remove stale definitions`);
326
+ }
327
+ }
328
+ if (tokenHealthScore < 10 && metrics.componentCount > 0) {
329
+ if (frameworks.includes('tailwind')) {
330
+ suggestions.push('Tailwind detected but token coverage is low — extend your theme config with custom values');
331
+ }
332
+ else if (frameworks.includes('mui') || frameworks.includes('chakra') || frameworks.includes('mantine')) {
333
+ const lib = frameworks.find(f => ['mui', 'chakra', 'mantine'].includes(f)) ?? 'your library';
334
+ suggestions.push(`${lib} detected — use its theming API for consistent values across components`);
335
+ }
336
+ else {
337
+ suggestions.push('No design token system detected — add CSS custom properties or a utility framework like Tailwind');
338
+ }
339
+ }
340
+ // Pillar 3: Consistency (0-10)
341
+ // Includes naming-inconsistency + semantic-mismatch signals
342
+ const inconsistencyCount = metrics.namingInconsistencyCount + (metrics.semanticMismatchCount ?? 0);
343
+ const namingRate = inconsistencyCount / Math.max(metrics.componentCount, 1);
344
+ const consistencyRaw = 10 * clamp(1 - namingRate / 0.25, 0, 1);
345
+ // Only surface naming inconsistencies above noise threshold
346
+ if (inconsistencyCount > 3 || (inconsistencyCount > 0 && namingRate > 0.05)) {
347
+ if (namingRate > 0.15) {
348
+ suggestions.push(`${inconsistencyCount} naming/semantic inconsistencies across ${metrics.componentCount} components — establish and document naming conventions`);
349
+ }
350
+ else if (namingRate > 0.05) {
351
+ suggestions.push(`${inconsistencyCount} naming inconsistencies — standardize prop/component conventions`);
352
+ }
353
+ else {
354
+ suggestions.push(`${inconsistencyCount} minor naming inconsistencies — consider standardizing conventions`);
355
+ }
356
+ }
357
+ // Pillar 4: Critical Issues (0-10)
358
+ // Includes critical severity + deprecated patterns (2 deprecated = 1 critical equivalent)
359
+ const deprecatedCount = metrics.deprecatedPatternCount ?? 0;
360
+ const highDensityFiles = metrics.highDensityFileCount ?? 0;
361
+ const effectiveCriticalCount = metrics.criticalCount
362
+ + Math.ceil(deprecatedCount / 2)
363
+ + Math.floor(highDensityFiles / 3);
364
+ // Use 2-point steps for finer granularity (was 3-point)
365
+ const criticalRaw = Math.max(0, 10 - effectiveCriticalCount * 2);
366
+ if (metrics.criticalCount > 0) {
367
+ suggestions.push(`${metrics.criticalCount} critical issue${metrics.criticalCount === 1 ? '' : 's'} (accessibility/contrast) — fix immediately`);
368
+ }
369
+ if (deprecatedCount > 0) {
370
+ suggestions.push(`${deprecatedCount} deprecated pattern${deprecatedCount === 1 ? '' : 's'} — migrate to current API`);
371
+ }
372
+ if (metrics.uniqueSpacingValues && metrics.uniqueSpacingValues > 15) {
373
+ suggestions.push(`${metrics.uniqueSpacingValues} unique spacing values — consolidate to a consistent spacing scale (e.g., 4px/8px grid)`);
374
+ }
375
+ // Scale consistency and criticalIssues for repos with very few components
376
+ // Prevents trivially maxing these pillars when there's nothing to evaluate
377
+ const componentScale = Math.min(metrics.componentCount / 3, 1);
378
+ // Use raw (unrounded) values for total calculation to produce more distinct scores
379
+ const scaledConsistencyRaw = consistencyRaw * componentScale;
380
+ const scaledCriticalRaw = criticalRaw * componentScale;
381
+ const scaledConsistencyScore = Math.round(scaledConsistencyRaw);
382
+ const scaledCriticalScore = Math.round(scaledCriticalRaw);
383
+ // Total uses raw values, rounded once at the end for maximum granularity
384
+ let total = Math.round(valueDisciplineRaw + tokenHealthScore + scaledConsistencyRaw + scaledCriticalRaw);
385
+ // Drift density penalty: prevent high-drift repos from scoring "Great"
386
+ // Apps with many drift signals relative to their size should be capped
387
+ const totalDrift = metrics.totalDriftCount ?? metrics.hardcodedValueCount;
388
+ if (totalDrift > 0 && metrics.componentCount > 0) {
389
+ const driftPerComponent = totalDrift / metrics.componentCount;
390
+ if (totalDrift > 200) {
391
+ // >200 drift signals: cap at OK (69)
392
+ total = Math.min(total, 69);
393
+ }
394
+ else if (totalDrift > 100) {
395
+ // >100 drift: graduated cap based on density (74-84)
396
+ const densityFactor = clamp(driftPerComponent, 0, 1);
397
+ const cap = Math.round(74 + (1 - densityFactor) * 10);
398
+ total = Math.min(total, cap);
399
+ }
400
+ else if (totalDrift > 50 && driftPerComponent > 0.3) {
401
+ // >50 drift + high density: cap at 89
402
+ total = Math.min(total, 89);
403
+ }
404
+ }
405
+ // Ensure every scored app gets at least 1 suggestion
406
+ if (suggestions.length === 0) {
407
+ if (total === 100) {
408
+ suggestions.push('Perfect design system health — no issues detected');
409
+ }
410
+ else if (total >= 90) {
411
+ // Find the weakest pillar for aspirational suggestions
412
+ const vdPct = valueDisciplineScore / 60;
413
+ const thPct = tokenHealthScore / 20;
414
+ const coPct = scaledConsistencyScore / 10;
415
+ const ciPct = scaledCriticalScore / 10;
416
+ const minPct = Math.min(vdPct, thPct, coPct, ciPct);
417
+ if (minPct === vdPct && valueDisciplineScore < 60) {
418
+ suggestions.push(`Score ${total} — to reach 100, reduce the remaining hardcoded values in your components`);
419
+ }
420
+ else if (minPct === thPct && tokenHealthScore < 20) {
421
+ suggestions.push(`Score ${total} — to reach 100, improve token coverage and usage`);
422
+ }
423
+ else if (minPct === coPct && scaledConsistencyScore < 10) {
424
+ suggestions.push(`Score ${total} — to reach 100, address remaining naming inconsistencies`);
425
+ }
426
+ else {
427
+ suggestions.push(`Score ${total} — nearly perfect, review remaining drift signals for final improvements`);
428
+ }
429
+ }
430
+ else {
431
+ // 80-89: point to the weakest pillar
432
+ const vdPct = valueDisciplineScore / 60;
433
+ const thPct = tokenHealthScore / 20;
434
+ const coPct = metrics.componentCount >= 3 ? scaledConsistencyScore / 10 : 1;
435
+ const ciPct = metrics.componentCount >= 3 ? scaledCriticalScore / 10 : 1;
436
+ const minPct = Math.min(vdPct, thPct, coPct, ciPct);
437
+ if (minPct === vdPct) {
438
+ suggestions.push(`Your weakest area is Value Discipline (${valueDisciplineScore}/60) — focus on reducing hardcoded values`);
439
+ }
440
+ else if (minPct === thPct) {
441
+ suggestions.push(`Your weakest area is Token Health (${tokenHealthScore}/20) — improve token definitions and usage`);
442
+ }
443
+ else if (minPct === coPct) {
444
+ suggestions.push(`Your weakest area is Consistency (${scaledConsistencyScore}/10) — standardize naming conventions`);
445
+ }
446
+ else {
447
+ suggestions.push(`Your weakest area is Critical Issues (${scaledCriticalScore}/10) — address accessibility and deprecated patterns`);
448
+ }
449
+ }
450
+ }
451
+ return {
452
+ score: total,
453
+ tier: getHealthTier(total),
454
+ pillars: {
455
+ valueDiscipline: {
456
+ name: 'Value Discipline',
457
+ score: valueDisciplineScore,
458
+ maxScore: 60,
459
+ description: 'Hardcoded values per component',
460
+ },
461
+ tokenHealth: {
462
+ name: 'Token Health',
463
+ score: tokenHealthScore,
464
+ maxScore: 20,
465
+ description: 'Token system adoption',
466
+ },
467
+ consistency: {
468
+ name: 'Consistency',
469
+ score: scaledConsistencyScore,
470
+ maxScore: 10,
471
+ description: 'Naming convention adherence',
472
+ },
473
+ criticalIssues: {
474
+ name: 'Critical Issues',
475
+ score: scaledCriticalScore,
476
+ maxScore: 10,
477
+ description: 'Accessibility and critical failures',
478
+ },
479
+ },
480
+ suggestions,
481
+ metrics,
482
+ };
483
+ }
484
+ function clamp(value, min, max) {
485
+ return Math.min(max, Math.max(min, value));
486
+ }
487
+ export function getHealthTier(score) {
488
+ if (score >= 80)
489
+ return 'Great';
490
+ if (score >= 60)
491
+ return 'Good';
492
+ if (score >= 40)
493
+ return 'OK';
494
+ if (score >= 20)
495
+ return 'Bad';
496
+ return 'Terrible';
497
+ }
498
+ //# sourceMappingURL=audit.sync-conflict-20260306-131031-6PCZ3ZU.js.map