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