@trentapps/manager-protocol 1.1.3 → 1.2.1

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 (142) hide show
  1. package/README.md +164 -17
  2. package/dist/analyzers/CSSAnalyzer.d.ts +180 -8
  3. package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -1
  4. package/dist/analyzers/CSSAnalyzer.js +561 -105
  5. package/dist/analyzers/CSSAnalyzer.js.map +1 -1
  6. package/dist/config/dashboard.d.ts +55 -0
  7. package/dist/config/dashboard.d.ts.map +1 -0
  8. package/dist/config/dashboard.js +103 -0
  9. package/dist/config/dashboard.js.map +1 -0
  10. package/dist/config/index.d.ts +7 -0
  11. package/dist/config/index.d.ts.map +1 -0
  12. package/dist/config/index.js +7 -0
  13. package/dist/config/index.js.map +1 -0
  14. package/dist/dashboard/httpDashboard.d.ts +100 -0
  15. package/dist/dashboard/httpDashboard.d.ts.map +1 -0
  16. package/dist/dashboard/httpDashboard.js +1276 -0
  17. package/dist/dashboard/httpDashboard.js.map +1 -0
  18. package/dist/dashboard/index.d.ts +6 -0
  19. package/dist/dashboard/index.d.ts.map +1 -0
  20. package/dist/dashboard/index.js +7 -0
  21. package/dist/dashboard/index.js.map +1 -0
  22. package/dist/engine/AuditLogger.d.ts +370 -2
  23. package/dist/engine/AuditLogger.d.ts.map +1 -1
  24. package/dist/engine/AuditLogger.js +1064 -24
  25. package/dist/engine/AuditLogger.js.map +1 -1
  26. package/dist/engine/GitHubClient.d.ts +183 -0
  27. package/dist/engine/GitHubClient.d.ts.map +1 -0
  28. package/dist/engine/GitHubClient.js +411 -0
  29. package/dist/engine/GitHubClient.js.map +1 -0
  30. package/dist/engine/RateLimiter.d.ts +5 -3
  31. package/dist/engine/RateLimiter.d.ts.map +1 -1
  32. package/dist/engine/RateLimiter.js +49 -72
  33. package/dist/engine/RateLimiter.js.map +1 -1
  34. package/dist/engine/RuleDependencyAnalyzer.d.ts +73 -0
  35. package/dist/engine/RuleDependencyAnalyzer.d.ts.map +1 -0
  36. package/dist/engine/RuleDependencyAnalyzer.js +475 -0
  37. package/dist/engine/RuleDependencyAnalyzer.js.map +1 -0
  38. package/dist/engine/RulesEngine.d.ts +102 -3
  39. package/dist/engine/RulesEngine.d.ts.map +1 -1
  40. package/dist/engine/RulesEngine.js +326 -21
  41. package/dist/engine/RulesEngine.js.map +1 -1
  42. package/dist/engine/TaskManager.d.ts +10 -14
  43. package/dist/engine/TaskManager.d.ts.map +1 -1
  44. package/dist/engine/TaskManager.js +169 -197
  45. package/dist/engine/TaskManager.js.map +1 -1
  46. package/dist/engine/index.d.ts +3 -0
  47. package/dist/engine/index.d.ts.map +1 -1
  48. package/dist/engine/index.js +5 -0
  49. package/dist/engine/index.js.map +1 -1
  50. package/dist/rules/azure.d.ts.map +1 -1
  51. package/dist/rules/azure.js +12 -14
  52. package/dist/rules/azure.js.map +1 -1
  53. package/dist/rules/compliance.d.ts.map +1 -1
  54. package/dist/rules/compliance.js +23 -41
  55. package/dist/rules/compliance.js.map +1 -1
  56. package/dist/rules/condition-optimizer.d.ts +151 -0
  57. package/dist/rules/condition-optimizer.d.ts.map +1 -0
  58. package/dist/rules/condition-optimizer.js +479 -0
  59. package/dist/rules/condition-optimizer.js.map +1 -0
  60. package/dist/rules/css.d.ts.map +1 -1
  61. package/dist/rules/css.js +538 -0
  62. package/dist/rules/css.js.map +1 -1
  63. package/dist/rules/field-standards.d.ts +1172 -0
  64. package/dist/rules/field-standards.d.ts.map +1 -0
  65. package/dist/rules/field-standards.js +908 -0
  66. package/dist/rules/field-standards.js.map +1 -0
  67. package/dist/rules/flask.d.ts.map +1 -1
  68. package/dist/rules/flask.js +18 -31
  69. package/dist/rules/flask.js.map +1 -1
  70. package/dist/rules/index.d.ts +220 -0
  71. package/dist/rules/index.d.ts.map +1 -1
  72. package/dist/rules/index.js +155 -0
  73. package/dist/rules/index.js.map +1 -1
  74. package/dist/rules/ml-ai.d.ts.map +1 -1
  75. package/dist/rules/ml-ai.js +11 -13
  76. package/dist/rules/ml-ai.js.map +1 -1
  77. package/dist/rules/patterns.d.ts +568 -0
  78. package/dist/rules/patterns.d.ts.map +1 -0
  79. package/dist/rules/patterns.js +1359 -0
  80. package/dist/rules/patterns.js.map +1 -0
  81. package/dist/rules/security.d.ts.map +1 -1
  82. package/dist/rules/security.js +580 -19
  83. package/dist/rules/security.js.map +1 -1
  84. package/dist/rules/shared-patterns.d.ts +268 -0
  85. package/dist/rules/shared-patterns.d.ts.map +1 -0
  86. package/dist/rules/shared-patterns.js +556 -0
  87. package/dist/rules/shared-patterns.js.map +1 -0
  88. package/dist/rules/storage.d.ts +8 -2
  89. package/dist/rules/storage.d.ts.map +1 -1
  90. package/dist/rules/storage.js +541 -3
  91. package/dist/rules/storage.js.map +1 -1
  92. package/dist/rules/stripe.d.ts.map +1 -1
  93. package/dist/rules/stripe.js +19 -26
  94. package/dist/rules/stripe.js.map +1 -1
  95. package/dist/rules/websocket.d.ts.map +1 -1
  96. package/dist/rules/websocket.js +32 -40
  97. package/dist/rules/websocket.js.map +1 -1
  98. package/dist/supervisor/AgentSupervisor.d.ts +52 -0
  99. package/dist/supervisor/AgentSupervisor.d.ts.map +1 -1
  100. package/dist/supervisor/AgentSupervisor.js +120 -1
  101. package/dist/supervisor/AgentSupervisor.js.map +1 -1
  102. package/dist/supervisor/ManagedServerRegistry.d.ts +139 -2
  103. package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -1
  104. package/dist/supervisor/ManagedServerRegistry.js +590 -6
  105. package/dist/supervisor/ManagedServerRegistry.js.map +1 -1
  106. package/dist/supervisor/ProjectTracker.d.ts +2 -1
  107. package/dist/supervisor/ProjectTracker.d.ts.map +1 -1
  108. package/dist/supervisor/ProjectTracker.js +5 -9
  109. package/dist/supervisor/ProjectTracker.js.map +1 -1
  110. package/dist/testing/index.d.ts +11 -0
  111. package/dist/testing/index.d.ts.map +1 -0
  112. package/dist/testing/index.js +12 -0
  113. package/dist/testing/index.js.map +1 -0
  114. package/dist/testing/rule-tester.d.ts +217 -0
  115. package/dist/testing/rule-tester.d.ts.map +1 -0
  116. package/dist/testing/rule-tester.examples.d.ts +57 -0
  117. package/dist/testing/rule-tester.examples.d.ts.map +1 -0
  118. package/dist/testing/rule-tester.examples.js +375 -0
  119. package/dist/testing/rule-tester.examples.js.map +1 -0
  120. package/dist/testing/rule-tester.js +381 -0
  121. package/dist/testing/rule-tester.js.map +1 -0
  122. package/dist/testing/rule-validator.d.ts +141 -0
  123. package/dist/testing/rule-validator.d.ts.map +1 -0
  124. package/dist/testing/rule-validator.js +640 -0
  125. package/dist/testing/rule-validator.js.map +1 -0
  126. package/dist/types/index.d.ts +265 -4
  127. package/dist/types/index.d.ts.map +1 -1
  128. package/dist/types/index.js +57 -2
  129. package/dist/types/index.js.map +1 -1
  130. package/dist/utils/index.d.ts +2 -0
  131. package/dist/utils/index.d.ts.map +1 -1
  132. package/dist/utils/index.js +2 -0
  133. package/dist/utils/index.js.map +1 -1
  134. package/dist/utils/rate-limiting.d.ts +268 -0
  135. package/dist/utils/rate-limiting.d.ts.map +1 -0
  136. package/dist/utils/rate-limiting.js +403 -0
  137. package/dist/utils/rate-limiting.js.map +1 -0
  138. package/dist/utils/shared.d.ts +306 -0
  139. package/dist/utils/shared.d.ts.map +1 -0
  140. package/dist/utils/shared.js +464 -0
  141. package/dist/utils/shared.js.map +1 -0
  142. package/package.json +3 -2
