@trentapps/manager-protocol 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +29 -1
  2. package/dist/analyzers/CSSAnalyzer.d.ts +188 -8
  3. package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -1
  4. package/dist/analyzers/CSSAnalyzer.js +794 -192
  5. package/dist/analyzers/CSSAnalyzer.js.map +1 -1
  6. package/dist/cli.js +1 -1
  7. package/dist/config/dashboard.d.ts +55 -0
  8. package/dist/config/dashboard.d.ts.map +1 -0
  9. package/dist/config/dashboard.js +103 -0
  10. package/dist/config/dashboard.js.map +1 -0
  11. package/dist/config/index.d.ts +7 -0
  12. package/dist/config/index.d.ts.map +1 -0
  13. package/dist/config/index.js +7 -0
  14. package/dist/config/index.js.map +1 -0
  15. package/dist/dashboard/httpDashboard.d.ts +100 -0
  16. package/dist/dashboard/httpDashboard.d.ts.map +1 -0
  17. package/dist/dashboard/httpDashboard.js +1276 -0
  18. package/dist/dashboard/httpDashboard.js.map +1 -0
  19. package/dist/dashboard/index.d.ts +6 -0
  20. package/dist/dashboard/index.d.ts.map +1 -0
  21. package/dist/dashboard/index.js +7 -0
  22. package/dist/dashboard/index.js.map +1 -0
  23. package/dist/engine/AuditLogger.d.ts +370 -2
  24. package/dist/engine/AuditLogger.d.ts.map +1 -1
  25. package/dist/engine/AuditLogger.js +1067 -24
  26. package/dist/engine/AuditLogger.js.map +1 -1
  27. package/dist/engine/GitHubApprovalManager.d.ts +13 -0
  28. package/dist/engine/GitHubApprovalManager.d.ts.map +1 -1
  29. package/dist/engine/GitHubApprovalManager.js +72 -46
  30. package/dist/engine/GitHubApprovalManager.js.map +1 -1
  31. package/dist/engine/GitHubClient.d.ts +183 -0
  32. package/dist/engine/GitHubClient.d.ts.map +1 -0
  33. package/dist/engine/GitHubClient.js +411 -0
  34. package/dist/engine/GitHubClient.js.map +1 -0
  35. package/dist/engine/RateLimiter.d.ts +5 -3
  36. package/dist/engine/RateLimiter.d.ts.map +1 -1
  37. package/dist/engine/RateLimiter.js +53 -70
  38. package/dist/engine/RateLimiter.js.map +1 -1
  39. package/dist/engine/RuleDependencyAnalyzer.d.ts +73 -0
  40. package/dist/engine/RuleDependencyAnalyzer.d.ts.map +1 -0
  41. package/dist/engine/RuleDependencyAnalyzer.js +475 -0
  42. package/dist/engine/RuleDependencyAnalyzer.js.map +1 -0
  43. package/dist/engine/RulesEngine.d.ts +102 -3
  44. package/dist/engine/RulesEngine.d.ts.map +1 -1
  45. package/dist/engine/RulesEngine.js +326 -21
  46. package/dist/engine/RulesEngine.js.map +1 -1
  47. package/dist/engine/TaskManager.d.ts +11 -10
  48. package/dist/engine/TaskManager.d.ts.map +1 -1
  49. package/dist/engine/TaskManager.js +180 -195
  50. package/dist/engine/TaskManager.js.map +1 -1
  51. package/dist/engine/index.d.ts +3 -0
  52. package/dist/engine/index.d.ts.map +1 -1
  53. package/dist/engine/index.js +5 -0
  54. package/dist/engine/index.js.map +1 -1
  55. package/dist/rules/azure.d.ts.map +1 -1
  56. package/dist/rules/azure.js +12 -14
  57. package/dist/rules/azure.js.map +1 -1
  58. package/dist/rules/compliance.d.ts.map +1 -1
  59. package/dist/rules/compliance.js +23 -41
  60. package/dist/rules/compliance.js.map +1 -1
  61. package/dist/rules/condition-optimizer.d.ts +151 -0
  62. package/dist/rules/condition-optimizer.d.ts.map +1 -0
  63. package/dist/rules/condition-optimizer.js +479 -0
  64. package/dist/rules/condition-optimizer.js.map +1 -0
  65. package/dist/rules/css.d.ts.map +1 -1
  66. package/dist/rules/css.js +538 -0
  67. package/dist/rules/css.js.map +1 -1
  68. package/dist/rules/field-standards.d.ts +1172 -0
  69. package/dist/rules/field-standards.d.ts.map +1 -0
  70. package/dist/rules/field-standards.js +908 -0
  71. package/dist/rules/field-standards.js.map +1 -0
  72. package/dist/rules/flask.d.ts.map +1 -1
  73. package/dist/rules/flask.js +18 -31
  74. package/dist/rules/flask.js.map +1 -1
  75. package/dist/rules/index.d.ts +220 -0
  76. package/dist/rules/index.d.ts.map +1 -1
  77. package/dist/rules/index.js +155 -0
  78. package/dist/rules/index.js.map +1 -1
  79. package/dist/rules/ml-ai.d.ts.map +1 -1
  80. package/dist/rules/ml-ai.js +11 -13
  81. package/dist/rules/ml-ai.js.map +1 -1
  82. package/dist/rules/patterns.d.ts +568 -0
  83. package/dist/rules/patterns.d.ts.map +1 -0
  84. package/dist/rules/patterns.js +1359 -0
  85. package/dist/rules/patterns.js.map +1 -0
  86. package/dist/rules/security.d.ts.map +1 -1
  87. package/dist/rules/security.js +580 -19
  88. package/dist/rules/security.js.map +1 -1
  89. package/dist/rules/shared-patterns.d.ts +268 -0
  90. package/dist/rules/shared-patterns.d.ts.map +1 -0
  91. package/dist/rules/shared-patterns.js +556 -0
  92. package/dist/rules/shared-patterns.js.map +1 -0
  93. package/dist/rules/storage.d.ts +8 -2
  94. package/dist/rules/storage.d.ts.map +1 -1
  95. package/dist/rules/storage.js +541 -3
  96. package/dist/rules/storage.js.map +1 -1
  97. package/dist/rules/stripe.d.ts.map +1 -1
  98. package/dist/rules/stripe.js +19 -26
  99. package/dist/rules/stripe.js.map +1 -1
  100. package/dist/rules/websocket.d.ts.map +1 -1
  101. package/dist/rules/websocket.js +32 -40
  102. package/dist/rules/websocket.js.map +1 -1
  103. package/dist/server.d.ts.map +1 -1
  104. package/dist/server.js +96 -17
  105. package/dist/server.js.map +1 -1
  106. package/dist/supervisor/AgentSupervisor.d.ts +52 -0
  107. package/dist/supervisor/AgentSupervisor.d.ts.map +1 -1
  108. package/dist/supervisor/AgentSupervisor.js +120 -1
  109. package/dist/supervisor/AgentSupervisor.js.map +1 -1
  110. package/dist/supervisor/ManagedServerRegistry.d.ts +139 -2
  111. package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -1
  112. package/dist/supervisor/ManagedServerRegistry.js +590 -6
  113. package/dist/supervisor/ManagedServerRegistry.js.map +1 -1
  114. package/dist/supervisor/ProjectTracker.d.ts +24 -2
  115. package/dist/supervisor/ProjectTracker.d.ts.map +1 -1
  116. package/dist/supervisor/ProjectTracker.js +151 -59
  117. package/dist/supervisor/ProjectTracker.js.map +1 -1
  118. package/dist/testing/index.d.ts +11 -0
  119. package/dist/testing/index.d.ts.map +1 -0
  120. package/dist/testing/index.js +12 -0
  121. package/dist/testing/index.js.map +1 -0
  122. package/dist/testing/rule-tester.d.ts +217 -0
  123. package/dist/testing/rule-tester.d.ts.map +1 -0
  124. package/dist/testing/rule-tester.examples.d.ts +57 -0
  125. package/dist/testing/rule-tester.examples.d.ts.map +1 -0
  126. package/dist/testing/rule-tester.examples.js +375 -0
  127. package/dist/testing/rule-tester.examples.js.map +1 -0
  128. package/dist/testing/rule-tester.js +381 -0
  129. package/dist/testing/rule-tester.js.map +1 -0
  130. package/dist/testing/rule-validator.d.ts +141 -0
  131. package/dist/testing/rule-validator.d.ts.map +1 -0
  132. package/dist/testing/rule-validator.js +640 -0
  133. package/dist/testing/rule-validator.js.map +1 -0
  134. package/dist/types/index.d.ts +265 -4
  135. package/dist/types/index.d.ts.map +1 -1
  136. package/dist/types/index.js +57 -2
  137. package/dist/types/index.js.map +1 -1
  138. package/dist/utils/index.d.ts +2 -0
  139. package/dist/utils/index.d.ts.map +1 -1
  140. package/dist/utils/index.js +2 -0
  141. package/dist/utils/index.js.map +1 -1
  142. package/dist/utils/rate-limiting.d.ts +268 -0
  143. package/dist/utils/rate-limiting.d.ts.map +1 -0
  144. package/dist/utils/rate-limiting.js +403 -0
  145. package/dist/utils/rate-limiting.js.map +1 -0
  146. package/dist/utils/shared.d.ts +306 -0
  147. package/dist/utils/shared.d.ts.map +1 -0
  148. package/dist/utils/shared.js +464 -0
  149. package/dist/utils/shared.js.map +1 -0
  150. package/package.json +2 -1
