@trentapps/manager-protocol 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +639 -0
- package/dist/analyzers/ArchitectureDetector.d.ts +44 -0
- package/dist/analyzers/ArchitectureDetector.d.ts.map +1 -0
- package/dist/analyzers/ArchitectureDetector.js +218 -0
- package/dist/analyzers/ArchitectureDetector.js.map +1 -0
- package/dist/analyzers/CSSAnalyzer.d.ts +284 -0
- package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/CSSAnalyzer.js +1180 -0
- package/dist/analyzers/CSSAnalyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +5 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +5 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +174 -0
- package/dist/cli.js.map +1 -0
- package/dist/design-system/index.d.ts +6 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +6 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/tokens.d.ts +106 -0
- package/dist/design-system/tokens.d.ts.map +1 -0
- package/dist/design-system/tokens.js +554 -0
- package/dist/design-system/tokens.js.map +1 -0
- package/dist/engine/AuditLogger.d.ts +506 -0
- package/dist/engine/AuditLogger.d.ts.map +1 -0
- package/dist/engine/AuditLogger.js +1491 -0
- package/dist/engine/AuditLogger.js.map +1 -0
- package/dist/engine/GitHubApprovalManager.d.ts +123 -0
- package/dist/engine/GitHubApprovalManager.d.ts.map +1 -0
- package/dist/engine/GitHubApprovalManager.js +347 -0
- package/dist/engine/GitHubApprovalManager.js.map +1 -0
- 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 +81 -0
- package/dist/engine/RateLimiter.d.ts.map +1 -0
- package/dist/engine/RateLimiter.js +215 -0
- package/dist/engine/RateLimiter.js.map +1 -0
- 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 +176 -0
- package/dist/engine/RulesEngine.d.ts.map +1 -0
- package/dist/engine/RulesEngine.js +705 -0
- package/dist/engine/RulesEngine.js.map +1 -0
- package/dist/engine/TaskManager.d.ts +174 -0
- package/dist/engine/TaskManager.d.ts.map +1 -0
- package/dist/engine/TaskManager.js +663 -0
- package/dist/engine/TaskManager.js.map +1 -0
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +13 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/architecture.d.ts +9 -0
- package/dist/rules/architecture.d.ts.map +1 -0
- package/dist/rules/architecture.js +322 -0
- package/dist/rules/architecture.js.map +1 -0
- package/dist/rules/azure.d.ts +7 -0
- package/dist/rules/azure.d.ts.map +1 -0
- package/dist/rules/azure.js +136 -0
- package/dist/rules/azure.js.map +1 -0
- package/dist/rules/compliance.d.ts +9 -0
- package/dist/rules/compliance.d.ts.map +1 -0
- package/dist/rules/compliance.js +286 -0
- package/dist/rules/compliance.js.map +1 -0
- 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 +10 -0
- package/dist/rules/css.d.ts.map +1 -0
- package/dist/rules/css.js +1777 -0
- package/dist/rules/css.js.map +1 -0
- 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 +7 -0
- package/dist/rules/flask.d.ts.map +1 -0
- package/dist/rules/flask.js +142 -0
- package/dist/rules/flask.js.map +1 -0
- package/dist/rules/index.d.ts +827 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +556 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/ml-ai.d.ts +7 -0
- package/dist/rules/ml-ai.d.ts.map +1 -0
- package/dist/rules/ml-ai.js +148 -0
- package/dist/rules/ml-ai.js.map +1 -0
- package/dist/rules/operational.d.ts +9 -0
- package/dist/rules/operational.d.ts.map +1 -0
- package/dist/rules/operational.js +318 -0
- package/dist/rules/operational.js.map +1 -0
- 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 +9 -0
- package/dist/rules/security.d.ts.map +1 -0
- package/dist/rules/security.js +848 -0
- package/dist/rules/security.js.map +1 -0
- 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 +13 -0
- package/dist/rules/storage.d.ts.map +1 -0
- package/dist/rules/storage.js +672 -0
- package/dist/rules/storage.js.map +1 -0
- package/dist/rules/stripe.d.ts +7 -0
- package/dist/rules/stripe.d.ts.map +1 -0
- package/dist/rules/stripe.js +133 -0
- package/dist/rules/stripe.js.map +1 -0
- package/dist/rules/testing.d.ts +7 -0
- package/dist/rules/testing.d.ts.map +1 -0
- package/dist/rules/testing.js +135 -0
- package/dist/rules/testing.js.map +1 -0
- package/dist/rules/ux.d.ts +9 -0
- package/dist/rules/ux.d.ts.map +1 -0
- package/dist/rules/ux.js +280 -0
- package/dist/rules/ux.js.map +1 -0
- package/dist/rules/websocket.d.ts +7 -0
- package/dist/rules/websocket.d.ts.map +1 -0
- package/dist/rules/websocket.js +128 -0
- package/dist/rules/websocket.js.map +1 -0
- package/dist/server.d.ts +43 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1967 -0
- package/dist/server.js.map +1 -0
- package/dist/supervisor/AgentSupervisor.d.ts +195 -0
- package/dist/supervisor/AgentSupervisor.d.ts.map +1 -0
- package/dist/supervisor/AgentSupervisor.js +569 -0
- package/dist/supervisor/AgentSupervisor.js.map +1 -0
- package/dist/supervisor/ManagedServerRegistry.d.ts +185 -0
- package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -0
- package/dist/supervisor/ManagedServerRegistry.js +729 -0
- package/dist/supervisor/ManagedServerRegistry.js.map +1 -0
- package/dist/supervisor/ProjectTracker.d.ts +210 -0
- package/dist/supervisor/ProjectTracker.d.ts.map +1 -0
- package/dist/supervisor/ProjectTracker.js +709 -0
- package/dist/supervisor/ProjectTracker.js.map +1 -0
- package/dist/supervisor/index.d.ts +6 -0
- package/dist/supervisor/index.d.ts.map +1 -0
- package/dist/supervisor/index.js +6 -0
- package/dist/supervisor/index.js.map +1 -0
- 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 +1282 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +386 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/errors.d.ts +86 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +171 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- 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/dist/utils/shell.d.ts +22 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +29 -0
- package/dist/utils/shell.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Agent Supervisor - CSS Analyzer
|
|
3
|
+
*
|
|
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
|
|
9
|
+
*/
|
|
10
|
+
import { suggestToken, getRecommendedTokens } from '../design-system/index.js';
|
|
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
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Properties that commonly indicate global/reusable styles
|
|
63
|
+
* Legacy constant for backward compatibility
|
|
64
|
+
*/
|
|
65
|
+
const GLOBAL_PROPERTY_INDICATORS = [
|
|
66
|
+
'font-family',
|
|
67
|
+
'color',
|
|
68
|
+
'background-color',
|
|
69
|
+
'border-radius',
|
|
70
|
+
'box-shadow',
|
|
71
|
+
'transition',
|
|
72
|
+
'animation'
|
|
73
|
+
];
|
|
74
|
+
export class CSSAnalyzer {
|
|
75
|
+
knownUtilityClasses = new Set();
|
|
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
|
+
};
|
|
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');
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Main analysis entry point
|
|
221
|
+
*/
|
|
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
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
// Validate selector first
|
|
246
|
+
if (!this.isValidSelector(context.newRule.selector)) {
|
|
247
|
+
this.logger.warn('Invalid CSS selector', { selector: context.newRule.selector });
|
|
248
|
+
return {
|
|
249
|
+
ruleId: `css-invalid-${this.simpleHash(context.newRule.selector)}`,
|
|
250
|
+
newRule: context.newRule,
|
|
251
|
+
shouldExternalize: false,
|
|
252
|
+
shouldMakeGlobal: false,
|
|
253
|
+
duplicates: [],
|
|
254
|
+
suggestions: [{
|
|
255
|
+
type: 'specificity_warning',
|
|
256
|
+
severity: 'error',
|
|
257
|
+
message: 'Invalid CSS selector',
|
|
258
|
+
details: 'The selector appears to be malformed or contains invalid characters'
|
|
259
|
+
}],
|
|
260
|
+
removableCandidates: [],
|
|
261
|
+
riskScore: 50,
|
|
262
|
+
summary: 'Skipped analysis: invalid CSS selector'
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const ruleId = this.generateRuleId(context.newRule);
|
|
266
|
+
const suggestions = [];
|
|
267
|
+
const duplicates = [];
|
|
268
|
+
const removableCandidates = [];
|
|
269
|
+
let riskScore = 0;
|
|
270
|
+
// Task #51: Build or reuse index for existing rules
|
|
271
|
+
const existingRules = context.existingRules || [];
|
|
272
|
+
if (existingRules.length > 0) {
|
|
273
|
+
const indexResult = this.getOrBuildIndex(existingRules);
|
|
274
|
+
cacheHits += indexResult.cacheHit ? 1 : 0;
|
|
275
|
+
cacheMisses += indexResult.cacheHit ? 0 : 1;
|
|
276
|
+
}
|
|
277
|
+
// 1. Check for duplicates/similar rules (Task #51: optimized)
|
|
278
|
+
let stepStart = performance.now();
|
|
279
|
+
const duplicateCheck = this.findDuplicatesOptimized(context.newRule, existingRules);
|
|
280
|
+
timingMs.duplicateCheck = performance.now() - stepStart;
|
|
281
|
+
duplicates.push(...duplicateCheck.duplicates);
|
|
282
|
+
suggestions.push(...duplicateCheck.suggestions);
|
|
283
|
+
riskScore += duplicateCheck.duplicates.length * 10;
|
|
284
|
+
this.logger.debug('Duplicate check complete', {
|
|
285
|
+
duplicatesFound: duplicateCheck.duplicates.length,
|
|
286
|
+
timeMs: timingMs.duplicateCheck.toFixed(2)
|
|
287
|
+
});
|
|
288
|
+
// 2. Check if should be externalized
|
|
289
|
+
stepStart = performance.now();
|
|
290
|
+
const externalizeCheck = this.checkExternalization(context);
|
|
291
|
+
timingMs.externalizeCheck = performance.now() - stepStart;
|
|
292
|
+
suggestions.push(...externalizeCheck.suggestions);
|
|
293
|
+
if (externalizeCheck.shouldExternalize)
|
|
294
|
+
riskScore += 15;
|
|
295
|
+
// 3. Check if should be global
|
|
296
|
+
stepStart = performance.now();
|
|
297
|
+
const globalCheck = this.checkGlobalCandidate(context);
|
|
298
|
+
timingMs.globalCheck = performance.now() - stepStart;
|
|
299
|
+
suggestions.push(...globalCheck.suggestions);
|
|
300
|
+
if (globalCheck.shouldMakeGlobal)
|
|
301
|
+
riskScore += 10;
|
|
302
|
+
// 4. Check for variable candidates (Task #44: uses pattern catalog)
|
|
303
|
+
stepStart = performance.now();
|
|
304
|
+
const variableCheck = this.checkVariableCandidates(context.newRule);
|
|
305
|
+
timingMs.variableCheck = performance.now() - stepStart;
|
|
306
|
+
suggestions.push(...variableCheck.suggestions);
|
|
307
|
+
riskScore += variableCheck.suggestions.length * 5;
|
|
308
|
+
// 5. Check for utility class opportunities (Task #44: uses pattern catalog)
|
|
309
|
+
stepStart = performance.now();
|
|
310
|
+
if (context.hasStyleSystem) {
|
|
311
|
+
const utilityCheck = this.checkUtilityOpportunities(context);
|
|
312
|
+
suggestions.push(...utilityCheck.suggestions);
|
|
313
|
+
}
|
|
314
|
+
timingMs.utilityCheck = performance.now() - stepStart;
|
|
315
|
+
// 6. Check specificity issues
|
|
316
|
+
stepStart = performance.now();
|
|
317
|
+
const specificityCheck = this.checkSpecificity(context.newRule);
|
|
318
|
+
timingMs.specificityCheck = performance.now() - stepStart;
|
|
319
|
+
suggestions.push(...specificityCheck.suggestions);
|
|
320
|
+
riskScore += specificityCheck.riskContribution;
|
|
321
|
+
// 7. Check naming conventions
|
|
322
|
+
stepStart = performance.now();
|
|
323
|
+
const namingCheck = this.checkNamingConvention(context.newRule, context.framework);
|
|
324
|
+
timingMs.namingCheck = performance.now() - stepStart;
|
|
325
|
+
suggestions.push(...namingCheck.suggestions);
|
|
326
|
+
// 8. Find removable candidates (Task #51: optimized)
|
|
327
|
+
stepStart = performance.now();
|
|
328
|
+
if (existingRules.length > 0) {
|
|
329
|
+
removableCandidates.push(...this.findRemovableCandidatesOptimized(context.newRule, existingRules));
|
|
330
|
+
}
|
|
331
|
+
timingMs.removableCheck = performance.now() - stepStart;
|
|
332
|
+
// Generate summary
|
|
333
|
+
const summary = this.generateSummary(context.newRule, suggestions, duplicates, externalizeCheck.shouldExternalize, globalCheck.shouldMakeGlobal);
|
|
334
|
+
timingMs.total = performance.now() - totalStart;
|
|
335
|
+
// Task #52: Build metrics
|
|
336
|
+
const metrics = {
|
|
337
|
+
rulesAnalyzed: existingRules.length + 1,
|
|
338
|
+
duplicatesFound: duplicates.length,
|
|
339
|
+
suggestionsMade: suggestions.length,
|
|
340
|
+
suggestionsByType: this.countSuggestionsByType(suggestions),
|
|
341
|
+
suggestionsBySeverity: this.countSuggestionsBySeverity(suggestions),
|
|
342
|
+
timingMs,
|
|
343
|
+
cacheHits,
|
|
344
|
+
cacheMisses
|
|
345
|
+
};
|
|
346
|
+
// Task #52: Update cumulative metrics
|
|
347
|
+
this.cumulativeMetrics.totalAnalyses++;
|
|
348
|
+
this.cumulativeMetrics.totalRulesAnalyzed += metrics.rulesAnalyzed;
|
|
349
|
+
this.cumulativeMetrics.totalDuplicatesFound += metrics.duplicatesFound;
|
|
350
|
+
this.cumulativeMetrics.totalSuggestionsMade += metrics.suggestionsMade;
|
|
351
|
+
this.cumulativeMetrics.totalCacheHits += cacheHits;
|
|
352
|
+
this.cumulativeMetrics.totalCacheMisses += cacheMisses;
|
|
353
|
+
this.cumulativeMetrics.totalTimeMs += timingMs.total;
|
|
354
|
+
this.logger.info('CSS analysis complete', {
|
|
355
|
+
ruleId,
|
|
356
|
+
riskScore: Math.min(100, riskScore),
|
|
357
|
+
duplicatesFound: duplicates.length,
|
|
358
|
+
suggestionsCount: suggestions.length,
|
|
359
|
+
totalTimeMs: timingMs.total.toFixed(2)
|
|
360
|
+
});
|
|
361
|
+
const result = {
|
|
362
|
+
ruleId,
|
|
363
|
+
newRule: context.newRule,
|
|
364
|
+
shouldExternalize: externalizeCheck.shouldExternalize,
|
|
365
|
+
shouldMakeGlobal: globalCheck.shouldMakeGlobal,
|
|
366
|
+
duplicates,
|
|
367
|
+
suggestions: this.prioritizeSuggestions(suggestions),
|
|
368
|
+
removableCandidates,
|
|
369
|
+
riskScore: Math.min(100, riskScore),
|
|
370
|
+
summary
|
|
371
|
+
};
|
|
372
|
+
// Task #52: Include metrics if enabled
|
|
373
|
+
if (this.enableMetrics) {
|
|
374
|
+
result.metrics = metrics;
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
this.logger.error('CSS analysis failed', {
|
|
380
|
+
selector: context.newRule.selector,
|
|
381
|
+
error: error instanceof Error ? error.message : String(error)
|
|
382
|
+
});
|
|
383
|
+
// Gracefully handle unexpected errors during analysis
|
|
384
|
+
return {
|
|
385
|
+
ruleId: `css-error-${this.simpleHash(context.newRule.selector)}`,
|
|
386
|
+
newRule: context.newRule,
|
|
387
|
+
shouldExternalize: false,
|
|
388
|
+
shouldMakeGlobal: false,
|
|
389
|
+
duplicates: [],
|
|
390
|
+
suggestions: [{
|
|
391
|
+
type: 'specificity_warning',
|
|
392
|
+
severity: 'error',
|
|
393
|
+
message: 'CSS analysis error',
|
|
394
|
+
details: `Failed to analyze CSS selector: ${error instanceof Error ? error.message : String(error)}`
|
|
395
|
+
}],
|
|
396
|
+
removableCandidates: [],
|
|
397
|
+
riskScore: 50,
|
|
398
|
+
summary: 'Analysis failed due to unexpected error'
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// ============================================================================
|
|
403
|
+
// Task #52: METRICS HELPERS
|
|
404
|
+
// ============================================================================
|
|
405
|
+
countSuggestionsByType(suggestions) {
|
|
406
|
+
const counts = {};
|
|
407
|
+
for (const s of suggestions) {
|
|
408
|
+
counts[s.type] = (counts[s.type] || 0) + 1;
|
|
409
|
+
}
|
|
410
|
+
return counts;
|
|
411
|
+
}
|
|
412
|
+
countSuggestionsBySeverity(suggestions) {
|
|
413
|
+
const counts = {};
|
|
414
|
+
for (const s of suggestions) {
|
|
415
|
+
counts[s.severity] = (counts[s.severity] || 0) + 1;
|
|
416
|
+
}
|
|
417
|
+
return counts;
|
|
418
|
+
}
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// Task #51: OPTIMIZED INDEXING AND DUPLICATE DETECTION
|
|
421
|
+
// ============================================================================
|
|
422
|
+
/**
|
|
423
|
+
* Get or build index for existing rules (Task #51: caching for performance)
|
|
424
|
+
*/
|
|
425
|
+
getOrBuildIndex(existingRules) {
|
|
426
|
+
// Check if we can reuse the existing index
|
|
427
|
+
if (this.enableCaching && this._ruleIndex && this._lastIndexedRules === existingRules) {
|
|
428
|
+
this.logger.debug('Index cache hit');
|
|
429
|
+
return { index: this._ruleIndex, cacheHit: true };
|
|
430
|
+
}
|
|
431
|
+
this.logger.debug('Building new rule index', { ruleCount: existingRules.length });
|
|
432
|
+
const startTime = performance.now();
|
|
433
|
+
const index = {
|
|
434
|
+
byPropertyHash: new Map(),
|
|
435
|
+
bySelector: new Map(),
|
|
436
|
+
byPropertyKeys: new Map(),
|
|
437
|
+
signatures: new Map()
|
|
438
|
+
};
|
|
439
|
+
for (const rule of existingRules) {
|
|
440
|
+
const signature = this.computePropertySignature(rule.properties);
|
|
441
|
+
index.signatures.set(rule, signature);
|
|
442
|
+
// Index by property hash (for exact matches)
|
|
443
|
+
const hashRules = index.byPropertyHash.get(signature.hash) || [];
|
|
444
|
+
hashRules.push(rule);
|
|
445
|
+
index.byPropertyHash.set(signature.hash, hashRules);
|
|
446
|
+
// Index by selector
|
|
447
|
+
const selectorRules = index.bySelector.get(rule.selector) || [];
|
|
448
|
+
selectorRules.push(rule);
|
|
449
|
+
index.bySelector.set(rule.selector, selectorRules);
|
|
450
|
+
// Index by sorted property keys (for similarity pre-filtering)
|
|
451
|
+
const keyRules = index.byPropertyKeys.get(signature.sortedKeys) || [];
|
|
452
|
+
keyRules.push(rule);
|
|
453
|
+
index.byPropertyKeys.set(signature.sortedKeys, keyRules);
|
|
454
|
+
}
|
|
455
|
+
if (this.enableCaching) {
|
|
456
|
+
this._ruleIndex = index;
|
|
457
|
+
this._lastIndexedRules = existingRules;
|
|
458
|
+
}
|
|
459
|
+
this.logger.debug('Index built', {
|
|
460
|
+
timeMs: (performance.now() - startTime).toFixed(2),
|
|
461
|
+
uniquePropertyHashes: index.byPropertyHash.size,
|
|
462
|
+
uniqueSelectors: index.bySelector.size,
|
|
463
|
+
uniquePropertyKeyPatterns: index.byPropertyKeys.size
|
|
464
|
+
});
|
|
465
|
+
return { index, cacheHit: false };
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Compute a signature for CSS properties for fast comparison (Task #51)
|
|
469
|
+
*/
|
|
470
|
+
computePropertySignature(properties) {
|
|
471
|
+
const keys = Object.keys(properties).sort();
|
|
472
|
+
const sortedKeys = keys.join(',');
|
|
473
|
+
const values = keys.map(k => `${k}:${properties[k]}`).join(';');
|
|
474
|
+
const hash = this.simpleHash(values);
|
|
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) {
|
|
486
|
+
const duplicates = [];
|
|
487
|
+
const suggestions = [];
|
|
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) {
|
|
498
|
+
duplicates.push(existing);
|
|
499
|
+
suggestions.push({
|
|
500
|
+
type: 'use_existing',
|
|
501
|
+
severity: 'error',
|
|
502
|
+
message: `Identical CSS properties already exist in "${existing.selector}"`,
|
|
503
|
+
details: `Found at ${existing.source}${existing.file ? `: ${existing.file}` : ''}${existing.line ? `:${existing.line}` : ''}`,
|
|
504
|
+
existingRule: existing,
|
|
505
|
+
suggestedAction: `Use existing class "${existing.selector}" instead of creating new rule`,
|
|
506
|
+
codeExample: `class="${existing.selector.replace('.', '')}"`
|
|
507
|
+
});
|
|
508
|
+
}
|
|
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) {
|
|
515
|
+
suggestions.push({
|
|
516
|
+
type: 'consolidate',
|
|
517
|
+
severity: 'warning',
|
|
518
|
+
message: `Selector "${newRule.selector}" already exists with different properties`,
|
|
519
|
+
details: 'Multiple rules with same selector can cause specificity conflicts',
|
|
520
|
+
existingRule: existing,
|
|
521
|
+
suggestedAction: 'Merge properties into single rule or use more specific selectors'
|
|
522
|
+
});
|
|
523
|
+
}
|
|
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) {
|
|
545
|
+
suggestions.push({
|
|
546
|
+
type: 'consolidate',
|
|
547
|
+
severity: 'warning',
|
|
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',
|
|
550
|
+
existingRule: existing,
|
|
551
|
+
suggestedAction: 'Extend the existing rule or create a shared base class'
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return { duplicates, suggestions };
|
|
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
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Check if CSS should be externalized
|
|
597
|
+
*/
|
|
598
|
+
checkExternalization(context) {
|
|
599
|
+
const suggestions = [];
|
|
600
|
+
let shouldExternalize = false;
|
|
601
|
+
// Inline styles should almost always be externalized
|
|
602
|
+
if (context.newRule.source === 'inline') {
|
|
603
|
+
shouldExternalize = true;
|
|
604
|
+
suggestions.push({
|
|
605
|
+
type: 'externalize',
|
|
606
|
+
severity: 'warning',
|
|
607
|
+
message: 'Inline styles should be moved to external stylesheet',
|
|
608
|
+
details: 'Inline styles hurt cacheability, increase HTML size, and make maintenance difficult',
|
|
609
|
+
suggestedAction: 'Create a class in your stylesheet and apply it to the element',
|
|
610
|
+
codeExample: this.generateExternalizeExample(context.newRule, context.componentName)
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
// Style tags with many rules should be external
|
|
614
|
+
if (context.newRule.source === 'style_tag') {
|
|
615
|
+
const propertyCount = Object.keys(context.newRule.properties).length;
|
|
616
|
+
if (propertyCount > 5) {
|
|
617
|
+
shouldExternalize = true;
|
|
618
|
+
suggestions.push({
|
|
619
|
+
type: 'externalize',
|
|
620
|
+
severity: 'info',
|
|
621
|
+
message: 'Complex style rules (>5 properties) benefit from external stylesheets',
|
|
622
|
+
details: 'External stylesheets enable better caching, minification, and tooling support',
|
|
623
|
+
suggestedAction: `Move to ${context.globalStylesFile || 'styles.css'}`
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
// Reusable patterns should be external
|
|
627
|
+
if (this.isReusablePattern(context.newRule)) {
|
|
628
|
+
shouldExternalize = true;
|
|
629
|
+
suggestions.push({
|
|
630
|
+
type: 'externalize',
|
|
631
|
+
severity: 'suggestion',
|
|
632
|
+
message: 'This appears to be a reusable pattern',
|
|
633
|
+
details: 'Reusable CSS patterns should be in external files for consistency',
|
|
634
|
+
suggestedAction: 'Move to shared stylesheet or component library'
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return { shouldExternalize, suggestions };
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Check if CSS should be made global (Task #44: uses pattern catalog)
|
|
642
|
+
*/
|
|
643
|
+
checkGlobalCandidate(context) {
|
|
644
|
+
const suggestions = [];
|
|
645
|
+
let shouldMakeGlobal = false;
|
|
646
|
+
const properties = Object.keys(context.newRule.properties);
|
|
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
|
+
}
|
|
660
|
+
}
|
|
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
|
+
}
|
|
674
|
+
}
|
|
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
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return { shouldMakeGlobal, suggestions };
|
|
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
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Check for CSS variable candidates using design system tokens
|
|
707
|
+
*/
|
|
708
|
+
checkVariableCandidates(rule) {
|
|
709
|
+
const suggestions = [];
|
|
710
|
+
for (const [property, value] of Object.entries(rule.properties)) {
|
|
711
|
+
// First, try to find an exact or close match from the design system
|
|
712
|
+
const tokenSuggestion = suggestToken(value, property);
|
|
713
|
+
if (tokenSuggestion && tokenSuggestion.confidence >= 0.7) {
|
|
714
|
+
const { token, confidence } = tokenSuggestion;
|
|
715
|
+
const confidencePercent = Math.round(confidence * 100);
|
|
716
|
+
suggestions.push({
|
|
717
|
+
type: 'use_variable',
|
|
718
|
+
severity: confidence >= 0.95 ? 'warning' : 'info',
|
|
719
|
+
message: `${property}: ${value} should use design token var(${token.name})`,
|
|
720
|
+
details: confidence >= 0.95
|
|
721
|
+
? `Exact match found: "${token.description}" - Use design system token for consistency`
|
|
722
|
+
: `Close match (${confidencePercent}%): "${token.description}" - Consider using design system token`,
|
|
723
|
+
suggestedAction: `Replace with var(${token.name})`,
|
|
724
|
+
codeExample: `${property}: var(${token.name}); /* ${token.description} */`
|
|
725
|
+
});
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
// Fall back to generic variable suggestions for unmatched values
|
|
729
|
+
for (const candidate of this.patternCatalog.variablePatterns) {
|
|
730
|
+
if (candidate.pattern.test(value)) {
|
|
731
|
+
// Skip small values for spacing
|
|
732
|
+
if (candidate.type === 'spacing' && candidate.minValue) {
|
|
733
|
+
const numValue = parseInt(value);
|
|
734
|
+
if (numValue < candidate.minValue)
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
// Map candidate type to token category
|
|
738
|
+
const tokenCategory = this.mapTypeToTokenCategory(candidate.type);
|
|
739
|
+
const recommendedTokens = tokenCategory ? getRecommendedTokens(tokenCategory) : [];
|
|
740
|
+
const tokenList = recommendedTokens.slice(0, 3).map(t => t.name).join(', ');
|
|
741
|
+
suggestions.push({
|
|
742
|
+
type: 'use_variable',
|
|
743
|
+
severity: 'info',
|
|
744
|
+
message: `${property}: ${value} should use a CSS variable`,
|
|
745
|
+
details: recommendedTokens.length > 0
|
|
746
|
+
? `Available ${candidate.type} tokens: ${tokenList}`
|
|
747
|
+
: `${candidate.type} values should be defined as CSS custom properties for consistency`,
|
|
748
|
+
suggestedAction: recommendedTokens.length > 0
|
|
749
|
+
? `Consider design tokens: ${tokenList}`
|
|
750
|
+
: `Replace with var(${candidate.varPrefix}xxx)`,
|
|
751
|
+
codeExample: recommendedTokens.length > 0
|
|
752
|
+
? `${property}: var(${recommendedTokens[0].name}); /* ${recommendedTokens[0].description} */`
|
|
753
|
+
: `${property}: var(${candidate.varPrefix}primary);`
|
|
754
|
+
});
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return { suggestions };
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Check for utility class opportunities (Task #44: uses pattern catalog)
|
|
763
|
+
*/
|
|
764
|
+
checkUtilityOpportunities(context) {
|
|
765
|
+
const suggestions = [];
|
|
766
|
+
const properties = context.newRule.properties;
|
|
767
|
+
// Task #44: Use utility mappings from pattern catalog
|
|
768
|
+
for (const [property, value] of Object.entries(properties)) {
|
|
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
|
+
}
|
|
780
|
+
if (utilityClass) {
|
|
781
|
+
suggestions.push({
|
|
782
|
+
type: 'use_utility',
|
|
783
|
+
severity: 'suggestion',
|
|
784
|
+
message: `"${property}: ${value}" can be replaced with utility class`,
|
|
785
|
+
details: `Using ${context.styleSystemName || 'utility'} classes reduces custom CSS`,
|
|
786
|
+
suggestedAction: `Use class="${utilityClass}" instead`,
|
|
787
|
+
codeExample: `<div class="${utilityClass}">...</div>`
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return { suggestions };
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Check CSS specificity issues
|
|
796
|
+
*/
|
|
797
|
+
checkSpecificity(rule) {
|
|
798
|
+
const suggestions = [];
|
|
799
|
+
let riskContribution = 0;
|
|
800
|
+
const specificity = this.calculateSpecificity(rule.selector);
|
|
801
|
+
// ID selectors
|
|
802
|
+
if (specificity.ids > 0) {
|
|
803
|
+
suggestions.push({
|
|
804
|
+
type: 'specificity_warning',
|
|
805
|
+
severity: 'warning',
|
|
806
|
+
message: 'Avoid ID selectors in CSS',
|
|
807
|
+
details: 'ID selectors have high specificity making overrides difficult',
|
|
808
|
+
suggestedAction: 'Use class selectors instead of IDs'
|
|
809
|
+
});
|
|
810
|
+
riskContribution += 15;
|
|
811
|
+
}
|
|
812
|
+
// Deeply nested selectors
|
|
813
|
+
if (specificity.depth > 3) {
|
|
814
|
+
suggestions.push({
|
|
815
|
+
type: 'specificity_warning',
|
|
816
|
+
severity: 'warning',
|
|
817
|
+
message: `Deep selector nesting (${specificity.depth} levels)`,
|
|
818
|
+
details: 'Deeply nested selectors are brittle and hard to override',
|
|
819
|
+
suggestedAction: 'Use BEM or flat class naming instead'
|
|
820
|
+
});
|
|
821
|
+
riskContribution += 10;
|
|
822
|
+
}
|
|
823
|
+
// !important
|
|
824
|
+
const hasImportant = Object.values(rule.properties).some(v => v.includes('!important'));
|
|
825
|
+
if (hasImportant) {
|
|
826
|
+
suggestions.push({
|
|
827
|
+
type: 'specificity_warning',
|
|
828
|
+
severity: 'error',
|
|
829
|
+
message: 'Avoid !important declarations',
|
|
830
|
+
details: '!important creates specificity problems and indicates architecture issues',
|
|
831
|
+
suggestedAction: 'Refactor CSS to avoid needing !important'
|
|
832
|
+
});
|
|
833
|
+
riskContribution += 25;
|
|
834
|
+
}
|
|
835
|
+
return { suggestions, riskContribution };
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Check naming convention
|
|
839
|
+
*/
|
|
840
|
+
checkNamingConvention(rule, framework) {
|
|
841
|
+
const suggestions = [];
|
|
842
|
+
try {
|
|
843
|
+
const selector = rule.selector;
|
|
844
|
+
// Validate selector before processing
|
|
845
|
+
if (!selector || typeof selector !== 'string' || selector.length === 0) {
|
|
846
|
+
return { suggestions };
|
|
847
|
+
}
|
|
848
|
+
// Check for meaningful names
|
|
849
|
+
if (/^\.[a-z]$/.test(selector) || /^\.(div|span|container)\d*$/.test(selector)) {
|
|
850
|
+
suggestions.push({
|
|
851
|
+
type: 'naming_convention',
|
|
852
|
+
severity: 'warning',
|
|
853
|
+
message: 'Use descriptive class names',
|
|
854
|
+
details: 'Generic names like "div1" or single letters are hard to understand',
|
|
855
|
+
suggestedAction: 'Use semantic names that describe purpose (e.g., .card-header)'
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
// BEM recommendation for complex selectors
|
|
859
|
+
if (selector.includes(' ') && !selector.includes('__') && !selector.includes('--')) {
|
|
860
|
+
suggestions.push({
|
|
861
|
+
type: 'naming_convention',
|
|
862
|
+
severity: 'info',
|
|
863
|
+
message: 'Consider using BEM naming convention',
|
|
864
|
+
details: 'BEM (Block__Element--Modifier) creates clear relationships',
|
|
865
|
+
suggestedAction: 'Rename using BEM: .block__element--modifier',
|
|
866
|
+
codeExample: this.suggestBEMName(selector)
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
// Framework-specific conventions
|
|
870
|
+
if (framework === 'react' && selector.includes('_')) {
|
|
871
|
+
suggestions.push({
|
|
872
|
+
type: 'naming_convention',
|
|
873
|
+
severity: 'info',
|
|
874
|
+
message: 'React typically uses camelCase for CSS Modules',
|
|
875
|
+
details: 'CSS Modules work better with camelCase class names',
|
|
876
|
+
suggestedAction: 'Use camelCase: .cardHeader instead of .card_header'
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
catch (error) {
|
|
881
|
+
// Silently skip naming convention checks if they fail
|
|
882
|
+
}
|
|
883
|
+
return { suggestions };
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
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
|
|
889
|
+
*/
|
|
890
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
891
|
+
findRemovableCandidates(newRule, existingRules) {
|
|
892
|
+
// Delegate to optimized version
|
|
893
|
+
return this.findRemovableCandidatesOptimized(newRule, existingRules);
|
|
894
|
+
}
|
|
895
|
+
// ============================================================================
|
|
896
|
+
// VALIDATION METHODS
|
|
897
|
+
// ============================================================================
|
|
898
|
+
/**
|
|
899
|
+
* Validate that a selector is well-formed and safe to process
|
|
900
|
+
*/
|
|
901
|
+
isValidSelector(selector) {
|
|
902
|
+
try {
|
|
903
|
+
// Check for empty or whitespace-only selectors
|
|
904
|
+
if (!selector || typeof selector !== 'string' || selector.trim().length === 0) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
// Limit selector length to prevent processing extremely large selectors
|
|
908
|
+
if (selector.length > 2000) {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
// Check for unclosed brackets/quotes
|
|
912
|
+
const openBrackets = (selector.match(/\[/g) || []).length;
|
|
913
|
+
const closeBrackets = (selector.match(/\]/g) || []).length;
|
|
914
|
+
if (openBrackets !== closeBrackets) {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
// Check for unclosed parentheses
|
|
918
|
+
const openParens = (selector.match(/\(/g) || []).length;
|
|
919
|
+
const closeParens = (selector.match(/\)/g) || []).length;
|
|
920
|
+
if (openParens !== closeParens) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
// Check for unclosed quotes (both single and double)
|
|
924
|
+
const singleQuotes = (selector.match(/'/g) || []).length;
|
|
925
|
+
const doubleQuotes = (selector.match(/"/g) || []).length;
|
|
926
|
+
if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0) {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
// If validation itself throws, consider selector invalid
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Normalize a selector for safe analysis by removing problematic content
|
|
938
|
+
*/
|
|
939
|
+
normalizeSelectorForAnalysis(selector) {
|
|
940
|
+
try {
|
|
941
|
+
if (!selector || typeof selector !== 'string') {
|
|
942
|
+
return '';
|
|
943
|
+
}
|
|
944
|
+
// Remove pseudo-elements and pseudo-classes that contain content
|
|
945
|
+
let normalized = selector
|
|
946
|
+
.replace(/:not\([^)]*\)/gi, '') // Remove :not() pseudo-class with content
|
|
947
|
+
.replace(/::?before/gi, '')
|
|
948
|
+
.replace(/::?after/gi, '')
|
|
949
|
+
.replace(/::?first-line/gi, '')
|
|
950
|
+
.replace(/::?first-letter/gi, '');
|
|
951
|
+
// Limit length to prevent excessive processing
|
|
952
|
+
if (normalized.length > 2000) {
|
|
953
|
+
normalized = normalized.substring(0, 2000);
|
|
954
|
+
}
|
|
955
|
+
return normalized;
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return '';
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
// ============================================================================
|
|
962
|
+
// HELPER METHODS
|
|
963
|
+
// ============================================================================
|
|
964
|
+
generateRuleId(rule) {
|
|
965
|
+
const hash = this.simpleHash(rule.selector + JSON.stringify(rule.properties));
|
|
966
|
+
return `css-${hash}`;
|
|
967
|
+
}
|
|
968
|
+
simpleHash(str) {
|
|
969
|
+
return hashString(str);
|
|
970
|
+
}
|
|
971
|
+
calculateSimilarity(props1, props2) {
|
|
972
|
+
const keys1 = Object.keys(props1);
|
|
973
|
+
const keys2 = Object.keys(props2);
|
|
974
|
+
const allKeys = new Set([...keys1, ...keys2]);
|
|
975
|
+
if (allKeys.size === 0)
|
|
976
|
+
return 0;
|
|
977
|
+
let matches = 0;
|
|
978
|
+
for (const key of allKeys) {
|
|
979
|
+
if (props1[key] === props2[key]) {
|
|
980
|
+
matches++;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return matches / allKeys.size;
|
|
984
|
+
}
|
|
985
|
+
calculateSpecificity(selector) {
|
|
986
|
+
try {
|
|
987
|
+
// Sanitize and normalize selector
|
|
988
|
+
const normalizedSelector = this.normalizeSelectorForAnalysis(selector);
|
|
989
|
+
if (!normalizedSelector) {
|
|
990
|
+
return { ids: 0, classes: 0, elements: 0, depth: 0 };
|
|
991
|
+
}
|
|
992
|
+
const ids = (normalizedSelector.match(/#/g) || []).length;
|
|
993
|
+
const classes = (normalizedSelector.match(/\./g) || []).length;
|
|
994
|
+
const elements = (normalizedSelector.match(/^[a-z]+|[\s>+~][a-z]+/gi) || []).length;
|
|
995
|
+
const depth = Math.min(normalizedSelector.split(/[\s>+~]/).length, 100); // Cap depth at 100
|
|
996
|
+
return { ids, classes, elements, depth };
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
// Return safe defaults if parsing fails
|
|
1000
|
+
return { ids: 0, classes: 0, elements: 0, depth: 0 };
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Check if rule is a reusable pattern (Task #44: uses pattern catalog)
|
|
1005
|
+
*/
|
|
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
|
|
1014
|
+
const props = Object.keys(rule.properties);
|
|
1015
|
+
// Button patterns
|
|
1016
|
+
if (props.includes('padding') && props.includes('border-radius') &&
|
|
1017
|
+
(props.includes('background') || props.includes('background-color'))) {
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
// Card patterns
|
|
1021
|
+
if (props.includes('box-shadow') && props.includes('border-radius') &&
|
|
1022
|
+
props.includes('padding')) {
|
|
1023
|
+
return true;
|
|
1024
|
+
}
|
|
1025
|
+
return false;
|
|
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
|
|
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
|
|
1041
|
+
const typographyProps = ['font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing'];
|
|
1042
|
+
const props = Object.keys(rule.properties);
|
|
1043
|
+
const typographyCount = props.filter(p => typographyProps.includes(p)).length;
|
|
1044
|
+
return typographyCount >= 2;
|
|
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
|
|
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
|
|
1058
|
+
const props = Object.keys(rule.properties);
|
|
1059
|
+
const buttonIndicators = ['cursor', 'background', 'border', 'padding', 'border-radius'];
|
|
1060
|
+
const matchCount = props.filter(p => buttonIndicators.some(b => p.includes(b))).length;
|
|
1061
|
+
return matchCount >= 3 && rule.properties['cursor'] === 'pointer';
|
|
1062
|
+
}
|
|
1063
|
+
generateExternalizeExample(rule, componentName) {
|
|
1064
|
+
const className = componentName
|
|
1065
|
+
? `${componentName.toLowerCase()}-style`
|
|
1066
|
+
: 'custom-class';
|
|
1067
|
+
const cssProps = Object.entries(rule.properties)
|
|
1068
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
1069
|
+
.join('\n');
|
|
1070
|
+
return `.${className} {\n${cssProps}\n}`;
|
|
1071
|
+
}
|
|
1072
|
+
getFontWeightName(value) {
|
|
1073
|
+
const weights = {
|
|
1074
|
+
'100': 'thin', '200': 'extralight', '300': 'light',
|
|
1075
|
+
'400': 'normal', '500': 'medium', '600': 'semibold',
|
|
1076
|
+
'700': 'bold', '800': 'extrabold', '900': 'black'
|
|
1077
|
+
};
|
|
1078
|
+
return weights[value] || value;
|
|
1079
|
+
}
|
|
1080
|
+
suggestBEMName(selector) {
|
|
1081
|
+
try {
|
|
1082
|
+
const trimmed = selector.trim();
|
|
1083
|
+
// Handle edge cases
|
|
1084
|
+
if (!trimmed || trimmed.length > 1000) {
|
|
1085
|
+
return selector;
|
|
1086
|
+
}
|
|
1087
|
+
const parts = trimmed.split(/\s+/).filter(p => p.length > 0);
|
|
1088
|
+
if (parts.length >= 2) {
|
|
1089
|
+
const block = parts[0].replace(/[^a-zA-Z0-9_-]/g, '');
|
|
1090
|
+
const element = parts[parts.length - 1].replace(/[^a-zA-Z0-9_-]/g, '');
|
|
1091
|
+
if (block && element) {
|
|
1092
|
+
return `.${block}__${element}`;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return selector;
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
return selector;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
completelyOverrides(newRule, existing) {
|
|
1102
|
+
if (newRule.selector !== existing.selector)
|
|
1103
|
+
return false;
|
|
1104
|
+
const existingProps = Object.keys(existing.properties);
|
|
1105
|
+
const newProps = Object.keys(newRule.properties);
|
|
1106
|
+
return existingProps.every(prop => newProps.includes(prop));
|
|
1107
|
+
}
|
|
1108
|
+
isLikelyUnused(rule) {
|
|
1109
|
+
try {
|
|
1110
|
+
const selector = rule.selector;
|
|
1111
|
+
// Handle edge cases
|
|
1112
|
+
if (!selector || typeof selector !== 'string' || selector.length > 2000) {
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
// Very specific selectors that might be stale
|
|
1116
|
+
if (selector.includes('[data-v-') || selector.includes('[_ngcontent-')) {
|
|
1117
|
+
return true; // Likely framework-generated and orphaned
|
|
1118
|
+
}
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
prioritizeSuggestions(suggestions) {
|
|
1126
|
+
const severityOrder = { error: 0, warning: 1, info: 2, suggestion: 3 };
|
|
1127
|
+
return suggestions.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
1128
|
+
}
|
|
1129
|
+
generateSummary(_rule, suggestions, duplicates, shouldExternalize, shouldMakeGlobal) {
|
|
1130
|
+
const parts = [];
|
|
1131
|
+
if (duplicates.length > 0) {
|
|
1132
|
+
parts.push(`Found ${duplicates.length} duplicate rule(s)`);
|
|
1133
|
+
}
|
|
1134
|
+
if (shouldExternalize) {
|
|
1135
|
+
parts.push('Should be moved to external stylesheet');
|
|
1136
|
+
}
|
|
1137
|
+
if (shouldMakeGlobal) {
|
|
1138
|
+
parts.push('Consider making global');
|
|
1139
|
+
}
|
|
1140
|
+
const errors = suggestions.filter(s => s.severity === 'error').length;
|
|
1141
|
+
const warnings = suggestions.filter(s => s.severity === 'warning').length;
|
|
1142
|
+
if (errors > 0) {
|
|
1143
|
+
parts.push(`${errors} error(s)`);
|
|
1144
|
+
}
|
|
1145
|
+
if (warnings > 0) {
|
|
1146
|
+
parts.push(`${warnings} warning(s)`);
|
|
1147
|
+
}
|
|
1148
|
+
if (parts.length === 0) {
|
|
1149
|
+
return 'CSS rule looks good';
|
|
1150
|
+
}
|
|
1151
|
+
return parts.join('. ') + '.';
|
|
1152
|
+
}
|
|
1153
|
+
initializeKnownPatterns() {
|
|
1154
|
+
// Add common utility classes
|
|
1155
|
+
const utilities = [
|
|
1156
|
+
'flex', 'grid', 'hidden', 'block', 'inline', 'inline-block',
|
|
1157
|
+
'relative', 'absolute', 'fixed', 'sticky',
|
|
1158
|
+
'text-center', 'text-left', 'text-right',
|
|
1159
|
+
'font-bold', 'font-normal', 'font-medium'
|
|
1160
|
+
];
|
|
1161
|
+
utilities.forEach(u => this.knownUtilityClasses.add(u));
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Map pattern catalog variable types to design token categories
|
|
1165
|
+
*/
|
|
1166
|
+
mapTypeToTokenCategory(candidateType) {
|
|
1167
|
+
const mapping = {
|
|
1168
|
+
'color': 'color',
|
|
1169
|
+
'spacing': 'spacing',
|
|
1170
|
+
'font-size': 'font-size',
|
|
1171
|
+
'font-weight': 'color', // No direct match in token categories
|
|
1172
|
+
'duration': 'transition',
|
|
1173
|
+
'easing': 'transition'
|
|
1174
|
+
};
|
|
1175
|
+
return mapping[candidateType] || null;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// Export singleton
|
|
1179
|
+
export const cssAnalyzer = new CSSAnalyzer();
|
|
1180
|
+
//# sourceMappingURL=CSSAnalyzer.js.map
|