@@ -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,16 +73,178 @@ 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) {
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
+ });
41
244
  try {
42
245
  // Validate selector first
43
246
  if (!this.isValidSelector(context.newRule.selector)) {
247
+ this.logger.warn('Invalid CSS selector', { selector: context.newRule.selector });
44
248
  return {
45
249
  ruleId: `css-invalid-${this.simpleHash(context.newRule.selector)}`,
46
250
  newRule: context.newRule,
@@ -63,44 +267,98 @@ export class CSSAnalyzer {
63
267
  const duplicates = [];
64
268
  const removableCandidates = [];
65
269
  let riskScore = 0;
66
- // 1. Check for duplicates/similar rules
67
- const duplicateCheck = this.findDuplicates(context.newRule, context.existingRules || []);
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;
68
281
  duplicates.push(...duplicateCheck.duplicates);
69
282
  suggestions.push(...duplicateCheck.suggestions);
70
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
+ });
71
288
  // 2. Check if should be externalized
289
+ stepStart = performance.now();
72
290
  const externalizeCheck = this.checkExternalization(context);
291
+ timingMs.externalizeCheck = performance.now() - stepStart;
73
292
  suggestions.push(...externalizeCheck.suggestions);
74
293
  if (externalizeCheck.shouldExternalize)
75
294
  riskScore += 15;
76
295
  // 3. Check if should be global
296
+ stepStart = performance.now();
77
297
  const globalCheck = this.checkGlobalCandidate(context);
298
+ timingMs.globalCheck = performance.now() - stepStart;
78
299
  suggestions.push(...globalCheck.suggestions);
79
300
  if (globalCheck.shouldMakeGlobal)
80
301
  riskScore += 10;
81
- // 4. Check for variable candidates
302
+ // 4. Check for variable candidates (Task #44: uses pattern catalog)
303
+ stepStart = performance.now();
82
304
  const variableCheck = this.checkVariableCandidates(context.newRule);
305
+ timingMs.variableCheck = performance.now() - stepStart;
83
306
  suggestions.push(...variableCheck.suggestions);
84
307
  riskScore += variableCheck.suggestions.length * 5;
85
- // 5. Check for utility class opportunities
308
+ // 5. Check for utility class opportunities (Task #44: uses pattern catalog)
309
+ stepStart = performance.now();
86
310
  if (context.hasStyleSystem) {
87
311
  const utilityCheck = this.checkUtilityOpportunities(context);
88
312
  suggestions.push(...utilityCheck.suggestions);
89
313
  }
314
+ timingMs.utilityCheck = performance.now() - stepStart;
90
315
  // 6. Check specificity issues
316
+ stepStart = performance.now();
91
317
  const specificityCheck = this.checkSpecificity(context.newRule);
318
+ timingMs.specificityCheck = performance.now() - stepStart;
92
319
  suggestions.push(...specificityCheck.suggestions);
93
320
  riskScore += specificityCheck.riskContribution;
94
321
  // 7. Check naming conventions
322
+ stepStart = performance.now();
95
323
  const namingCheck = this.checkNamingConvention(context.newRule, context.framework);
324
+ timingMs.namingCheck = performance.now() - stepStart;
96
325
  suggestions.push(...namingCheck.suggestions);
97
- // 8. Find removable candidates
98
- if (context.existingRules) {
99
- removableCandidates.push(...this.findRemovableCandidates(context.newRule, context.existingRules));
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));
100
330
  }
