@trentapps/manager-protocol 1.1.3 → 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.
- package/README.md +28 -0
- package/dist/analyzers/CSSAnalyzer.d.ts +180 -8
- package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/CSSAnalyzer.js +561 -105
- package/dist/analyzers/CSSAnalyzer.js.map +1 -1
- package/dist/config/dashboard.d.ts +55 -0
- package/dist/config/dashboard.d.ts.map +1 -0
- package/dist/config/dashboard.js +103 -0
- package/dist/config/dashboard.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/dashboard/httpDashboard.d.ts +100 -0
- package/dist/dashboard/httpDashboard.d.ts.map +1 -0
- package/dist/dashboard/httpDashboard.js +1276 -0
- package/dist/dashboard/httpDashboard.js.map +1 -0
- package/dist/dashboard/index.d.ts +6 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +7 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/engine/AuditLogger.d.ts +370 -2
- package/dist/engine/AuditLogger.d.ts.map +1 -1
- package/dist/engine/AuditLogger.js +1064 -24
- package/dist/engine/AuditLogger.js.map +1 -1
- package/dist/engine/GitHubClient.d.ts +183 -0
- package/dist/engine/GitHubClient.d.ts.map +1 -0
- package/dist/engine/GitHubClient.js +411 -0
- package/dist/engine/GitHubClient.js.map +1 -0
- package/dist/engine/RateLimiter.d.ts +5 -3
- package/dist/engine/RateLimiter.d.ts.map +1 -1
- package/dist/engine/RateLimiter.js +49 -72
- package/dist/engine/RateLimiter.js.map +1 -1
- package/dist/engine/RuleDependencyAnalyzer.d.ts +73 -0
- package/dist/engine/RuleDependencyAnalyzer.d.ts.map +1 -0
- package/dist/engine/RuleDependencyAnalyzer.js +475 -0
- package/dist/engine/RuleDependencyAnalyzer.js.map +1 -0
- package/dist/engine/RulesEngine.d.ts +102 -3
- package/dist/engine/RulesEngine.d.ts.map +1 -1
- package/dist/engine/RulesEngine.js +326 -21
- package/dist/engine/RulesEngine.js.map +1 -1
- package/dist/engine/TaskManager.d.ts +10 -14
- package/dist/engine/TaskManager.d.ts.map +1 -1
- package/dist/engine/TaskManager.js +169 -197
- package/dist/engine/TaskManager.js.map +1 -1
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +5 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/rules/azure.d.ts.map +1 -1
- package/dist/rules/azure.js +12 -14
- package/dist/rules/azure.js.map +1 -1
- package/dist/rules/compliance.d.ts.map +1 -1
- package/dist/rules/compliance.js +23 -41
- package/dist/rules/compliance.js.map +1 -1
- package/dist/rules/condition-optimizer.d.ts +151 -0
- package/dist/rules/condition-optimizer.d.ts.map +1 -0
- package/dist/rules/condition-optimizer.js +479 -0
- package/dist/rules/condition-optimizer.js.map +1 -0
- package/dist/rules/css.d.ts.map +1 -1
- package/dist/rules/css.js +538 -0
- package/dist/rules/css.js.map +1 -1
- package/dist/rules/field-standards.d.ts +1172 -0
- package/dist/rules/field-standards.d.ts.map +1 -0
- package/dist/rules/field-standards.js +908 -0
- package/dist/rules/field-standards.js.map +1 -0
- package/dist/rules/flask.d.ts.map +1 -1
- package/dist/rules/flask.js +18 -31
- package/dist/rules/flask.js.map +1 -1
- package/dist/rules/index.d.ts +220 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +155 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/ml-ai.d.ts.map +1 -1
- package/dist/rules/ml-ai.js +11 -13
- package/dist/rules/ml-ai.js.map +1 -1
- package/dist/rules/patterns.d.ts +568 -0
- package/dist/rules/patterns.d.ts.map +1 -0
- package/dist/rules/patterns.js +1359 -0
- package/dist/rules/patterns.js.map +1 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +580 -19
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/shared-patterns.d.ts +268 -0
- package/dist/rules/shared-patterns.d.ts.map +1 -0
- package/dist/rules/shared-patterns.js +556 -0
- package/dist/rules/shared-patterns.js.map +1 -0
- package/dist/rules/storage.d.ts +8 -2
- package/dist/rules/storage.d.ts.map +1 -1
- package/dist/rules/storage.js +541 -3
- package/dist/rules/storage.js.map +1 -1
- package/dist/rules/stripe.d.ts.map +1 -1
- package/dist/rules/stripe.js +19 -26
- package/dist/rules/stripe.js.map +1 -1
- package/dist/rules/websocket.d.ts.map +1 -1
- package/dist/rules/websocket.js +32 -40
- package/dist/rules/websocket.js.map +1 -1
- package/dist/supervisor/AgentSupervisor.d.ts +52 -0
- package/dist/supervisor/AgentSupervisor.d.ts.map +1 -1
- package/dist/supervisor/AgentSupervisor.js +120 -1
- package/dist/supervisor/AgentSupervisor.js.map +1 -1
- package/dist/supervisor/ManagedServerRegistry.d.ts +139 -2
- package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -1
- package/dist/supervisor/ManagedServerRegistry.js +590 -6
- package/dist/supervisor/ManagedServerRegistry.js.map +1 -1
- package/dist/supervisor/ProjectTracker.d.ts +2 -1
- package/dist/supervisor/ProjectTracker.d.ts.map +1 -1
- package/dist/supervisor/ProjectTracker.js +5 -9
- package/dist/supervisor/ProjectTracker.js.map +1 -1
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/rule-tester.d.ts +217 -0
- package/dist/testing/rule-tester.d.ts.map +1 -0
- package/dist/testing/rule-tester.examples.d.ts +57 -0
- package/dist/testing/rule-tester.examples.d.ts.map +1 -0
- package/dist/testing/rule-tester.examples.js +375 -0
- package/dist/testing/rule-tester.examples.js.map +1 -0
- package/dist/testing/rule-tester.js +381 -0
- package/dist/testing/rule-tester.js.map +1 -0
- package/dist/testing/rule-validator.d.ts +141 -0
- package/dist/testing/rule-validator.d.ts.map +1 -0
- package/dist/testing/rule-validator.js +640 -0
- package/dist/testing/rule-validator.js.map +1 -0
- package/dist/types/index.d.ts +265 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +57 -2
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/rate-limiting.d.ts +268 -0
- package/dist/utils/rate-limiting.d.ts.map +1 -0
- package/dist/utils/rate-limiting.js +403 -0
- package/dist/utils/rate-limiting.js.map +1 -0
- package/dist/utils/shared.d.ts +306 -0
- package/dist/utils/shared.d.ts.map +1 -0
- package/dist/utils/shared.js +464 -0
- package/dist/utils/shared.js.map +1 -0
- 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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
67
|
-
const
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
423
|
+
* Get or build index for existing rules (Task #51: caching for performance)
|
|
137
424
|
*/
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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: `
|
|
161
|
-
details: '
|
|
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: '
|
|
521
|
+
suggestedAction: 'Merge properties into single rule or use more specific selectors'
|
|
164
522
|
});
|
|
165
523
|
}
|
|
166
|
-
|
|
167
|
-
|
|
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: `
|
|
172
|
-
details: '
|
|
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: '
|
|
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
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
//
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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
|
|
1164
|
+
* Map pattern catalog variable types to design token categories
|
|
709
1165
|
*/
|
|
710
1166
|
mapTypeToTokenCategory(candidateType) {
|
|
711
1167
|
const mapping = {
|