@@ -2,23 +2,65 @@
2
2
  * Enterprise Agent Supervisor - CSS Analyzer
3
3
  *
4
4
  * Analyzes CSS rules for optimization, deduplication, and best practices.
5
+ *
6
+ * Task #44: Extensible pattern catalog system
7
+ * Task #51: Optimized similarity calculation with hashing/indexing
8
+ * Task #52: Comprehensive logging and metrics tracking
5
9
  */
6
10
  import { suggestToken, getRecommendedTokens } from '../design-system/index.js';
7
- /**
8
- * Common CSS property patterns that should be variables
9
- */
10
- const VARIABLE_CANDIDATES = [
11
- { pattern: /^#[0-9a-fA-F]{3,8}$/, type: 'color', varPrefix: '--color-' },
12
- { pattern: /^rgb\(|^rgba\(|^hsl\(|^hsla\(/, type: 'color', varPrefix: '--color-' },
13
- { pattern: /^\d+px$/, type: 'spacing', varPrefix: '--spacing-', minValue: 8 },
14
- { pattern: /^\d+rem$/, type: 'spacing', varPrefix: '--spacing-' },
15
- { pattern: /^(\d+(\.\d+)?)(px|rem|em)$/, type: 'font-size', varPrefix: '--font-size-' },
16
- { pattern: /^\d{3}$/, type: 'font-weight', varPrefix: '--font-weight-' },
17
- { pattern: /^(\d+(\.\d+)?)(s|ms)$/, type: 'duration', varPrefix: '--duration-' },
18
- { pattern: /^cubic-bezier\(/, type: 'easing', varPrefix: '--easing-' }
19
- ];
11
+ import { hashString } from '../utils/shared.js';
12
+ // Default no-op logger
13
+ const noopLogger = {
14
+ debug: () => { },
15
+ info: () => { },
16
+ warn: () => { },
17
+ error: () => { }
18
+ };
19
+ // Console logger for debugging
20
+ export const consoleLogger = {
21
+ debug: (msg, data) => console.debug(`[CSSAnalyzer:DEBUG] ${msg}`, data || ''),
22
+ info: (msg, data) => console.info(`[CSSAnalyzer:INFO] ${msg}`, data || ''),
23
+ warn: (msg, data) => console.warn(`[CSSAnalyzer:WARN] ${msg}`, data || ''),
24
+ error: (msg, data) => console.error(`[CSSAnalyzer:ERROR] ${msg}`, data || '')
25
+ };
26
+ // Default pattern catalog
27
+ const DEFAULT_PATTERN_CATALOG = {
28
+ variablePatterns: [
29
+ { id: 'color-hex', pattern: /^#[0-9a-fA-F]{3,8}$/, type: 'color', varPrefix: '--color-', description: 'Hex color values', enabled: true },
30
+ { id: 'color-rgb', pattern: /^rgb\(|^rgba\(/, type: 'color', varPrefix: '--color-', description: 'RGB/RGBA color values', enabled: true },
31
+ { id: 'color-hsl', pattern: /^hsl\(|^hsla\(/, type: 'color', varPrefix: '--color-', description: 'HSL/HSLA color values', enabled: true },
32
+ { id: 'spacing-px', pattern: /^\d+px$/, type: 'spacing', varPrefix: '--spacing-', minValue: 8, description: 'Pixel spacing values', enabled: true },
33
+ { id: 'spacing-rem', pattern: /^\d+rem$/, type: 'spacing', varPrefix: '--spacing-', description: 'Rem spacing values', enabled: true },
34
+ { id: 'font-size', pattern: /^(\d+(\.\d+)?)(px|rem|em)$/, type: 'font-size', varPrefix: '--font-size-', description: 'Font size values', enabled: true },
35
+ { id: 'font-weight', pattern: /^\d{3}$/, type: 'font-weight', varPrefix: '--font-weight-', description: 'Font weight values', enabled: true },
36
+ { id: 'duration', pattern: /^(\d+(\.\d+)?)(s|ms)$/, type: 'duration', varPrefix: '--duration-', description: 'Animation/transition durations', enabled: true },
37
+ { id: 'easing', pattern: /^cubic-bezier\(/, type: 'easing', varPrefix: '--easing-', description: 'Cubic bezier easing functions', enabled: true }
38
+ ],
39
+ globalPatterns: [
40
+ { id: 'typography', properties: ['font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing'], minMatches: 2, description: 'Typography styles', enabled: true },
41
+ { id: 'theme-colors', properties: ['color', 'background-color'], minMatches: 1, description: 'Theme color properties', enabled: true },
42
+ { id: 'design-tokens', properties: ['border-radius', 'box-shadow', 'transition', 'animation'], minMatches: 2, description: 'Design token properties', enabled: true }
43
+ ],
44
+ reusablePatterns: [
45
+ { id: 'button', name: 'Button', requiredProperties: ['padding', 'border-radius', 'background', 'cursor'], optionalProperties: ['background-color', 'border'], description: 'Button-like component', enabled: true },
46
+ { id: 'card', name: 'Card', requiredProperties: ['box-shadow', 'border-radius', 'padding'], description: 'Card component', enabled: true },
47
+ { id: 'input', name: 'Input', requiredProperties: ['border', 'padding'], optionalProperties: ['border-radius', 'outline'], description: 'Form input', enabled: true }
48
+ ],
49
+ utilityMappings: [
50
+ { property: 'display', valueMapper: (v) => v === 'flex' ? 'flex' : v === 'grid' ? 'grid' : v === 'none' ? 'hidden' : null, enabled: true },
51
+ { property: 'flex-direction', valueMapper: (v) => v === 'column' ? 'flex-col' : v === 'row' ? 'flex-row' : null, enabled: true },
52
+ { property: 'justify-content', valueMapper: (v) => `justify-${v.replace('flex-', '').replace('space-', '')}`, enabled: true },
53
+ { property: 'align-items', valueMapper: (v) => `items-${v.replace('flex-', '')}`, enabled: true },
54
+ { property: 'text-align', valueMapper: (v) => `text-${v}`, enabled: true },
55
+ { property: 'font-weight', valueMapper: () => null, enabled: true }, // Handled separately with getFontWeightName
56
+ { property: 'position', valueMapper: (v) => v, enabled: true },
57
+ { property: 'overflow', valueMapper: (v) => `overflow-${v}`, enabled: true },
58
+ { property: 'cursor', valueMapper: (v) => `cursor-${v}`, enabled: true }
59
+ ]
60
+ };
20
61
  /**
21
62
  * Properties that commonly indicate global/reusable styles
63
+ * Legacy constant for backward compatibility
22
64
  */
23
65
  const GLOBAL_PROPERTY_INDICATORS = [
24
66
  'font-family',
@@ -31,77 +73,428 @@ const GLOBAL_PROPERTY_INDICATORS = [
31
73
  ];
32
74
  export class CSSAnalyzer {
33
75
  knownUtilityClasses = new Set();
34
- constructor() {
76
+ // Task #44: Pattern catalog (extensible)
77
+ patternCatalog;
78
+ // Task #51: Optimized indexing
79
+ // ruleIndex and lastIndexedRules used internally by getOrBuildIndex for caching
80
+ _ruleIndex = null;
81
+ _lastIndexedRules = null;
82
+ similarityThreshold;
83
+ enableCaching;
84
+ // Task #52: Logging and metrics
85
+ logger;
86
+ enableMetrics;
87
+ cumulativeMetrics;
88
+ constructor(options = {}) {
89
+ this.logger = options.logger || noopLogger;
90
+ this.enableMetrics = options.enableMetrics ?? false;
91
+ this.enableCaching = options.enableCaching ?? true;
92
+ this.similarityThreshold = options.similarityThreshold ?? 0.7;
93
+ // Task #44: Merge custom patterns with defaults
94
+ this.patternCatalog = this.mergePatternCatalog(options.patternCatalog);
95
+ // Task #52: Initialize cumulative metrics
96
+ this.cumulativeMetrics = {
97
+ totalAnalyses: 0,
98
+ totalRulesAnalyzed: 0,
99
+ totalDuplicatesFound: 0,
100
+ totalSuggestionsMade: 0,
101
+ totalCacheHits: 0,
102
+ totalCacheMisses: 0,
103
+ totalTimeMs: 0
104
+ };
35
105
  this.initializeKnownPatterns();
106
+ this.logger.info('CSSAnalyzer initialized', {
107
+ enableMetrics: this.enableMetrics,
108
+ enableCaching: this.enableCaching,
109
+ similarityThreshold: this.similarityThreshold,
110
+ patternCount: {
111
+ variablePatterns: this.patternCatalog.variablePatterns.filter(p => p.enabled).length,
112
+ globalPatterns: this.patternCatalog.globalPatterns.filter(p => p.enabled).length,
113
+ reusablePatterns: this.patternCatalog.reusablePatterns.filter(p => p.enabled).length,
114
+ utilityMappings: this.patternCatalog.utilityMappings.filter(p => p.enabled).length
115
+ }
116
+ });
117
+ }
118
+ // ============================================================================
119
+ // Task #44: PATTERN CATALOG MANAGEMENT
120
+ // ============================================================================
121
+ /**
122
+ * Merge custom patterns with defaults
123
+ */
124
+ mergePatternCatalog(custom) {
125
+ if (!custom)
126
+ return { ...DEFAULT_PATTERN_CATALOG };
127
+ return {
128
+ variablePatterns: [
129
+ ...DEFAULT_PATTERN_CATALOG.variablePatterns,
130
+ ...(custom.variablePatterns || [])
131
+ ],
132
+ globalPatterns: [
133
+ ...DEFAULT_PATTERN_CATALOG.globalPatterns,
134
+ ...(custom.globalPatterns || [])
135
+ ],
136
+ reusablePatterns: [
137
+ ...DEFAULT_PATTERN_CATALOG.reusablePatterns,
138
+ ...(custom.reusablePatterns || [])
139
+ ],
140
+ utilityMappings: [
141
+ ...DEFAULT_PATTERN_CATALOG.utilityMappings,
142
+ ...(custom.utilityMappings || [])
143
+ ]
144
+ };
145
+ }
146
+ /**
147
+ * Add a custom variable pattern
148
+ */
149
+ addVariablePattern(pattern) {
150
+ this.patternCatalog.variablePatterns.push(pattern);
151
+ this.logger.debug('Added variable pattern', { id: pattern.id });
152
+ }
153
+ /**
154
+ * Add a custom global pattern
155
+ */
156
+ addGlobalPattern(pattern) {
157
+ this.patternCatalog.globalPatterns.push(pattern);
158
+ this.logger.debug('Added global pattern', { id: pattern.id });
159
+ }
160
+ /**
161
+ * Add a custom reusable pattern
162
+ */
163
+ addReusablePattern(pattern) {
164
+ this.patternCatalog.reusablePatterns.push(pattern);
165
+ this.logger.debug('Added reusable pattern', { id: pattern.id });
166
+ }
167
+ /**
168
+ * Add a custom utility mapping
169
+ */
170
+ addUtilityMapping(mapping) {
171
+ this.patternCatalog.utilityMappings.push(mapping);
172
+ this.logger.debug('Added utility mapping', { property: mapping.property });
173
+ }
174
+ /**
175
+ * Enable or disable a pattern by ID
176
+ */
177
+ setPatternEnabled(patternId, enabled) {
178
+ for (const patterns of [
179
+ this.patternCatalog.variablePatterns,
180
+ this.patternCatalog.globalPatterns,
181
+ this.patternCatalog.reusablePatterns
182
+ ]) {
183
+ const pattern = patterns.find(p => p.id === patternId);
184
+ if (pattern) {
185
+ pattern.enabled = enabled;
186
+ this.logger.debug('Pattern enabled state changed', { id: patternId, enabled });
187
+ return true;
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+ /**
193
+ * Get the current pattern catalog (for inspection/debugging)
194
+ */
195
+ getPatternCatalog() {
196
+ return { ...this.patternCatalog };
197
+ }
198
+ /**
199
+ * Get cumulative metrics (Task #52)
200
+ */
201
+ getCumulativeMetrics() {
202
+ return { ...this.cumulativeMetrics };
203
+ }
204
+ /**
205
+ * Reset cumulative metrics (Task #52)
206
+ */
207
+ resetCumulativeMetrics() {
208
+ this.cumulativeMetrics = {
209
+ totalAnalyses: 0,
210
+ totalRulesAnalyzed: 0,
211
+ totalDuplicatesFound: 0,
212
+ totalSuggestionsMade: 0,
213
+ totalCacheHits: 0,
214
+ totalCacheMisses: 0,
215
+ totalTimeMs: 0
216
+ };
217
+ this.logger.debug('Cumulative metrics reset');
36
218
  }
37
219
  /**
38
220
  * Main analysis entry point
39
221
  */
40
222
  analyze(context) {
41
- const ruleId = this.generateRuleId(context.newRule);
42
- const suggestions = [];
43
- const duplicates = [];
44
- const removableCandidates = [];
45
- let riskScore = 0;
46
- // 1. Check for duplicates/similar rules
47
- const duplicateCheck = this.findDuplicates(context.newRule, context.existingRules || []);
48
- duplicates.push(...duplicateCheck.duplicates);
49
- suggestions.push(...duplicateCheck.suggestions);
50
- riskScore += duplicateCheck.duplicates.length * 10;
51
- // 2. Check if should be externalized
52
- const externalizeCheck = this.checkExternalization(context);
53
- suggestions.push(...externalizeCheck.suggestions);
54
- if (externalizeCheck.shouldExternalize)
55
- riskScore += 15;
56
- // 3. Check if should be global
57
- const globalCheck = this.checkGlobalCandidate(context);
58
- suggestions.push(...globalCheck.suggestions);
59
- if (globalCheck.shouldMakeGlobal)
60
- riskScore += 10;
61
- // 4. Check for variable candidates
62
- const variableCheck = this.checkVariableCandidates(context.newRule);
63
- suggestions.push(...variableCheck.suggestions);
64
- riskScore += variableCheck.suggestions.length * 5;
65
- // 5. Check for utility class opportunities
66
- if (context.hasStyleSystem) {
67
- const utilityCheck = this.checkUtilityOpportunities(context);
68
- suggestions.push(...utilityCheck.suggestions);
69
- }
70
- // 6. Check specificity issues
71
- const specificityCheck = this.checkSpecificity(context.newRule);
72
- suggestions.push(...specificityCheck.suggestions);
73
- riskScore += specificityCheck.riskContribution;
74
- // 7. Check naming conventions
75
- const namingCheck = this.checkNamingConvention(context.newRule, context.framework);
76
- suggestions.push(...namingCheck.suggestions);
77
- // 8. Find removable candidates
78
- if (context.existingRules) {
79
- removableCandidates.push(...this.findRemovableCandidates(context.newRule, context.existingRules));
80
- }
81
- // Generate summary
82
- const summary = this.generateSummary(context.newRule, suggestions, duplicates, externalizeCheck.shouldExternalize, globalCheck.shouldMakeGlobal);
223
+ // Task #52: Start timing
224
+ const totalStart = performance.now();
225
+ const timingMs = {
226
+ total: 0,
227
+ duplicateCheck: 0,
228
+ externalizeCheck: 0,
229
+ globalCheck: 0,
230
+ variableCheck: 0,
231
+ utilityCheck: 0,
232
+ specificityCheck: 0,
233
+ namingCheck: 0,
234
+ removableCheck: 0
235
+ };
236
+ let cacheHits = 0;
237
+ let cacheMisses = 0;
238
+ this.logger.debug('Starting CSS analysis', {
239
+ selector: context.newRule.selector,
240
+ source: context.newRule.source,
241
+ propertyCount: Object.keys(context.newRule.properties).length,
242
+ existingRulesCount: context.existingRules?.length || 0
243
+ });
244
+ try {
245
+ // Validate selector first
246
+ if (!this.isValidSelector(context.newRule.selector)) {
247
+ this.logger.warn('Invalid CSS selector', { selector: context.newRule.selector });
248
+ return {
249
+ ruleId: `css-invalid-${this.simpleHash(context.newRule.selector)}`,
250
+ newRule: context.newRule,
251
+ shouldExternalize: false,
252
+ shouldMakeGlobal: false,
253
+ duplicates: [],
254
+ suggestions: [{
255
+ type: 'specificity_warning',
256
+ severity: 'error',
257
+ message: 'Invalid CSS selector',
258
+ details: 'The selector appears to be malformed or contains invalid characters'
259
+ }],
260
+ removableCandidates: [],
261
+ riskScore: 50,
262
+ summary: 'Skipped analysis: invalid CSS selector'
263
+ };
264
+ }
265
+ const ruleId = this.generateRuleId(context.newRule);
266
+ const suggestions = [];
267
+ const duplicates = [];
268
+ const removableCandidates = [];
269
+ let riskScore = 0;
270
+ // Task #51: Build or reuse index for existing rules
271
+ const existingRules = context.existingRules || [];
272
+ if (existingRules.length > 0) {
273
+ const indexResult = this.getOrBuildIndex(existingRules);
274
+ cacheHits += indexResult.cacheHit ? 1 : 0;
275
+ cacheMisses += indexResult.cacheHit ? 0 : 1;
276
+ }
277
+ // 1. Check for duplicates/similar rules (Task #51: optimized)
278
+ let stepStart = performance.now();
279
+ const duplicateCheck = this.findDuplicatesOptimized(context.newRule, existingRules);
280
+ timingMs.duplicateCheck = performance.now() - stepStart;
281
+ duplicates.push(...duplicateCheck.duplicates);
282
+ suggestions.push(...duplicateCheck.suggestions);
283
+ riskScore += duplicateCheck.duplicates.length * 10;
284
+ this.logger.debug('Duplicate check complete', {
285
+ duplicatesFound: duplicateCheck.duplicates.length,
286
+ timeMs: timingMs.duplicateCheck.toFixed(2)
287
+ });
288
+ // 2. Check if should be externalized
289
+ stepStart = performance.now();
290
+ const externalizeCheck = this.checkExternalization(context);
291
+ timingMs.externalizeCheck = performance.now() - stepStart;
292
+ suggestions.push(...externalizeCheck.suggestions);
293
+ if (externalizeCheck.shouldExternalize)
294
+ riskScore += 15;
295
+ // 3. Check if should be global
296
+ stepStart = performance.now();
297
+ const globalCheck = this.checkGlobalCandidate(context);
298
+ timingMs.globalCheck = performance.now() - stepStart;
299
+ suggestions.push(...globalCheck.suggestions);
300
+ if (globalCheck.shouldMakeGlobal)
301
+ riskScore += 10;
302
+ // 4. Check for variable candidates (Task #44: uses pattern catalog)
303
+ stepStart = performance.now();
304
+ const variableCheck = this.checkVariableCandidates(context.newRule);
305
+ timingMs.variableCheck = performance.now() - stepStart;
306
+ suggestions.push(...variableCheck.suggestions);
307
+ riskScore += variableCheck.suggestions.length * 5;
308
+ // 5. Check for utility class opportunities (Task #44: uses pattern catalog)
309
+ stepStart = performance.now();
310
+ if (context.hasStyleSystem) {
311
+ const utilityCheck = this.checkUtilityOpportunities(context);
312
+ suggestions.push(...utilityCheck.suggestions);
313
+ }
314
+ timingMs.utilityCheck = performance.now() - stepStart;
315
+ // 6. Check specificity issues
316
+ stepStart = performance.now();
317
+ const specificityCheck = this.checkSpecificity(context.newRule);
318
+ timingMs.specificityCheck = performance.now() - stepStart;
319
+ suggestions.push(...specificityCheck.suggestions);
320
+ riskScore += specificityCheck.riskContribution;
321
+ // 7. Check naming conventions
322
+ stepStart = performance.now();
323
+ const namingCheck = this.checkNamingConvention(context.newRule, context.framework);
324
+ timingMs.namingCheck = performance.now() - stepStart;
325
+ suggestions.push(...namingCheck.suggestions);
326
+ // 8. Find removable candidates (Task #51: optimized)
327
+ stepStart = performance.now();
328
+ if (existingRules.length > 0) {
329
+ removableCandidates.push(...this.findRemovableCandidatesOptimized(context.newRule, existingRules));
330
+ }
331
+ timingMs.removableCheck = performance.now() - stepStart;
332
+ // Generate summary
333
+ const summary = this.generateSummary(context.newRule, suggestions, duplicates, externalizeCheck.shouldExternalize, globalCheck.shouldMakeGlobal);
334
+ timingMs.total = performance.now() - totalStart;
335
+ // Task #52: Build metrics
336
+ const metrics = {
337
+ rulesAnalyzed: existingRules.length + 1,
338
+ duplicatesFound: duplicates.length,
339
+ suggestionsMade: suggestions.length,
340
+ suggestionsByType: this.countSuggestionsByType(suggestions),
341
+ suggestionsBySeverity: this.countSuggestionsBySeverity(suggestions),
342
+ timingMs,
343
+ cacheHits,
344
+ cacheMisses
345
+ };
346
+ // Task #52: Update cumulative metrics
347
+ this.cumulativeMetrics.totalAnalyses++;
348
+ this.cumulativeMetrics.totalRulesAnalyzed += metrics.rulesAnalyzed;
349
+ this.cumulativeMetrics.totalDuplicatesFound += metrics.duplicatesFound;
350
+ this.cumulativeMetrics.totalSuggestionsMade += metrics.suggestionsMade;
351
+ this.cumulativeMetrics.totalCacheHits += cacheHits;
352
+ this.cumulativeMetrics.totalCacheMisses += cacheMisses;
353
+ this.cumulativeMetrics.totalTimeMs += timingMs.total;
354
+ this.logger.info('CSS analysis complete', {
355
+ ruleId,
356
+ riskScore: Math.min(100, riskScore),
357
+ duplicatesFound: duplicates.length,
358
+ suggestionsCount: suggestions.length,
359
+ totalTimeMs: timingMs.total.toFixed(2)
360
+ });
361
+ const result = {
362
+ ruleId,
363
+ newRule: context.newRule,
364
+ shouldExternalize: externalizeCheck.shouldExternalize,
365
+ shouldMakeGlobal: globalCheck.shouldMakeGlobal,
366
+ duplicates,
367
+ suggestions: this.prioritizeSuggestions(suggestions),
368
+ removableCandidates,
369
+ riskScore: Math.min(100, riskScore),
370
+ summary
371
+ };
372
+ // Task #52: Include metrics if enabled
373
+ if (this.enableMetrics) {
374
+ result.metrics = metrics;
375
+ }
376
+ return result;
377
+ }
378
+ catch (error) {
379
+ this.logger.error('CSS analysis failed', {
380
+ selector: context.newRule.selector,
381
+ error: error instanceof Error ? error.message : String(error)
382
+ });
383
+ // Gracefully handle unexpected errors during analysis
384
+ return {
385
+ ruleId: `css-error-${this.simpleHash(context.newRule.selector)}`,
386
+ newRule: context.newRule,
387
+ shouldExternalize: false,
388
+ shouldMakeGlobal: false,
389
+ duplicates: [],
390
+ suggestions: [{
391
+ type: 'specificity_warning',
392
+ severity: 'error',
393
+ message: 'CSS analysis error',
394
+ details: `Failed to analyze CSS selector: ${error instanceof Error ? error.message : String(error)}`
395
+ }],
396
+ removableCandidates: [],
397
+ riskScore: 50,
398
+ summary: 'Analysis failed due to unexpected error'
399
+ };
400
+ }
401
+ }
402
+ // ============================================================================
403
+ // Task #52: METRICS HELPERS
404
+ // ============================================================================
405
+ countSuggestionsByType(suggestions) {
406
+ const counts = {};
407
+ for (const s of suggestions) {
408
+ counts[s.type] = (counts[s.type] || 0) + 1;
409
+ }
410
+ return counts;
411
+ }
412
+ countSuggestionsBySeverity(suggestions) {
413
+ const counts = {};
414
+ for (const s of suggestions) {
415
+ counts[s.severity] = (counts[s.severity] || 0) + 1;
416
+ }
417
+ return counts;
418
+ }
419
+ // ============================================================================
420
+ // Task #51: OPTIMIZED INDEXING AND DUPLICATE DETECTION
421
+ // ============================================================================
422
+ /**
423
+ * Get or build index for existing rules (Task #51: caching for performance)
424
+ */
425
+ getOrBuildIndex(existingRules) {
426
+ // Check if we can reuse the existing index
427
+ if (this.enableCaching && this._ruleIndex && this._lastIndexedRules === existingRules) {
428
+ this.logger.debug('Index cache hit');
429
+ return { index: this._ruleIndex, cacheHit: true };
430
+ }
431
+ this.logger.debug('Building new rule index', { ruleCount: existingRules.length });
432
+ const startTime = performance.now();
433
+ const index = {
434
+ byPropertyHash: new Map(),
435
+ bySelector: new Map(),
436
+ byPropertyKeys: new Map(),
437
+ signatures: new Map()
438
+ };
439
+ for (const rule of existingRules) {
440
+ const signature = this.computePropertySignature(rule.properties);
441
+ index.signatures.set(rule, signature);
442
+ // Index by property hash (for exact matches)
443
+ const hashRules = index.byPropertyHash.get(signature.hash) || [];
444
+ hashRules.push(rule);
445
+ index.byPropertyHash.set(signature.hash, hashRules);
446
+ // Index by selector
447
+ const selectorRules = index.bySelector.get(rule.selector) || [];
448
+ selectorRules.push(rule);
449
+ index.bySelector.set(rule.selector, selectorRules);
450
+ // Index by sorted property keys (for similarity pre-filtering)
451
+ const keyRules = index.byPropertyKeys.get(signature.sortedKeys) || [];
452
+ keyRules.push(rule);
453
+ index.byPropertyKeys.set(signature.sortedKeys, keyRules);
454
+ }
455
+ if (this.enableCaching) {
456
+ this._ruleIndex = index;
457
+ this._lastIndexedRules = existingRules;
458
+ }
459
+ this.logger.debug('Index built', {
460
+ timeMs: (performance.now() - startTime).toFixed(2),
461
+ uniquePropertyHashes: index.byPropertyHash.size,
462
+ uniqueSelectors: index.bySelector.size,
463
+ uniquePropertyKeyPatterns: index.byPropertyKeys.size
464
+ });
465
+ return { index, cacheHit: false };
466
+ }
467
+ /**
468
+ * Compute a signature for CSS properties for fast comparison (Task #51)
469
+ */
470
+ computePropertySignature(properties) {
471
+ const keys = Object.keys(properties).sort();
472
+ const sortedKeys = keys.join(',');
473
+ const values = keys.map(k => `${k}:${properties[k]}`).join(';');
474
+ const hash = this.simpleHash(values);
83
475
  return {
84
- ruleId,
85
- newRule: context.newRule,
86
- shouldExternalize: externalizeCheck.shouldExternalize,
87
- shouldMakeGlobal: globalCheck.shouldMakeGlobal,
88
- duplicates,
89
- suggestions: this.prioritizeSuggestions(suggestions),
90
- removableCandidates,
91
- riskScore: Math.min(100, riskScore),
92
- summary
476
+ hash,
477
+ propertyCount: keys.length,
478
+ sortedKeys
93
479
  };
94
480
  }
95
481
  /**
96
- * Find duplicate or similar CSS rules
482
+ * Find duplicates with optimized O(1) hash lookup + O(k) similarity check (Task #51)
483
+ * where k is the number of rules with similar property counts/keys
97
484
  */
98
- findDuplicates(newRule, existingRules) {
485
+ findDuplicatesOptimized(newRule, existingRules) {
99
486
  const duplicates = [];
100
487
  const suggestions = [];
101
- for (const existing of existingRules) {
102
- // Exact property match
103
- const similarity = this.calculateSimilarity(newRule.properties, existing.properties);
104
- if (similarity === 1) {
488
+ if (existingRules.length === 0) {
489
+ return { duplicates, suggestions };
490
+ }
491
+ const { index } = this.getOrBuildIndex(existingRules);
492
+ const newSignature = this.computePropertySignature(newRule.properties);
493
+ // Step 1: O(1) lookup for exact property matches by hash
494
+ const exactMatches = index.byPropertyHash.get(newSignature.hash) || [];
495
+ for (const existing of exactMatches) {
496
+ // Verify it's truly an exact match (hash collision protection)
497
+ if (this.calculateSimilarity(newRule.properties, existing.properties) === 1) {
105
498
  duplicates.push(existing);
106
499
  suggestions.push({
107
500
  type: 'use_existing',
@@ -113,30 +506,92 @@ export class CSSAnalyzer {
113
506
  codeExample: `class="${existing.selector.replace('.', '')}"`
114
507
  });
115
508
  }
116
- else if (similarity >= 0.7) {
509
+ }
510
+ // Step 2: Check for same selector with different properties
511
+ const sameSelectors = index.bySelector.get(newRule.selector) || [];
512
+ for (const existing of sameSelectors) {
513
+ const similarity = this.calculateSimilarity(newRule.properties, existing.properties);
514
+ if (similarity < 1 && similarity > 0) {
117
515
  suggestions.push({
118
516
  type: 'consolidate',
119
517
  severity: 'warning',
120
- message: `Similar CSS properties (${Math.round(similarity * 100)}% match) found in "${existing.selector}"`,
121
- details: 'Consider consolidating these rules to reduce CSS bundle size',
518
+ message: `Selector "${newRule.selector}" already exists with different properties`,
519
+ details: 'Multiple rules with same selector can cause specificity conflicts',
122
520
  existingRule: existing,
123
- suggestedAction: 'Extend the existing rule or create a shared base class'
521
+ suggestedAction: 'Merge properties into single rule or use more specific selectors'
124
522
  });
125
523
  }
126
- // Check for same selector different properties
127
- if (existing.selector === newRule.selector && similarity < 1) {
524
+ }
525
+ // Step 3: Similarity search with early termination (Task #51 optimization)
526
+ // Only check rules with similar property counts (within 50% range)
527
+ const newCount = newSignature.propertyCount;
528
+ const minCount = Math.floor(newCount * 0.5);
529
+ const maxCount = Math.ceil(newCount * 1.5);
530
+ // Get candidate rules by property key patterns for pre-filtering
531
+ const checked = new Set(exactMatches);
532
+ for (const existing of existingRules) {
533
+ if (checked.has(existing))
534
+ continue;
535
+ const existingSig = index.signatures.get(existing);
536
+ if (!existingSig)
537
+ continue;
538
+ // Early termination: skip if property count difference is too large
539
+ if (existingSig.propertyCount < minCount || existingSig.propertyCount > maxCount) {
540
+ continue;
541
+ }
542
+ // Calculate similarity only for candidates that pass pre-filter
543
+ const similarity = this.calculateSimilarity(newRule.properties, existing.properties);
544
+ if (similarity >= this.similarityThreshold && similarity < 1) {
128
545
  suggestions.push({
129
546
  type: 'consolidate',
130
547
  severity: 'warning',
131
- message: `Selector "${newRule.selector}" already exists with different properties`,
132
- details: 'Multiple rules with same selector can cause specificity conflicts',
548
+ message: `Similar CSS properties (${Math.round(similarity * 100)}% match) found in "${existing.selector}"`,
549
+ details: 'Consider consolidating these rules to reduce CSS bundle size',
133
550
  existingRule: existing,
134
- suggestedAction: 'Merge properties into single rule or use more specific selectors'
551
+ suggestedAction: 'Extend the existing rule or create a shared base class'
135
552
  });
136
553
  }
137
554
  }
138
555
  return { duplicates, suggestions };
139
556
  }
557
+ /**
558
+ * Find removable candidates with optimized lookup (Task #51)
559
+ */
560
+ findRemovableCandidatesOptimized(newRule, existingRules) {
561
+ const removable = [];
562
+ const { index } = this.getOrBuildIndex(existingRules);
563
+ // Only check rules with the same selector
564
+ const sameSelectors = index.bySelector.get(newRule.selector) || [];
565
+ for (const existing of sameSelectors) {
566
+ if (this.completelyOverrides(newRule, existing)) {
567
+ removable.push(existing);
568
+ }
569
+ }
570
+ // Check for likely unused rules (no optimization needed, fast check)
571
+ for (const existing of existingRules) {
572
+ if (this.isLikelyUnused(existing) && !removable.includes(existing)) {
573
+ removable.push(existing);
574
+ }
575
+ }
576
+ return removable;
577
+ }
578
+ /**
579
+ * Clear the rule index cache (call when rules change)
580
+ */
581
+ clearIndexCache() {
582
+ this._ruleIndex = null;
583
+ this._lastIndexedRules = null;
584
+ this.logger.debug('Index cache cleared');
585
+ }
586
+ /**
587
+ * Find duplicate or similar CSS rules
588
+ * @deprecated Use findDuplicatesOptimized instead (Task #51)
589
+ * @internal Kept for backward compatibility - delegates to optimized version
590
+ */
591
+ findDuplicates(newRule, existingRules) {
592
+ // Delegate to optimized version
593
+ return this.findDuplicatesOptimized(newRule, existingRules);
594
+ }
140
595
  /**
141
596
  * Check if CSS should be externalized
142
597
  */
@@ -183,48 +638,70 @@ export class CSSAnalyzer {
183
638
  return { shouldExternalize, suggestions };
184
639
  }
185
640
  /**
186
- * Check if CSS should be made global
641
+ * Check if CSS should be made global (Task #44: uses pattern catalog)
187
642
  */
188
643
  checkGlobalCandidate(context) {
189
644
  const suggestions = [];
190
645
  let shouldMakeGlobal = false;
191
646
  const properties = Object.keys(context.newRule.properties);
192
- // Check for global property indicators
193
- const globalIndicators = properties.filter(p => GLOBAL_PROPERTY_INDICATORS.includes(p));
194
- if (globalIndicators.length >= 2) {
195
- shouldMakeGlobal = true;
196
- suggestions.push({
197
- type: 'make_global',
198
- severity: 'suggestion',
199
- message: `Rule contains commonly global properties: ${globalIndicators.join(', ')}`,
200
- details: 'These properties often indicate design system tokens that should be global',
201
- suggestedAction: 'Consider adding to global styles or design system'
202
- });
647
+ // Task #44: Check against global patterns from catalog
648
+ for (const pattern of this.patternCatalog.globalPatterns.filter(p => p.enabled)) {
649
+ const matches = properties.filter(p => pattern.properties.includes(p));
650
+ if (matches.length >= pattern.minMatches) {
651
+ shouldMakeGlobal = true;
652
+ suggestions.push({
653
+ type: 'make_global',
654
+ severity: 'suggestion',
655
+ message: `Rule matches global pattern "${pattern.id}": ${matches.join(', ')}`,
656
+ details: pattern.description || 'These properties often indicate design system tokens that should be global',
657
+ suggestedAction: 'Consider adding to global styles or design system'
658
+ });
659
+ }
203
660
  }
204
- // Typography rules should typically be global
205
- if (this.isTypographyRule(context.newRule)) {
206
- shouldMakeGlobal = true;
207
- suggestions.push({
208
- type: 'make_global',
209
- severity: 'suggestion',
210
- message: 'Typography styles should be defined globally for consistency',
211
- details: 'Consistent typography improves design cohesion and accessibility',
212
- suggestedAction: 'Add to typography section in global styles'
213
- });
661
+ // Legacy check for backward compatibility (only if not already matched)
662
+ if (!shouldMakeGlobal) {
663
+ const globalIndicators = properties.filter(p => GLOBAL_PROPERTY_INDICATORS.includes(p));
664
+ if (globalIndicators.length >= 2) {
665
+ shouldMakeGlobal = true;
666
+ suggestions.push({
667
+ type: 'make_global',
668
+ severity: 'suggestion',
669
+ message: `Rule contains commonly global properties: ${globalIndicators.join(', ')}`,
670
+ details: 'These properties often indicate design system tokens that should be global',
671
+ suggestedAction: 'Consider adding to global styles or design system'
672
+ });
673
+ }
214
674
  }
215
- // Button-like patterns should be global
216
- if (this.isButtonPattern(context.newRule)) {
217
- shouldMakeGlobal = true;
218
- suggestions.push({
219
- type: 'make_global',
220
- severity: 'suggestion',
221
- message: 'Button/interactive styles should be global components',
222
- details: 'Consistent interactive elements improve UX and accessibility',
223
- suggestedAction: 'Create a reusable button component/class'
224
- });
675
+ // Task #44: Check reusable patterns from catalog
676
+ for (const pattern of this.patternCatalog.reusablePatterns.filter(p => p.enabled)) {
677
+ if (this.matchesReusablePattern(context.newRule, pattern)) {
678
+ shouldMakeGlobal = true;
679
+ suggestions.push({
680
+ type: 'make_global',
681
+ severity: 'suggestion',
682
+ message: `${pattern.name} styles should be global components`,
683
+ details: pattern.description || 'Consistent interactive elements improve UX and accessibility',
684
+ suggestedAction: `Create a reusable ${pattern.name.toLowerCase()} component/class`
685
+ });
686
+ }
225
687
  }
226
688
  return { shouldMakeGlobal, suggestions };
227
689
  }
690
+ /**
691
+ * Check if a rule matches a reusable pattern from the catalog (Task #44)
692
+ */
693
+ matchesReusablePattern(rule, pattern) {
694
+ const props = Object.keys(rule.properties);
695
+ // All required properties must be present (or start with the property name for variants like background-color)
696
+ const hasAllRequired = pattern.requiredProperties.every(req => props.some(p => p === req || p.startsWith(req)));
697
+ if (!hasAllRequired)
698
+ return false;
699
+ // For button pattern, also check cursor: pointer
700
+ if (pattern.id === 'button' && rule.properties['cursor'] !== 'pointer') {
701
+ return false;
702
+ }
703
+ return true;
704
+ }
228
705
  /**
229
706
  * Check for CSS variable candidates using design system tokens
230
707
  */
@@ -249,7 +726,7 @@ export class CSSAnalyzer {
249
726
  continue;
250
727
  }
251
728
  // Fall back to generic variable suggestions for unmatched values
252
- for (const candidate of VARIABLE_CANDIDATES) {
729
+ for (const candidate of this.patternCatalog.variablePatterns) {
253
730
  if (candidate.pattern.test(value)) {
254
731
  // Skip small values for spacing
255
732
  if (candidate.type === 'spacing' && candidate.minValue) {
@@ -282,26 +759,24 @@ export class CSSAnalyzer {
282
759
  return { suggestions };
283
760
  }
284
761
  /**
285
- * Check for utility class opportunities
762
+ * Check for utility class opportunities (Task #44: uses pattern catalog)
286
763
  */
287
764
  checkUtilityOpportunities(context) {
288
765
  const suggestions = [];
289
766
  const properties = context.newRule.properties;
290
- // Common utility mappings
291
- const utilityMappings = {
292
- 'display': (v) => v === 'flex' ? 'flex' : v === 'grid' ? 'grid' : v === 'none' ? 'hidden' : null,
293
- 'flex-direction': (v) => v === 'column' ? 'flex-col' : v === 'row' ? 'flex-row' : null,
294
- 'justify-content': (v) => `justify-${v.replace('flex-', '').replace('space-', '')}`,
295
- 'align-items': (v) => `items-${v.replace('flex-', '')}`,
296
- 'text-align': (v) => `text-${v}`,
297
- 'font-weight': (v) => `font-${this.getFontWeightName(v)}`,
298
- 'position': (v) => v,
299
- 'overflow': (v) => `overflow-${v}`,
300
- 'cursor': (v) => `cursor-${v}`
301
- };
767
+ // Task #44: Use utility mappings from pattern catalog
302
768
  for (const [property, value] of Object.entries(properties)) {
303
- if (utilityMappings[property]) {
304
- const utilityClass = utilityMappings[property](value);
769
+ // First check catalog mappings
770
+ const catalogMapping = this.patternCatalog.utilityMappings.find(m => m.enabled && m.property === property);
771
+ if (catalogMapping) {
772
+ // Special handling for font-weight (uses getFontWeightName)
773
+ let utilityClass = null;
774
+ if (property === 'font-weight') {
775
+ utilityClass = `font-${this.getFontWeightName(value)}`;
776
+ }
777
+ else {
778
+ utilityClass = catalogMapping.valueMapper(value);
779
+ }
305
780
  if (utilityClass) {
306
781
  suggestions.push({
307
782
  type: 'use_utility',
@@ -364,56 +839,124 @@ export class CSSAnalyzer {
364
839
  */
365
840
  checkNamingConvention(rule, framework) {
366
841
  const suggestions = [];
367
- const selector = rule.selector;
368
- // Check for meaningful names
369
- if (/^\.[a-z]$/.test(selector) || /^\.(div|span|container)\d*$/.test(selector)) {
370
- suggestions.push({
371
- type: 'naming_convention',
372
- severity: 'warning',
373
- message: 'Use descriptive class names',
374
- details: 'Generic names like "div1" or single letters are hard to understand',
375
- suggestedAction: 'Use semantic names that describe purpose (e.g., .card-header)'
376
- });
377
- }
378
- // BEM recommendation for complex selectors
379
- if (selector.includes(' ') && !selector.includes('__') && !selector.includes('--')) {
380
- suggestions.push({
381
- type: 'naming_convention',
382
- severity: 'info',
383
- message: 'Consider using BEM naming convention',
384
- details: 'BEM (Block__Element--Modifier) creates clear relationships',
385
- suggestedAction: 'Rename using BEM: .block__element--modifier',
386
- codeExample: this.suggestBEMName(selector)
387
- });
842
+ try {
843
+ const selector = rule.selector;
844
+ // Validate selector before processing
845
+ if (!selector || typeof selector !== 'string' || selector.length === 0) {
846
+ return { suggestions };
847
+ }
848
+ // Check for meaningful names
849
+ if (/^\.[a-z]$/.test(selector) || /^\.(div|span|container)\d*$/.test(selector)) {
850
+ suggestions.push({
851
+ type: 'naming_convention',
852
+ severity: 'warning',
853
+ message: 'Use descriptive class names',
854
+ details: 'Generic names like "div1" or single letters are hard to understand',
855
+ suggestedAction: 'Use semantic names that describe purpose (e.g., .card-header)'
856
+ });
857
+ }
858
+ // BEM recommendation for complex selectors
859
+ if (selector.includes(' ') && !selector.includes('__') && !selector.includes('--')) {
860
+ suggestions.push({
861
+ type: 'naming_convention',
862
+ severity: 'info',
863
+ message: 'Consider using BEM naming convention',
864
+ details: 'BEM (Block__Element--Modifier) creates clear relationships',
865
+ suggestedAction: 'Rename using BEM: .block__element--modifier',
866
+ codeExample: this.suggestBEMName(selector)
867
+ });
868
+ }
869
+ // Framework-specific conventions
870
+ if (framework === 'react' && selector.includes('_')) {
871
+ suggestions.push({
872
+ type: 'naming_convention',
873
+ severity: 'info',
874
+ message: 'React typically uses camelCase for CSS Modules',
875
+ details: 'CSS Modules work better with camelCase class names',
876
+ suggestedAction: 'Use camelCase: .cardHeader instead of .card_header'
877
+ });
878
+ }
388
879
  }
389
- // Framework-specific conventions
390
- if (framework === 'react' && selector.includes('_')) {
391
- suggestions.push({
392
- type: 'naming_convention',
393
- severity: 'info',
394
- message: 'React typically uses camelCase for CSS Modules',
395
- details: 'CSS Modules work better with camelCase class names',
396
- suggestedAction: 'Use camelCase: .cardHeader instead of .card_header'
397
- });
880
+ catch (error) {
881
+ // Silently skip naming convention checks if they fail
398
882
  }
399
883
  return { suggestions };
400
884
  }
401
885
  /**
402
886
  * Find CSS rules that can be safely removed
887
+ * @deprecated Use findRemovableCandidatesOptimized instead (Task #51)
888
+ * @internal Kept for backward compatibility - delegates to optimized version
403
889
  */
890
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
404
891
  findRemovableCandidates(newRule, existingRules) {
405
- const removable = [];
406
- for (const existing of existingRules) {
407
- // If new rule completely overrides existing
408
- if (this.completelyOverrides(newRule, existing)) {
409
- removable.push(existing);
892
+ // Delegate to optimized version
893
+ return this.findRemovableCandidatesOptimized(newRule, existingRules);
894
+ }
895
+ // ============================================================================
896
+ // VALIDATION METHODS
897
+ // ============================================================================
898
+ /**
899
+ * Validate that a selector is well-formed and safe to process
900
+ */
901
+ isValidSelector(selector) {
902
+ try {
903
+ // Check for empty or whitespace-only selectors
904
+ if (!selector || typeof selector !== 'string' || selector.trim().length === 0) {
905
+ return false;
410
906
  }
411
- // Dead selectors (would need DOM analysis in real implementation)
412
- if (this.isLikelyUnused(existing)) {
413
- removable.push(existing);
907
+ // Limit selector length to prevent processing extremely large selectors
908
+ if (selector.length > 2000) {
909
+ return false;
910
+ }
911
+ // Check for unclosed brackets/quotes
912
+ const openBrackets = (selector.match(/\[/g) || []).length;
913
+ const closeBrackets = (selector.match(/\]/g) || []).length;
914
+ if (openBrackets !== closeBrackets) {
915
+ return false;
916
+ }
917
+ // Check for unclosed parentheses
918
+ const openParens = (selector.match(/\(/g) || []).length;
919
+ const closeParens = (selector.match(/\)/g) || []).length;
920
+ if (openParens !== closeParens) {
921
+ return false;
414
922
  }
923
+ // Check for unclosed quotes (both single and double)
924
+ const singleQuotes = (selector.match(/'/g) || []).length;
925
+ const doubleQuotes = (selector.match(/"/g) || []).length;
926
+ if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0) {
927
+ return false;
928
+ }
929
+ return true;
930
+ }
931
+ catch {
932
+ // If validation itself throws, consider selector invalid
933
+ return false;
934
+ }
935
+ }
936
+ /**
937
+ * Normalize a selector for safe analysis by removing problematic content
938
+ */
939
+ normalizeSelectorForAnalysis(selector) {
940
+ try {
941
+ if (!selector || typeof selector !== 'string') {
942
+ return '';
943
+ }
944
+ // Remove pseudo-elements and pseudo-classes that contain content
945
+ let normalized = selector
946
+ .replace(/:not\([^)]*\)/gi, '') // Remove :not() pseudo-class with content
947
+ .replace(/::?before/gi, '')
948
+ .replace(/::?after/gi, '')
949
+ .replace(/::?first-line/gi, '')
950
+ .replace(/::?first-letter/gi, '');
951
+ // Limit length to prevent excessive processing
952
+ if (normalized.length > 2000) {
953
+ normalized = normalized.substring(0, 2000);
954
+ }
955
+ return normalized;
956
+ }
957
+ catch {
958
+ return '';
415
959
  }
416
- return removable;
417
960
  }
418
961
  // ============================================================================
419
962
  // HELPER METHODS
@@ -423,13 +966,7 @@ export class CSSAnalyzer {
423
966
  return `css-${hash}`;
424
967
  }
425
968
  simpleHash(str) {
426
- let hash = 0;
427
- for (let i = 0; i < str.length; i++) {
428
- const char = str.charCodeAt(i);
429
- hash = ((hash << 5) - hash) + char;
430
- hash = hash & hash;
431
- }
432
- return Math.abs(hash).toString(16).substring(0, 8);
969
+ return hashString(str);
433
970
  }
434
971
  calculateSimilarity(props1, props2) {
435
972
  const keys1 = Object.keys(props1);
@@ -446,13 +983,34 @@ export class CSSAnalyzer {
446
983
  return matches / allKeys.size;
447
984
  }
448
985
  calculateSpecificity(selector) {
449
- const ids = (selector.match(/#/g) || []).length;
450
- const classes = (selector.match(/\./g) || []).length;
451
- const elements = (selector.match(/^[a-z]+|[\s>+~][a-z]+/gi) || []).length;
452
- const depth = selector.split(/[\s>+~]/).length;
453
- return { ids, classes, elements, depth };
986
+ try {
987
+ // Sanitize and normalize selector
988
+ const normalizedSelector = this.normalizeSelectorForAnalysis(selector);
989
+ if (!normalizedSelector) {
990
+ return { ids: 0, classes: 0, elements: 0, depth: 0 };
991
+ }
992
+ const ids = (normalizedSelector.match(/#/g) || []).length;
993
+ const classes = (normalizedSelector.match(/\./g) || []).length;
994
+ const elements = (normalizedSelector.match(/^[a-z]+|[\s>+~][a-z]+/gi) || []).length;
995
+ const depth = Math.min(normalizedSelector.split(/[\s>+~]/).length, 100); // Cap depth at 100
996
+ return { ids, classes, elements, depth };
997
+ }
998
+ catch {
999
+ // Return safe defaults if parsing fails
1000
+ return { ids: 0, classes: 0, elements: 0, depth: 0 };
1001
+ }
454
1002
  }
1003
+ /**
1004
+ * Check if rule is a reusable pattern (Task #44: uses pattern catalog)
1005
+ */
455
1006
  isReusablePattern(rule) {
1007
+ // Check against reusable patterns from catalog
1008
+ for (const pattern of this.patternCatalog.reusablePatterns.filter(p => p.enabled)) {
1009
+ if (this.matchesReusablePattern(rule, pattern)) {
1010
+ return true;
1011
+ }
1012
+ }
1013
+ // Legacy fallback
456
1014
  const props = Object.keys(rule.properties);
457
1015
  // Button patterns
458
1016
  if (props.includes('padding') && props.includes('border-radius') &&
@@ -466,13 +1024,37 @@ export class CSSAnalyzer {
466
1024
  }
467
1025
  return false;
468
1026
  }
1027
+ /**
1028
+ * Check if rule is a typography rule (Task #44: uses pattern catalog)
1029
+ * @internal Exposed for testing - now uses pattern catalog internally
1030
+ */
1031
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
469
1032
  isTypographyRule(rule) {
1033
+ // Check against typography global pattern from catalog
1034
+ const typographyPattern = this.patternCatalog.globalPatterns.find(p => p.id === 'typography' && p.enabled);
1035
+ if (typographyPattern) {
1036
+ const props = Object.keys(rule.properties);
1037
+ const matchCount = props.filter(p => typographyPattern.properties.includes(p)).length;
1038
+ return matchCount >= typographyPattern.minMatches;
1039
+ }
1040
+ // Legacy fallback
470
1041
  const typographyProps = ['font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing'];
471
1042
  const props = Object.keys(rule.properties);
472
1043
  const typographyCount = props.filter(p => typographyProps.includes(p)).length;
473
1044
  return typographyCount >= 2;
474
1045
  }
1046
+ /**
1047
+ * Check if rule is a button pattern (Task #44: uses pattern catalog)
1048
+ * @internal Exposed for testing - now uses pattern catalog internally
1049
+ */
1050
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
475
1051
  isButtonPattern(rule) {
1052
+ // Check against button reusable pattern from catalog
1053
+ const buttonPattern = this.patternCatalog.reusablePatterns.find(p => p.id === 'button' && p.enabled);
1054
+ if (buttonPattern) {
1055
+ return this.matchesReusablePattern(rule, buttonPattern);
1056
+ }
1057
+ // Legacy fallback
476
1058
  const props = Object.keys(rule.properties);
477
1059
  const buttonIndicators = ['cursor', 'background', 'border', 'padding', 'border-radius'];
478
1060
  const matchCount = props.filter(p => buttonIndicators.some(b => p.includes(b))).length;
@@ -496,13 +1078,25 @@ export class CSSAnalyzer {
496
1078
  return weights[value] || value;
497
1079
  }
498
1080
  suggestBEMName(selector) {
499
- const parts = selector.trim().split(/\s+/);
500
- if (parts.length >= 2) {
501
- const block = parts[0].replace('.', '');
502
- const element = parts[parts.length - 1].replace('.', '');
503
- return `.${block}__${element}`;
1081
+ try {
1082
+ const trimmed = selector.trim();
1083
+ // Handle edge cases
1084
+ if (!trimmed || trimmed.length > 1000) {
1085
+ return selector;
1086
+ }
1087
+ const parts = trimmed.split(/\s+/).filter(p => p.length > 0);
1088
+ if (parts.length >= 2) {
1089
+ const block = parts[0].replace(/[^a-zA-Z0-9_-]/g, '');
1090
+ const element = parts[parts.length - 1].replace(/[^a-zA-Z0-9_-]/g, '');
1091
+ if (block && element) {
1092
+ return `.${block}__${element}`;
1093
+ }
1094
+ }
1095
+ return selector;
1096
+ }
1097
+ catch {
1098
+ return selector;
504
1099
  }
505
- return selector;
506
1100
  }
507
1101
  completelyOverrides(newRule, existing) {
508
1102
  if (newRule.selector !== existing.selector)
@@ -512,13 +1106,21 @@ export class CSSAnalyzer {
512
1106
  return existingProps.every(prop => newProps.includes(prop));
513
1107
  }
514
1108
  isLikelyUnused(rule) {
515
- // Heuristics for likely unused rules
516
- const selector = rule.selector;
517
- // Very specific selectors that might be stale
518
- if (selector.includes('[data-v-') || selector.includes('[_ngcontent-')) {
519
- return true; // Likely framework-generated and orphaned
1109
+ try {
1110
+ const selector = rule.selector;
1111
+ // Handle edge cases
1112
+ if (!selector || typeof selector !== 'string' || selector.length > 2000) {
1113
+ return false;
1114
+ }
1115
+ // Very specific selectors that might be stale
1116
+ if (selector.includes('[data-v-') || selector.includes('[_ngcontent-')) {
1117
+ return true; // Likely framework-generated and orphaned
1118
+ }
1119
+ return false;
1120
+ }
1121
+ catch {
1122
+ return false;
520
1123
  }
521
- return false;
522
1124
  }
523
1125
  prioritizeSuggestions(suggestions) {
524
1126
  const severityOrder = { error: 0, warning: 1, info: 2, suggestion: 3 };
@@ -559,7 +1161,7 @@ export class CSSAnalyzer {
559
1161
  utilities.forEach(u => this.knownUtilityClasses.add(u));
560
1162
  }
561
1163
  /**
562
- * Map VARIABLE_CANDIDATES types to design token categories
1164
+ * Map pattern catalog variable types to design token categories
563
1165
  */
564
1166
  mapTypeToTokenCategory(candidateType) {
565
1167
  const mapping = {