331
+ timingMs.removableCheck = performance.now() - stepStart;
101
332
  // Generate summary
102
333
  const summary = this.generateSummary(context.newRule, suggestions, duplicates, externalizeCheck.shouldExternalize, globalCheck.shouldMakeGlobal);
103
- return {
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 = {
104
362
  ruleId,
105
363
  newRule: context.newRule,
106
364
  shouldExternalize: externalizeCheck.shouldExternalize,
@@ -111,8 +369,17 @@ export class CSSAnalyzer {
111
369
  riskScore: Math.min(100, riskScore),
112
370
  summary
113
371
  };
372
+ // Task #52: Include metrics if enabled
373
+ if (this.enableMetrics) {
374
+ result.metrics = metrics;
375
+ }
376
+ return result;
114
377
  }
115
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
+ });
116
383
  // Gracefully handle unexpected errors during analysis
117
384
  return {
118
385
  ruleId: `css-error-${this.simpleHash(context.newRule.selector)}`,
@@ -132,16 +399,102 @@ export class CSSAnalyzer {
132
399
  };
133
400
  }
134
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
+ // ============================================================================
135
422
  /**
136
- * Find duplicate or similar CSS rules
423
+ * Get or build index for existing rules (Task #51: caching for performance)
137
424
  */
138
- findDuplicates(newRule, existingRules) {
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);
475
+ return {
476
+ hash,
477
+ propertyCount: keys.length,
478
+ sortedKeys
479
+ };
480
+ }
481
+ /**
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
484
+ */
485
+ findDuplicatesOptimized(newRule, existingRules) {
139
486
  const duplicates = [];
140
487
  const suggestions = [];
141
- for (const existing of existingRules) {
142
- // Exact property match
143
- const similarity = this.calculateSimilarity(newRule.properties, existing.properties);
144
- 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) {
145
498
  duplicates.push(existing);
146
499
  suggestions.push({
147
500
  type: 'use_existing',
@@ -153,30 +506,92 @@ export class CSSAnalyzer {
153
506
  codeExample: `class="${existing.selector.replace('.', '')}"`
154
507
  });
155
508
  }
156
- 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) {
157
515
  suggestions.push({
158
516
  type: 'consolidate',
159
517
  severity: 'warning',
160
- message: `Similar CSS properties (${Math.round(similarity * 100)}% match) found in "${existing.selector}"`,
161
- 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',
162
520
  existingRule: existing,
163
- suggestedAction: 'Extend the existing rule or create a shared base class'
521
+ suggestedAction: 'Merge properties into single rule or use more specific selectors'
164
522
  });
165
523
  }
166
- // Check for same selector different properties
167
- 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) {
168
545
  suggestions.push({
169
546
  type: 'consolidate',
170
547
  severity: 'warning',
171
- message: `Selector "${newRule.selector}" already exists with different properties`,
172
- 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',
173
550
  existingRule: existing,
174
- suggestedAction: 'Merge properties into single rule or use more specific selectors'
551
+ suggestedAction: 'Extend the existing rule or create a shared base class'
175
552
  });
176
553
  }
