@lifeonlars/prime-yggdrasil 0.2.6 → 0.4.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.
@@ -0,0 +1,447 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { validateCommand } from './validate.js';
3
+
4
+ // Import Phase 6 rules for autofix
5
+ import interactionPatternsRules from '../rules/interaction-patterns/index.js';
6
+ import accessibilityRules from '../rules/accessibility/index.js';
7
+
8
+ /**
9
+ * Audit command - Detailed analysis with autofix
10
+ *
11
+ * Provides detailed analysis of violations with autofix suggestions.
12
+ * Can automatically fix certain violations when --fix flag is used.
13
+ *
14
+ * For simple report-only validation, use the validate command.
15
+ */
16
+
17
+ /**
18
+ * Autofix strategies for each rule
19
+ */
20
+ const AUTOFIXES = {
21
+ 'no-utility-on-components': {
22
+ canAutoFix: true,
23
+ fix: (content, violation) => {
24
+ // Remove utility classes from PrimeReact components
25
+ // This is a simplified version - real implementation would use AST parsing
26
+ const lines = content.split('\n');
27
+ const line = lines[violation.line - 1];
28
+
29
+ // Extract utility classes to remove
30
+ const utilityMatch = violation.message.match(/classes? "([^"]+)"/);
31
+ if (utilityMatch) {
32
+ const utilities = utilityMatch[1].split(', ');
33
+
34
+ let fixedLine = line;
35
+ utilities.forEach(utility => {
36
+ // Remove the utility class
37
+ fixedLine = fixedLine.replace(new RegExp(`\\b${utility}\\b\\s*`, 'g'), '');
38
+ });
39
+
40
+ // Clean up empty className
41
+ fixedLine = fixedLine.replace(/className=["'`]\s*["'`]/g, '');
42
+
43
+ lines[violation.line - 1] = fixedLine;
44
+ return lines.join('\n');
45
+ }
46
+
47
+ return content;
48
+ },
49
+ explanation: 'Removes PrimeFlex utility classes from PrimeReact components. The theme handles all component styling.'
50
+ },
51
+
52
+ 'no-tailwind': {
53
+ canAutoFix: true,
54
+ fix: (content, violation) => {
55
+ // Remove Tailwind classes
56
+ const lines = content.split('\n');
57
+ const line = lines[violation.line - 1];
58
+
59
+ const tailwindMatch = violation.message.match(/classes? "([^"]+)"/);
60
+ if (tailwindMatch) {
61
+ const tailwindClasses = tailwindMatch[1].split(', ');
62
+
63
+ let fixedLine = line;
64
+ tailwindClasses.forEach(cls => {
65
+ fixedLine = fixedLine.replace(new RegExp(`\\b${cls}\\b\\s*`, 'g'), '');
66
+ });
67
+
68
+ // Clean up empty className
69
+ fixedLine = fixedLine.replace(/className=["'`]\s*["'`]/g, '');
70
+
71
+ lines[violation.line - 1] = fixedLine;
72
+ return lines.join('\n');
73
+ }
74
+
75
+ return content;
76
+ },
77
+ explanation: 'Removes Tailwind CSS classes. Use PrimeFlex for layout or semantic tokens for design.'
78
+ },
79
+
80
+ 'no-hardcoded-colors': {
81
+ canAutoFix: false,
82
+ suggestions: [
83
+ 'Replace with semantic token: var(--surface-neutral-primary) for backgrounds',
84
+ 'Replace with semantic token: var(--text-neutral-default) for text',
85
+ 'Replace with semantic token: var(--border-neutral-default) for borders',
86
+ 'Consult .ai/yggdrasil/semantic-token-intent.md for complete token catalog'
87
+ ],
88
+ explanation: 'Cannot auto-fix - requires semantic understanding of color intent. Use semantic tokens based on the element\'s purpose.'
89
+ },
90
+
91
+ 'semantic-tokens-only': {
92
+ canAutoFix: false,
93
+ suggestions: [
94
+ 'var(--blue-500) → var(--surface-brand-primary) or var(--text-state-interactive)',
95
+ 'var(--green-500) → var(--surface-context-success) or var(--text-context-success)',
96
+ 'var(--red-500) → var(--surface-context-danger) or var(--text-context-danger)',
97
+ 'var(--gray-100) → var(--surface-neutral-secondary) or var(--text-neutral-subdued)',
98
+ 'Consult .ai/yggdrasil/semantic-token-intent.md for complete mapping'
99
+ ],
100
+ explanation: 'Cannot auto-fix - requires understanding of token purpose. Foundation tokens are for theme definition only.'
101
+ },
102
+
103
+ 'valid-spacing': {
104
+ canAutoFix: true,
105
+ fix: (content, violation) => {
106
+ const lines = content.split('\n');
107
+ const line = lines[violation.line - 1];
108
+
109
+ // Extract off-grid value and nearest suggestion
110
+ const valueMatch = violation.message.match(/(\d+)px/);
111
+ const suggestionMatch = violation.suggestion.match(/(\d+)px/);
112
+
113
+ if (valueMatch && suggestionMatch) {
114
+ const oldValue = valueMatch[1];
115
+ const newValue = suggestionMatch[1];
116
+
117
+ // Replace the value
118
+ const fixedLine = line.replace(
119
+ new RegExp(`${oldValue}px`, 'g'),
120
+ `${newValue}px`
121
+ );
122
+
123
+ lines[violation.line - 1] = fixedLine;
124
+ return lines.join('\n');
125
+ }
126
+
127
+ // Handle invalid PrimeFlex classes
128
+ const classMatch = violation.message.match(/([pm][trblxy]?-\d+)/);
129
+ if (classMatch) {
130
+ const invalidClass = classMatch[1];
131
+ const number = parseInt(invalidClass.match(/\d+/)[0], 10);
132
+ const nearestValid = Math.min(8, Math.round(number / 4) * 4 / 4);
133
+ const validClass = invalidClass.replace(/\d+/, nearestValid);
134
+
135
+ const fixedLine = line.replace(invalidClass, validClass);
136
+ lines[violation.line - 1] = fixedLine;
137
+ return lines.join('\n');
138
+ }
139
+
140
+ return content;
141
+ },
142
+ explanation: 'Rounds spacing values to nearest 4px grid value (0, 4, 8, 12, 16, 20, 24, 28, 32px).'
143
+ },
144
+
145
+ // Phase 6 Autofixes: Dynamically add from rule definitions
146
+ ...Object.fromEntries(
147
+ Object.entries({ ...interactionPatternsRules, ...accessibilityRules }).map(([key, rule]) => [
148
+ key,
149
+ {
150
+ canAutoFix: typeof rule.autofix === 'function',
151
+ fix: (content, violation) => {
152
+ if (rule.autofix) {
153
+ const result = rule.autofix(content, violation);
154
+ return result.fixed ? result.content : content;
155
+ }
156
+ return content;
157
+ },
158
+ explanation: rule.autofix ? `${rule.description} (Phase 6 rule)` : 'Manual fix required'
159
+ }
160
+ ])
161
+ )
162
+ };
163
+
164
+ /**
165
+ * Generate detailed audit report
166
+ */
167
+ function generateAuditReport(results) {
168
+ const report = {
169
+ summary: {
170
+ totalFiles: 0,
171
+ filesWithViolations: 0,
172
+ totalViolations: 0,
173
+ errors: 0,
174
+ warnings: 0,
175
+ autoFixable: 0
176
+ },
177
+ violations: [],
178
+ recommendations: []
179
+ };
180
+
181
+ Object.entries(results).forEach(([filePath, fileResults]) => {
182
+ report.summary.totalFiles++;
183
+
184
+ if (Object.keys(fileResults).length > 0 && !fileResults.error) {
185
+ report.summary.filesWithViolations++;
186
+
187
+ Object.entries(fileResults).forEach(([ruleId, result]) => {
188
+ result.violations.forEach(violation => {
189
+ report.summary.totalViolations++;
190
+
191
+ if (result.severity === 'error') {
192
+ report.summary.errors++;
193
+ } else {
194
+ report.summary.warnings++;
195
+ }
196
+
197
+ const autofix = AUTOFIXES[ruleId];
198
+ if (autofix?.canAutoFix) {
199
+ report.summary.autoFixable++;
200
+ }
201
+
202
+ report.violations.push({
203
+ file: filePath,
204
+ rule: ruleId,
205
+ ruleName: result.rule,
206
+ severity: result.severity,
207
+ line: violation.line,
208
+ column: violation.column,
209
+ message: violation.message,
210
+ suggestion: violation.suggestion,
211
+ autoFixable: autofix?.canAutoFix || false,
212
+ autoFixExplanation: autofix?.explanation,
213
+ manualFixSuggestions: autofix?.suggestions
214
+ });
215
+ });
216
+ });
217
+ }
218
+ });
219
+
220
+ // Generate recommendations
221
+ const violationsByRule = {};
222
+ report.violations.forEach(v => {
223
+ violationsByRule[v.rule] = (violationsByRule[v.rule] || 0) + 1;
224
+ });
225
+
226
+ const sortedRules = Object.entries(violationsByRule)
227
+ .sort((a, b) => b[1] - a[1])
228
+ .slice(0, 3);
229
+
230
+ if (sortedRules.length > 0) {
231
+ report.recommendations.push({
232
+ priority: 'high',
233
+ title: 'Focus on Most Common Violations',
234
+ description: `Top violations: ${sortedRules.map(([rule, count]) => `${rule} (${count})`).join(', ')}`
235
+ });
236
+ }
237
+
238
+ if (report.summary.autoFixable > 0) {
239
+ report.recommendations.push({
240
+ priority: 'high',
241
+ title: 'Run Autofix',
242
+ description: `${report.summary.autoFixable} violations can be automatically fixed. Run: yggdrasil audit --fix`
243
+ });
244
+ }
245
+
246
+ if (violationsByRule['no-hardcoded-colors'] || violationsByRule['semantic-tokens-only']) {
247
+ report.recommendations.push({
248
+ priority: 'medium',
249
+ title: 'Review Semantic Token Guide',
250
+ description: 'Read .ai/yggdrasil/semantic-token-intent.md for token selection guidance'
251
+ });
252
+ }
253
+
254
+ return report;
255
+ }
256
+
257
+ /**
258
+ * Format audit report
259
+ */
260
+ function formatAuditReport(report, format = 'cli') {
261
+ if (format === 'json') {
262
+ return JSON.stringify(report, null, 2);
263
+ }
264
+
265
+ if (format === 'markdown') {
266
+ let md = '# Yggdrasil Design System Audit Report\n\n';
267
+ md += '## Summary\n\n';
268
+ md += `- **Total Files Scanned:** ${report.summary.totalFiles}\n`;
269
+ md += `- **Files with Violations:** ${report.summary.filesWithViolations}\n`;
270
+ md += `- **Total Violations:** ${report.summary.totalViolations}\n`;
271
+ md += `- **Errors:** ${report.summary.errors}\n`;
272
+ md += `- **Warnings:** ${report.summary.warnings}\n`;
273
+ md += `- **Auto-Fixable:** ${report.summary.autoFixable}\n\n`;
274
+
275
+ if (report.recommendations.length > 0) {
276
+ md += '## Recommendations\n\n';
277
+ report.recommendations.forEach(rec => {
278
+ const icon = rec.priority === 'high' ? '🔴' : '🟡';
279
+ md += `${icon} **${rec.title}**\n`;
280
+ md += ` ${rec.description}\n\n`;
281
+ });
282
+ }
283
+
284
+ md += '## Violations\n\n';
285
+ report.violations.forEach(v => {
286
+ const icon = v.severity === 'error' ? '❌' : '⚠️';
287
+ md += `${icon} **${v.ruleName}** in \`${v.file}:${v.line}\`\n`;
288
+ md += ` ${v.message}\n`;
289
+ md += ` 💡 ${v.suggestion}\n`;
290
+ if (v.autoFixable) {
291
+ md += ` ✨ Auto-fixable\n`;
292
+ }
293
+ md += '\n';
294
+ });
295
+
296
+ return md;
297
+ }
298
+
299
+ // CLI format
300
+ let output = '\n';
301
+ output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
302
+ output += '📊 Yggdrasil Design System Audit Report\n';
303
+ output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
304
+
305
+ output += '📈 Summary:\n';
306
+ output += ` Total Files Scanned: ${report.summary.totalFiles}\n`;
307
+ output += ` Files with Violations: ${report.summary.filesWithViolations}\n`;
308
+ output += ` Total Violations: ${report.summary.totalViolations}\n`;
309
+ output += ` ❌ Errors: ${report.summary.errors}\n`;
310
+ output += ` ⚠️ Warnings: ${report.summary.warnings}\n`;
311
+ output += ` ✨ Auto-Fixable: ${report.summary.autoFixable}\n\n`;
312
+
313
+ if (report.recommendations.length > 0) {
314
+ output += '💡 Recommendations:\n\n';
315
+ report.recommendations.forEach(rec => {
316
+ const icon = rec.priority === 'high' ? '🔴' : '🟡';
317
+ output += ` ${icon} ${rec.title}\n`;
318
+ output += ` ${rec.description}\n\n`;
319
+ });
320
+ }
321
+
322
+ if (report.violations.length > 0) {
323
+ output += '📋 Violations by File:\n\n';
324
+
325
+ const violationsByFile = {};
326
+ report.violations.forEach(v => {
327
+ if (!violationsByFile[v.file]) {
328
+ violationsByFile[v.file] = [];
329
+ }
330
+ violationsByFile[v.file].push(v);
331
+ });
332
+
333
+ Object.entries(violationsByFile).forEach(([file, violations]) => {
334
+ output += `📄 ${file}\n`;
335
+
336
+ violations.forEach(v => {
337
+ const icon = v.severity === 'error' ? '❌' : '⚠️';
338
+ const fixIcon = v.autoFixable ? ' ✨' : '';
339
+ output += ` ${icon}${fixIcon} ${v.ruleName} (line ${v.line})\n`;
340
+ output += ` ${v.message}\n`;
341
+ output += ` 💡 ${v.suggestion}\n`;
342
+
343
+ if (v.autoFixable) {
344
+ output += ` ✨ ${v.autoFixExplanation}\n`;
345
+ } else if (v.manualFixSuggestions) {
346
+ output += ` 📝 Manual fix suggestions:\n`;
347
+ v.manualFixSuggestions.forEach(suggestion => {
348
+ output += ` • ${suggestion}\n`;
349
+ });
350
+ }
351
+ output += '\n';
352
+ });
353
+ });
354
+ }
355
+
356
+ output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
357
+
358
+ return output;
359
+ }
360
+
361
+ /**
362
+ * Apply autofixes to files
363
+ */
364
+ function applyAutofixes(results) {
365
+ const fixed = {};
366
+
367
+ Object.entries(results).forEach(([filePath, fileResults]) => {
368
+ let content = readFileSync(filePath, 'utf8');
369
+ let modified = false;
370
+
371
+ Object.entries(fileResults).forEach(([ruleId, result]) => {
372
+ const autofix = AUTOFIXES[ruleId];
373
+
374
+ if (autofix?.canAutoFix) {
375
+ result.violations.forEach(violation => {
376
+ const newContent = autofix.fix(content, violation);
377
+ if (newContent !== content) {
378
+ content = newContent;
379
+ modified = true;
380
+
381
+ if (!fixed[filePath]) {
382
+ fixed[filePath] = [];
383
+ }
384
+ fixed[filePath].push({
385
+ rule: result.rule,
386
+ line: violation.line,
387
+ message: violation.message
388
+ });
389
+ }
390
+ });
391
+ }
392
+ });
393
+
394
+ if (modified) {
395
+ writeFileSync(filePath, content, 'utf8');
396
+ }
397
+ });
398
+
399
+ return fixed;
400
+ }
401
+
402
+ /**
403
+ * Main audit command
404
+ */
405
+ export async function auditCommand(options = {}) {
406
+ const fix = options.fix || false;
407
+ const format = options.format || 'cli';
408
+
409
+ console.log(`
410
+ 🌳 Yggdrasil Design System Audit
411
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
412
+
413
+ 🔍 Running comprehensive analysis...
414
+ `);
415
+
416
+ // Run validation
417
+ const results = await validateCommand({ ...options, format: 'json' });
418
+
419
+ // Generate audit report
420
+ const report = generateAuditReport(results);
421
+
422
+ // Apply fixes if requested
423
+ if (fix && report.summary.autoFixable > 0) {
424
+ console.log('✨ Applying automatic fixes...\n');
425
+ const fixed = applyAutofixes(results);
426
+
427
+ console.log(`✅ Fixed ${Object.keys(fixed).length} files:\n`);
428
+ Object.entries(fixed).forEach(([file, fixes]) => {
429
+ console.log(` 📄 ${file}`);
430
+ fixes.forEach(f => {
431
+ console.log(` ✓ ${f.rule} (line ${f.line})`);
432
+ });
433
+ console.log('');
434
+ });
435
+
436
+ // Re-run validation to show remaining violations
437
+ console.log('🔍 Re-validating after fixes...\n');
438
+ const newResults = await validateCommand({ ...options, format: 'json' });
439
+ const newReport = generateAuditReport(newResults);
440
+
441
+ console.log(formatAuditReport(newReport, format));
442
+ } else {
443
+ console.log(formatAuditReport(report, format));
444
+ }
445
+
446
+ return report;
447
+ }