177
554
  }
178
555
  return { duplicates, suggestions };
179
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
+ }
180
595
  /**
181
596
  * Check if CSS should be externalized
182
597
  */
@@ -223,48 +638,70 @@ export class CSSAnalyzer {
223
638
  return { shouldExternalize, suggestions };
224
639
  }
225
640
  /**
226
- * Check if CSS should be made global
641
+ * Check if CSS should be made global (Task #44: uses pattern catalog)
227
642
  */
228
643
  checkGlobalCandidate(context) {
229
644
  const suggestions = [];
230
645
  let shouldMakeGlobal = false;
231
646
  const properties = Object.keys(context.newRule.properties);
232
- // Check for global property indicators
233
- const globalIndicators = properties.filter(p => GLOBAL_PROPERTY_INDICATORS.includes(p));
234
- if (globalIndicators.length >= 2) {
235
- shouldMakeGlobal = true;
236
- suggestions.push({
237
- type: 'make_global',
238
- severity: 'suggestion',
239
- message: `Rule contains commonly global properties: ${globalIndicators.join(', ')}`,
240
- details: 'These properties often indicate design system tokens that should be global',
241
- suggestedAction: 'Consider adding to global styles or design system'
242
- });
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
+ }
243
660
  }
244
- // Typography rules should typically be global
245
- if (this.isTypographyRule(context.newRule)) {
246
- shouldMakeGlobal = true;
247
- suggestions.push({
248
- type: 'make_global',
249
- severity: 'suggestion',
250
- message: 'Typography styles should be defined globally for consistency',
251
- details: 'Consistent typography improves design cohesion and accessibility',
252
- suggestedAction: 'Add to typography section in global styles'
253
- });
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
+ }
254
674
  }
255
- // Button-like patterns should be global
256
- if (this.isButtonPattern(context.newRule)) {
257
- shouldMakeGlobal = true;
258
- suggestions.push({
259
- type: 'make_global',
260
- severity: 'suggestion',
261
- message: 'Button/interactive styles should be global components',
262
- details: 'Consistent interactive elements improve UX and accessibility',
263
- suggestedAction: 'Create a reusable button component/class'
264
- });
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
+ }
265
687
  }
266
688
  return { shouldMakeGlobal, suggestions };
267
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
+ }
268
705
  /**
269
706
  * Check for CSS variable candidates using design system tokens
270
707
  */
@@ -289,7 +726,7 @@ export class CSSAnalyzer {
289
726
  continue;
290
727
  }
291
728
  // Fall back to generic variable suggestions for unmatched values
292
- for (const candidate of VARIABLE_CANDIDATES) {
729
+ for (const candidate of this.patternCatalog.variablePatterns) {
293
730
  if (candidate.pattern.test(value)) {
294
731
  // Skip small values for spacing
295
732
  if (candidate.type === 'spacing' && candidate.minValue) {
@@ -322,26 +759,24 @@ export class CSSAnalyzer {
322
759
  return { suggestions };
323
760
  }
324
761
  /**
325
- * Check for utility class opportunities
762
+ * Check for utility class opportunities (Task #44: uses pattern catalog)
326
763
  */
327
764
  checkUtilityOpportunities(context) {
328
765
  const suggestions = [];
329
766
  const properties = context.newRule.properties;
330
- // Common utility mappings
331
- const utilityMappings = {
332
- 'display': (v) => v === 'flex' ? 'flex' : v === 'grid' ? 'grid' : v === 'none' ? 'hidden' : null,
333
- 'flex-direction': (v) => v === 'column' ? 'flex-col' : v === 'row' ? 'flex-row' : null,
334
- 'justify-content': (v) => `justify-${v.replace('flex-', '').replace('space-', '')}`,
335
- 'align-items': (v) => `items-${v.replace('flex-', '')}`,
336
- 'text-align': (v) => `text-${v}`,
337
- 'font-weight': (v) => `font-${this.getFontWeightName(v)}`,
338
- 'position': (v) => v,
339
- 'overflow': (v) => `overflow-${v}`,
340
- 'cursor': (v) => `cursor-${v}`
341
- };
767
+ // Task #44: Use utility mappings from pattern catalog
342
768
  for (const [property, value] of Object.entries(properties)) {
343
- if (utilityMappings[property]) {
344
- 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
+ }
345
780
  if (utilityClass) {
346
781
  suggestions.push({
347
782
  type: 'use_utility',
@@ -449,20 +884,13 @@ export class CSSAnalyzer {
449
884
  }
450
885
  /**
451
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
452
889
  */
890
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
453
891
  findRemovableCandidates(newRule, existingRules) {
454
- const removable = [];
455
- for (const existing of existingRules) {
456
- // If new rule completely overrides existing
457
- if (this.completelyOverrides(newRule, existing)) {
458
- removable.push(existing);
459
- }
460
- // Dead selectors (would need DOM analysis in real implementation)
461
- if (this.isLikelyUnused(existing)) {
462
- removable.push(existing);
463
- }
464
- }
465
- return removable;
892
+ // Delegate to optimized version
893
+ return this.findRemovableCandidatesOptimized(newRule, existingRules);
466
894
  }
467
895
  // ============================================================================
468
896
  // VALIDATION METHODS
@@ -538,13 +966,7 @@ export class CSSAnalyzer {
538
966
  return `css-${hash}`;
539
967
  }
540
968
  simpleHash(str) {
541
- let hash = 0;
542
- for (let i = 0; i < str.length; i++) {
543
- const char = str.charCodeAt(i);
544
- hash = ((hash << 5) - hash) + char;
545
- hash = hash & hash;
546
- }
547
- return Math.abs(hash).toString(16).substring(0, 8);
969
+ return hashString(str);
548
970
  }
549
971
  calculateSimilarity(props1, props2) {
550
972
  const keys1 = Object.keys(props1);
@@ -578,7 +1000,17 @@ export class CSSAnalyzer {
578
1000
  return { ids: 0, classes: 0, elements: 0, depth: 0 };
579
1001
  }
580
1002
  }
1003
+ /**
1004
+ * Check if rule is a reusable pattern (Task #44: uses pattern catalog)
1005
+ */
581
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
582
1014
  const props = Object.keys(rule.properties);
583
1015
  // Button patterns
584
1016
  if (props.includes('padding') && props.includes('border-radius') &&
@@ -592,13 +1024,37 @@ export class CSSAnalyzer {
592
1024
  }
593
1025
  return false;
594
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
595
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
596
1041
  const typographyProps = ['font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing'];
597
1042
  const props = Object.keys(rule.properties);
598
1043
  const typographyCount = props.filter(p => typographyProps.includes(p)).length;
599
1044
  return typographyCount >= 2;
600
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
601
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
602
1058
  const props = Object.keys(rule.properties);
603
1059
  const buttonIndicators = ['cursor', 'background', 'border', 'padding', 'border-radius'];
604
1060
  const matchCount = props.filter(p => buttonIndicators.some(b => p.includes(b))).length;
@@ -705,7 +1161,7 @@ export class CSSAnalyzer {
705
1161
  utilities.forEach(u => this.knownUtilityClasses.add(u));
706
1162
  }
707
1163
  /**
708
- * Map VARIABLE_CANDIDATES types to design token categories
1164
+ * Map pattern catalog variable types to design token categories
709
1165
  */
710
1166
  mapTypeToTokenCategory(candidateType) {
711
1167
  const mapping = {