code-auditor-mcp 3.4.11 → 3.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +119 -1
  3. package/dist/analyzers/__verify_self_probe.d.ts +2 -0
  4. package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
  5. package/dist/analyzers/__verify_self_probe.js +5 -0
  6. package/dist/analyzers/__verify_self_probe.js.map +1 -0
  7. package/dist/analyzers/analyzerUtils.d.ts +34 -1
  8. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  9. package/dist/analyzers/analyzerUtils.js +61 -22
  10. package/dist/analyzers/analyzerUtils.js.map +1 -1
  11. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  13. package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
  14. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  15. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
  16. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  17. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
  18. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  19. package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
  20. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  21. package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
  22. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  23. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
  24. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  25. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
  26. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  27. package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
  28. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  29. package/dist/analyzers/documentationAnalyzer.js +259 -198
  30. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  31. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  32. package/dist/analyzers/invariantsAnalyzer.js +59 -46
  33. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  34. package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
  35. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
  36. package/dist/analyzers/orm/adapterRegistry.js +10 -0
  37. package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
  38. package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
  39. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
  40. package/dist/analyzers/orm/drizzleAdapter.js +42 -44
  41. package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
  42. package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
  43. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
  44. package/dist/analyzers/orm/prismaAdapter.js +30 -0
  45. package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
  46. package/dist/analyzers/provenance.d.ts +72 -2
  47. package/dist/analyzers/provenance.d.ts.map +1 -1
  48. package/dist/analyzers/provenance.js +462 -338
  49. package/dist/analyzers/provenance.js.map +1 -1
  50. package/dist/analyzers/reactAnalyzer.d.ts +11 -0
  51. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  52. package/dist/analyzers/reactAnalyzer.js +188 -125
  53. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  54. package/dist/analyzers/ruleRegistry.d.ts +118 -1
  55. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  56. package/dist/analyzers/ruleRegistry.js +1717 -104
  57. package/dist/analyzers/ruleRegistry.js.map +1 -1
  58. package/dist/analyzers/ruleTiming.d.ts +54 -0
  59. package/dist/analyzers/ruleTiming.d.ts.map +1 -0
  60. package/dist/analyzers/ruleTiming.js +71 -0
  61. package/dist/analyzers/ruleTiming.js.map +1 -0
  62. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
  63. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  64. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
  65. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  66. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
  67. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  68. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
  69. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  70. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +24 -186
  71. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  72. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1081 -1061
  73. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  74. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
  75. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  76. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
  77. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  78. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
  79. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  80. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
  81. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  82. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -212
  83. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  84. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1518
  85. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  86. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
  87. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  88. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -472
  89. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  90. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
  91. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
  92. package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
  93. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
  94. package/dist/analyzers/universal/schema/config.d.ts +31 -0
  95. package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
  96. package/dist/analyzers/universal/schema/config.js +57 -0
  97. package/dist/analyzers/universal/schema/config.js.map +1 -0
  98. package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
  99. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
  100. package/dist/analyzers/universal/schema/discovery.js +616 -0
  101. package/dist/analyzers/universal/schema/discovery.js.map +1 -0
  102. package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
  103. package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
  104. package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
  105. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
  106. package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
  107. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
  108. package/dist/analyzers/universal/schema/migrations.js +238 -0
  109. package/dist/analyzers/universal/schema/migrations.js.map +1 -0
  110. package/dist/analyzers/universal/schema/types.d.ts +131 -0
  111. package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
  112. package/dist/analyzers/universal/schema/types.js +10 -0
  113. package/dist/analyzers/universal/schema/types.js.map +1 -0
  114. package/dist/analyzers/universal/schema/violations.d.ts +44 -0
  115. package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
  116. package/dist/analyzers/universal/schema/violations.js +54 -0
  117. package/dist/analyzers/universal/schema/violations.js.map +1 -0
  118. package/dist/auditRunner.d.ts.map +1 -1
  119. package/dist/auditRunner.js +101 -39
  120. package/dist/auditRunner.js.map +1 -1
  121. package/dist/cli.js +348 -40
  122. package/dist/cli.js.map +1 -1
  123. package/dist/config/configLoader.d.ts.map +1 -1
  124. package/dist/config/configLoader.js +9 -7
  125. package/dist/config/configLoader.js.map +1 -1
  126. package/dist/config/defaults.d.ts +12 -3
  127. package/dist/config/defaults.d.ts.map +1 -1
  128. package/dist/config/defaults.js +15 -5
  129. package/dist/config/defaults.js.map +1 -1
  130. package/dist/config/effectiveConfig.d.ts +75 -0
  131. package/dist/config/effectiveConfig.d.ts.map +1 -0
  132. package/dist/config/effectiveConfig.js +213 -0
  133. package/dist/config/effectiveConfig.js.map +1 -0
  134. package/dist/config/pathProfiles.d.ts +7 -4
  135. package/dist/config/pathProfiles.d.ts.map +1 -1
  136. package/dist/config/pathProfiles.js +8 -10
  137. package/dist/config/pathProfiles.js.map +1 -1
  138. package/dist/config/thresholdRationales.d.ts +45 -0
  139. package/dist/config/thresholdRationales.d.ts.map +1 -0
  140. package/dist/config/thresholdRationales.js +67 -0
  141. package/dist/config/thresholdRationales.js.map +1 -0
  142. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  143. package/dist/conventions/conventionMiner.js +6 -2
  144. package/dist/conventions/conventionMiner.js.map +1 -1
  145. package/dist/enforcement/diffGate.d.ts +55 -0
  146. package/dist/enforcement/diffGate.d.ts.map +1 -0
  147. package/dist/enforcement/diffGate.js +136 -0
  148. package/dist/enforcement/diffGate.js.map +1 -0
  149. package/dist/enforcement/gate.d.ts +36 -0
  150. package/dist/enforcement/gate.d.ts.map +1 -0
  151. package/dist/enforcement/gate.js +72 -0
  152. package/dist/enforcement/gate.js.map +1 -0
  153. package/dist/enforcement/suppressions.d.ts +41 -0
  154. package/dist/enforcement/suppressions.d.ts.map +1 -0
  155. package/dist/enforcement/suppressions.js +170 -0
  156. package/dist/enforcement/suppressions.js.map +1 -0
  157. package/dist/fingerprint.d.ts.map +1 -1
  158. package/dist/fingerprint.js +4 -1
  159. package/dist/fingerprint.js.map +1 -1
  160. package/dist/grammars/tree-sitter-scss.wasm +0 -0
  161. package/dist/invariants/ruleEngine.d.ts +3 -4
  162. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  163. package/dist/invariants/ruleEngine.js +2 -2
  164. package/dist/invariants/ruleEngine.js.map +1 -1
  165. package/dist/languages/LanguageOrchestrator.d.ts +14 -0
  166. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  167. package/dist/languages/LanguageOrchestrator.js +93 -55
  168. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  169. package/dist/languages/LanguageRegistry.d.ts +8 -0
  170. package/dist/languages/LanguageRegistry.d.ts.map +1 -1
  171. package/dist/languages/LanguageRegistry.js +8 -0
  172. package/dist/languages/LanguageRegistry.js.map +1 -1
  173. package/dist/languages/RuntimeManager.d.ts +82 -60
  174. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  175. package/dist/languages/RuntimeManager.js +404 -349
  176. package/dist/languages/RuntimeManager.js.map +1 -1
  177. package/dist/languages/UniversalAnalyzer.d.ts +51 -6
  178. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  179. package/dist/languages/UniversalAnalyzer.js +127 -88
  180. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  181. package/dist/languages/adapterBridge.d.ts +36 -0
  182. package/dist/languages/adapterBridge.d.ts.map +1 -1
  183. package/dist/languages/adapterBridge.js +89 -35
  184. package/dist/languages/adapterBridge.js.map +1 -1
  185. package/dist/languages/go/GoAdapter.d.ts +66 -23
  186. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  187. package/dist/languages/go/GoAdapter.js +553 -474
  188. package/dist/languages/go/GoAdapter.js.map +1 -1
  189. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +38 -9
  190. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
  191. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +111 -74
  192. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  193. package/dist/languages/tree-sitter/converter.d.ts +18 -0
  194. package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
  195. package/dist/languages/tree-sitter/converter.js +18 -0
  196. package/dist/languages/tree-sitter/converter.js.map +1 -1
  197. package/dist/languages/tree-sitter/parser.d.ts +39 -2
  198. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  199. package/dist/languages/tree-sitter/parser.js +95 -12
  200. package/dist/languages/tree-sitter/parser.js.map +1 -1
  201. package/dist/languages/types.d.ts +67 -2
  202. package/dist/languages/types.d.ts.map +1 -1
  203. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -62
  204. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  205. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1461 -791
  206. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  207. package/dist/mcp-tools/whitelistTools.d.ts +1 -1
  208. package/dist/mcp-tools-shared.d.ts.map +1 -1
  209. package/dist/mcp-tools-shared.js +1 -0
  210. package/dist/mcp-tools-shared.js.map +1 -1
  211. package/dist/mcpAuditJobs.d.ts.map +1 -1
  212. package/dist/mcpAuditJobs.js +1 -0
  213. package/dist/mcpAuditJobs.js.map +1 -1
  214. package/dist/pipeline.d.ts +30 -1
  215. package/dist/pipeline.d.ts.map +1 -1
  216. package/dist/pipeline.js +527 -203
  217. package/dist/pipeline.js.map +1 -1
  218. package/dist/pipelineAdapters.d.ts +5 -12
  219. package/dist/pipelineAdapters.d.ts.map +1 -1
  220. package/dist/pipelineAdapters.js +175 -100
  221. package/dist/pipelineAdapters.js.map +1 -1
  222. package/dist/presets/presets.d.ts +54 -0
  223. package/dist/presets/presets.d.ts.map +1 -0
  224. package/dist/presets/presets.js +181 -0
  225. package/dist/presets/presets.js.map +1 -0
  226. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  227. package/dist/reporting/sarifReportGenerator.js +0 -2
  228. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  229. package/dist/ruleAliases.d.ts +67 -0
  230. package/dist/ruleAliases.d.ts.map +1 -0
  231. package/dist/ruleAliases.js +79 -0
  232. package/dist/ruleAliases.js.map +1 -0
  233. package/dist/styles/cssAstExtractor.d.ts +20 -5
  234. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  235. package/dist/styles/cssAstExtractor.js +247 -9
  236. package/dist/styles/cssAstExtractor.js.map +1 -1
  237. package/dist/styles/styleIndexer.js +13 -4
  238. package/dist/styles/styleIndexer.js.map +1 -1
  239. package/dist/styles/tailwindConfigLoader.d.ts +8 -6
  240. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  241. package/dist/styles/tailwindConfigLoader.js +61 -15
  242. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  243. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  244. package/dist/styles/tailwindUtilityExpander.js +13 -1
  245. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  246. package/dist/types.d.ts +201 -1
  247. package/dist/types.d.ts.map +1 -1
  248. package/dist/types.js +8 -0
  249. package/dist/types.js.map +1 -1
  250. package/package.json +7 -4
  251. package/plugin/.claude-plugin/plugin.json +1 -1
  252. package/plugin/hooks/hooks.json +1 -1
  253. package/plugin/scripts/hook-audit.sh +25 -7
  254. package/plugin/skills/code-auditor/SKILL.md +5 -5
@@ -10,6 +10,7 @@
10
10
  * compute histograms, clusters, and distributions across the entire codebase.
11
11
  */
12
12
  import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
13
+ import { withRuleTiming } from '../ruleTiming.js';
13
14
  import { makeVisitorStatus } from '../../pipeline.js';
14
15
  import { getTailwindExpander } from '../../styles/tailwindUtilityExpander.js';
15
16
  import { normalizeValue } from '../../styles/normalizer.js';
@@ -46,125 +47,153 @@ const TAILWIND_SPACING_PX = {
46
47
  '96': 384,
47
48
  };
48
49
  const TAILWIND_SCALE_VALUES = Object.values(TAILWIND_SPACING_PX).sort((a, b) => a - b);
49
- // ---------------------------------------------------------------------------
50
- // Analyzer
51
- // ---------------------------------------------------------------------------
52
- export class UniversalStylesAnalyzer extends UniversalAnalyzer {
50
+ /**
51
+ * Values so common that coincidental token-name matches are always noise.
52
+ * Filtered before any token comparison (Spec 22 Item 1).
53
+ */
54
+ const TRIVIAL_VALUES = new Set([
55
+ '0', '0px', '0rem', '0em', '0%', 'none', 'transparent',
56
+ 'inherit', 'initial', 'unset', 'currentcolor', 'auto',
57
+ '100%', '50%',
58
+ ]);
59
+ /**
60
+ * Leaf layer: identity + stateless style helpers used by every detector.
61
+ * Kept free of cross-method orchestration so it stays a small, stable base.
62
+ */
63
+ class UniversalStylesAnalyzerBase extends UniversalAnalyzer {
53
64
  name = 'styles';
54
65
  description = 'Detects style fragmentation, value drift, token bypass, dead classes, ' +
55
66
  'off-scale values, mechanism mixing, declaration-set similarity, and z-index sprawl';
56
67
  category = 'style';
57
- /**
58
- * Override analyze() to query the full style index in one pass instead
59
- * of per-file AST processing. The base class analyze() loop is bypassed.
60
- */
61
- async analyze(files, config = {}, options = {}) {
62
- const startTime = Date.now();
63
- const cfg = { ...DEFAULT_STYLES_CONFIG, ...config };
64
- const violations = [];
65
- // Use the IndexHandle passed through the pipeline; fall back if absent.
66
- const indexHandle = config.indexHandle;
67
- if (!indexHandle) {
68
- return {
69
- violations: [],
70
- errors: [{ file: '', error: 'No index handle available — style index not open' }],
71
- status: makeVisitorStatus(0),
72
- executionTime: Date.now() - startTime,
73
- analyzerName: this.name,
74
- metrics: { filesAnalyzed: 0, totalViolations: 0, executionTime: Date.now() - startTime },
75
- };
76
- }
77
- // Query all declarations, tokens, and class usage
78
- const declarations = this.queryDeclarations(indexHandle);
79
- const tokens = this.queryTokens(indexHandle);
80
- const classUsage = this.queryClassUsage(indexHandle);
81
- if (declarations.length === 0) {
82
- return {
83
- violations: [],
84
- errors: [],
85
- status: makeVisitorStatus(files.length),
86
- executionTime: Date.now() - startTime,
87
- analyzerName: this.name,
88
- metrics: { filesAnalyzed: files.length, totalViolations: 0, executionTime: Date.now() - startTime },
89
- };
90
- }
91
- // Build helpers
92
- const tokenValueMap = new Map(); // normalized value → token info
93
- for (const t of tokens) {
94
- const normalizedTokenVal = normalizeValue(t.value, '__token__');
95
- tokenValueMap.set(t.value, { name: t.name, valueType: normalizedTokenVal?.type ?? null });
96
- }
97
- // Declarations by property
98
- const byProperty = new Map();
99
- for (const d of declarations) {
100
- const list = byProperty.get(d.property) || [];
101
- list.push(d);
102
- byProperty.set(d.property, list);
68
+ makeViolation(filePath, line, message, classification) {
69
+ const v = {
70
+ file: filePath,
71
+ line,
72
+ column: 1,
73
+ severity: classification.severity,
74
+ message,
75
+ rule: classification.rule,
76
+ analyzer: this.name,
77
+ };
78
+ if (classification.symbol) {
79
+ v.functionName = classification.symbol;
103
80
  }
104
- // Run detectors
105
- violations.push(...this.detectValueDrift(byProperty, cfg, declarations));
106
- violations.push(...this.detectOffScaleValues(byProperty, cfg));
107
- violations.push(...await this.detectUndefinedClasses(classUsage, declarations, byProperty, cfg));
108
- violations.push(...this.detectTokenBypass(declarations, tokenValueMap, cfg));
109
- violations.push(...this.detectMechanismFragmentation(declarations, cfg));
110
- violations.push(...this.detectDeclarationSetSimilarity(declarations, cfg));
111
- violations.push(...this.detectZIndexInventory(byProperty, cfg));
112
- // Apply severity overrides from config
113
- const severityOverrides = config.severityOverrides ?? {};
114
- if (Object.keys(severityOverrides).length > 0) {
115
- for (const v of violations) {
116
- const override = severityOverrides[v.rule];
117
- if (override) {
118
- v.severity = override;
119
- }
120
- }
81
+ if (classification.resolution) {
82
+ v.resolution = classification.resolution;
121
83
  }
122
- const filtered = violations.filter(v => v.severity !== 'off');
123
- const uniqueFiles = new Set(declarations.map(d => d.file_path));
124
- return {
125
- violations: filtered,
126
- errors: [],
127
- status: makeVisitorStatus(uniqueFiles.size),
128
- executionTime: Date.now() - startTime,
129
- analyzerName: this.name,
130
- metrics: {
131
- filesAnalyzed: uniqueFiles.size,
132
- totalViolations: filtered.length,
133
- executionTime: Date.now() - startTime,
134
- },
135
- };
136
- }
137
- /** Not used — we override analyze() directly. */
138
- async analyzeAST(_ast, _adapter, _config, _sourceCode) {
139
- return [];
84
+ return v;
140
85
  }
141
- // -----------------------------------------------------------------------
142
- // Database queries
143
- // -----------------------------------------------------------------------
144
- queryDeclarations(indexHandle) {
86
+ /** Parse a CSS color string to [R, G, B] or null. */
87
+ parseColorToRGB(raw) {
145
88
  try {
146
- return indexHandle.query('SELECT * FROM style_declarations ORDER BY property, file_path, line');
89
+ let v = raw.toLowerCase().trim();
90
+ // Hex
91
+ if (v.startsWith('#')) {
92
+ if (v.length === 4) {
93
+ // #rgb → #rrggbb
94
+ v = '#' + v[1] + v[1] + v[2] + v[2] + v[3] + v[3];
95
+ }
96
+ if (v.length === 7) {
97
+ return [
98
+ parseInt(v.slice(1, 3), 16),
99
+ parseInt(v.slice(3, 5), 16),
100
+ parseInt(v.slice(5, 7), 16),
101
+ ];
102
+ }
103
+ if (v.length === 9) {
104
+ return [
105
+ parseInt(v.slice(1, 3), 16),
106
+ parseInt(v.slice(3, 5), 16),
107
+ parseInt(v.slice(5, 7), 16),
108
+ ];
109
+ }
110
+ }
111
+ // rgb(r, g, b) or rgb(r g b)
112
+ const rgbMatch = v.match(/rgb\(\s*(\d+)\s*,?\s*(\d+)\s*,?\s*(\d+)\s*\)/);
113
+ if (rgbMatch) {
114
+ return [
115
+ parseInt(rgbMatch[1]),
116
+ parseInt(rgbMatch[2]),
117
+ parseInt(rgbMatch[3]),
118
+ ];
119
+ }
120
+ // Named colors — minimal set for common use
121
+ const named = {
122
+ 'white': [255, 255, 255], 'black': [0, 0, 0],
123
+ 'red': [255, 0, 0], 'blue': [0, 0, 255], 'green': [0, 128, 0],
124
+ 'transparent': [0, 0, 0],
125
+ };
126
+ if (named[v])
127
+ return named[v];
128
+ return null;
147
129
  }
148
130
  catch {
149
- return [];
131
+ return null;
150
132
  }
151
133
  }
152
- queryTokens(indexHandle) {
153
- try {
154
- return indexHandle.query('SELECT * FROM style_tokens');
155
- }
156
- catch {
157
- return [];
134
+ /** Compute delta-E (CIE76) between two RGB colors. */
135
+ deltaE(a, b) {
136
+ const dr = a[0] - b[0];
137
+ const dg = a[1] - b[1];
138
+ const db = a[2] - b[2];
139
+ return Math.sqrt(dr * dr + dg * dg + db * db);
140
+ }
141
+ /**
142
+ * Cluster colors by delta-E distance.
143
+ * Simple greedy algorithm: each item joins the first cluster it's close enough to,
144
+ * or starts a new cluster.
145
+ */
146
+ clusterByDeltaE(items, threshold) {
147
+ const clusters = [];
148
+ for (const item of items) {
149
+ let placed = false;
150
+ for (const cluster of clusters) {
151
+ // Use the first item's RGB as cluster centroid
152
+ const centroid = cluster[0].rgb;
153
+ if (this.deltaE(item.rgb, centroid) < threshold) {
154
+ cluster.push(item);
155
+ placed = true;
156
+ break;
157
+ }
158
+ }
159
+ if (!placed) {
160
+ clusters.push([item]);
161
+ }
158
162
  }
163
+ return clusters;
159
164
  }
160
- queryClassUsage(indexHandle) {
165
+ /** Parse a CSS length value to px-equivalent, or null if not parseable. */
166
+ parseLengthToPx(raw) {
161
167
  try {
162
- return indexHandle.query('SELECT * FROM style_class_usage');
168
+ const v = raw.trim().toLowerCase();
169
+ if (v === '0' || v === '0px')
170
+ return 0;
171
+ const match = v.match(/^(-?\d+(?:\.\d+)?)\s*(px|rem|em|%|vh|vw|pt|cm|mm)?$/);
172
+ if (!match)
173
+ return null;
174
+ const num = parseFloat(match[1]);
175
+ const unit = match[2] || 'px';
176
+ // Approximate conversions (assuming 16px base for rem/em)
177
+ switch (unit) {
178
+ case 'px': return num;
179
+ case 'rem': return num * 16;
180
+ case 'em': return num * 16;
181
+ case 'pt': return num * 1.333;
182
+ case 'cm': return num * 37.795;
183
+ case 'mm': return num * 3.7795;
184
+ default: return null; // can't convert %/vh/vw without context
185
+ }
163
186
  }
164
187
  catch {
165
- return [];
188
+ return null;
166
189
  }
167
190
  }
191
+ }
192
+ // ---------------------------------------------------------------------------
193
+ // Detector layer: the seven style detectors, reporting through the base
194
+ // layer's makeViolation helper.
195
+ // ---------------------------------------------------------------------------
196
+ class UniversalStylesAnalyzerDetectors extends UniversalStylesAnalyzerBase {
168
197
  // -----------------------------------------------------------------------
169
198
  // Detector 1: Value Drift
170
199
  // -----------------------------------------------------------------------
@@ -251,33 +280,13 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
251
280
  }
252
281
  if (colors.length < cfg.minCorpus)
253
282
  return violations;
254
- // Cluster by delta-E
283
+ // Cluster by delta-E, then flag stragglers in non-dominant clusters.
255
284
  const clusters = this.clusterByDeltaE(colors, cfg.colorDeltaE);
256
- // Find the dominant cluster (largest)
257
285
  clusters.sort((a, b) => b.length - a.length);
258
286
  const dominant = clusters[0];
259
287
  if (!dominant || dominant.length < cfg.modeMinCount)
260
288
  return violations;
261
- const dominantSize = dominant.length;
262
- // Flag stragglers in non-dominant clusters
263
- for (let i = 1; i < clusters.length; i++) {
264
- const cluster = clusters[i];
265
- const share = cluster.length / colors.length;
266
- if (share < cfg.outlierMaxShare && dominantSize >= cfg.modeMinCount) {
267
- // Report once per distinct color value in the straggler cluster
268
- const seenValues = new Set();
269
- for (const item of cluster) {
270
- const normVal = item.decl.raw_value.toLowerCase();
271
- if (seenValues.has(normVal))
272
- continue;
273
- seenValues.add(normVal);
274
- violations.push(this.makeViolation(item.decl.file_path, item.decl.line, `Color drift in "${property}": "${item.decl.raw_value}" is a rare value ` +
275
- `(used ${cluster.length} time${cluster.length === 1 ? '' : 's'}, ` +
276
- `${(share * 100).toFixed(1)}% of ${colors.length} usages). ` +
277
- `Dominant cluster has ${dominantSize} values. Consider using a design token.`, 'warning', 'styles/value-drift', 'color'));
278
- }
279
- }
280
- }
289
+ violations.push(...flagColorDriftStragglers(clusters, property, cfg, this.makeViolation.bind(this)));
281
290
  return violations;
282
291
  }
283
292
  /**
@@ -315,7 +324,7 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
315
324
  violations.push(this.makeViolation(sample.file_path, sample.line, `Value drift in "${property}": "${sample.raw_value}" is rare ` +
316
325
  `(${list.length} of ${total} usages, ${(share * 100).toFixed(1)}%). ` +
317
326
  `The dominant value "${modeKey}" is used ${modeCount} times. ` +
318
- `Consider using a consistent value or design token.`, 'warning', 'styles/value-drift', 'exact'));
327
+ `Consider using a consistent value or design token.`, { severity: 'warning', rule: 'styles/value-drift', symbol: 'exact' }));
319
328
  }
320
329
  }
321
330
  return violations;
@@ -356,7 +365,7 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
356
365
  violations.push(this.makeViolation(decl.file_path, decl.line, `Off-scale "${property}" value: "${decl.raw_value}" (${px}px) ` +
357
366
  `does not align with the inferred ${step}px scale step. ` +
358
367
  `Nearby scale values: ${Math.floor(px / step) * step}px or ` +
359
- `${Math.ceil(px / step) * step}px.`, 'warning', 'styles/off-scale'));
368
+ `${Math.ceil(px / step) * step}px.`, { severity: 'warning', rule: 'styles/off-scale' }));
360
369
  }
361
370
  }
362
371
  }
@@ -389,6 +398,382 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
389
398
  return null;
390
399
  return bestStep;
391
400
  }
401
+ }
402
+ // ---------------------------------------------------------------------------
403
+ // Structure detectors: class/token/mechanism integrity checks over the raw
404
+ // declaration and class-usage rows. Split out of the value-drift detectors so
405
+ // neither class trips the aggregate-complexity ceiling. Lives off the analyzer
406
+ // inheritance chain — it reports through the shared factory and receives the
407
+ // analyzer name once at construction.
408
+ // ---------------------------------------------------------------------------
409
+ /**
410
+ * Filter class-usage rows down to candidate names worth batch-probing,
411
+ * applying the skip filters that distinguish definitions, known classes, and
412
+ * extraction artifacts from genuine consumptions of an unknown class.
413
+ */
414
+ function collectUndefinedClassCandidates(classUsage, definedClasses) {
415
+ const seen = new Set();
416
+ const candidates = [];
417
+ const usageEntries = [];
418
+ for (const u of classUsage) {
419
+ const key = `${u.class_name}::${u.file_path}`;
420
+ if (seen.has(key))
421
+ continue;
422
+ seen.add(key);
423
+ // Skip unresolvable individual class usages
424
+ if (u.unresolvable)
425
+ continue;
426
+ // Skip CSS class SELECTORS (definitions, not usages).
427
+ // CSS/SCSS files: mechanism 'class' = class_selector node = ".some-class { }"
428
+ // These DEFINE a class — they are not usages of one. Only usages through
429
+ // other mechanisms (className, apply, class in HTML) signal consumption.
430
+ // Without this guard, every CSS selector without a direct declaration
431
+ // (SCSS @include-only blocks, nested rule_sets) is falsely flagged.
432
+ if (u.mechanism === 'class' && /\.(css|scss)$/i.test(u.file_path))
433
+ continue;
434
+ // Skip known classes from CSS declarations
435
+ if (definedClasses.has(u.class_name))
436
+ continue;
437
+ // Skip PascalCase — likely a component
438
+ if (/^[A-Z]/.test(u.class_name))
439
+ continue;
440
+ // Skip function-like
441
+ if (u.class_name.includes('('))
442
+ continue;
443
+ // Skip numeric
444
+ if (/^\d/.test(u.class_name))
445
+ continue;
446
+ // Skip classes that start/end with [] — not valid CSS class names
447
+ if (u.class_name.startsWith('[') || u.class_name.startsWith(']'))
448
+ continue;
449
+ if (u.class_name.endsWith('[') || u.class_name.endsWith(']'))
450
+ continue;
451
+ // Skip extraction artifacts
452
+ if (/['`"${}?;!@#%^&*+=<>|\\,~]/.test(u.class_name))
453
+ continue;
454
+ usageEntries.push(u);
455
+ candidates.push(u.class_name);
456
+ }
457
+ return { candidates, usageEntries };
458
+ }
459
+ function flagColorDriftStragglers(clusters, property, cfg, report) {
460
+ const violations = [];
461
+ const dominant = clusters[0];
462
+ const dominantSize = dominant.length;
463
+ const total = clusters.reduce((sum, c) => sum + c.length, 0);
464
+ for (let i = 1; i < clusters.length; i++) {
465
+ const cluster = clusters[i];
466
+ const share = cluster.length / total;
467
+ if (share >= cfg.outlierMaxShare)
468
+ continue;
469
+ // Report once per distinct color value in the straggler cluster
470
+ const seenValues = new Set();
471
+ for (const item of cluster) {
472
+ const normVal = item.decl.raw_value.toLowerCase();
473
+ if (seenValues.has(normVal))
474
+ continue;
475
+ seenValues.add(normVal);
476
+ violations.push(report(item.decl.file_path, item.decl.line, `Color drift in "${property}": "${item.decl.raw_value}" is a rare value ` +
477
+ `(used ${cluster.length} time${cluster.length === 1 ? '' : 's'}, ` +
478
+ `${(share * 100).toFixed(1)}% of ${total} usages). ` +
479
+ `Dominant cluster has ${dominantSize} values. Consider using a design token.`, { severity: 'warning', rule: 'styles/value-drift', symbol: 'color' }));
480
+ }
481
+ }
482
+ return violations;
483
+ }
484
+ /**
485
+ * Collect the set of defined class names and the stylesheet that first defines
486
+ * each, in a single pass over the declarations. Bundled as one catalog so the
487
+ * undefined-class detector (Spec 37 R1: "nearest defined class by edit
488
+ * distance, and which stylesheet defines it") takes a single context object
489
+ * rather than threading the name set and location map as two separate
490
+ * parameters.
491
+ */
492
+ function collectDefinedClassCatalog(byProperty) {
493
+ const names = new Set();
494
+ const locations = new Map();
495
+ for (const [, decls] of byProperty) {
496
+ for (const d of decls) {
497
+ const ctx = d.context;
498
+ if (!ctx)
499
+ continue;
500
+ const matches = ctx.matchAll(/\.([a-zA-Z0-9_-]+)/g);
501
+ for (const m of matches) {
502
+ names.add(m[1]);
503
+ if (!locations.has(m[1]))
504
+ locations.set(m[1], d.file_path);
505
+ }
506
+ }
507
+ }
508
+ return { names, locations };
509
+ }
510
+ /**
511
+ * Levenshtein edit distance between two class names, capping the DP width so a
512
+ * large stylesheet catalog stays cheap. Returns Infinity when the length gap
513
+ * alone exceeds the match ceiling (mirrors schema/codeAnalysis.ts).
514
+ */
515
+ function levenshteinDistance(a, b, maxDist) {
516
+ if (Math.abs(a.length - b.length) > maxDist)
517
+ return Infinity;
518
+ const m = a.length;
519
+ const n = b.length;
520
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
521
+ for (let i = 0; i <= m; i++)
522
+ dp[i][0] = i;
523
+ for (let j = 0; j <= n; j++)
524
+ dp[0][j] = j;
525
+ for (let i = 1; i <= m; i++) {
526
+ for (let j = 1; j <= n; j++) {
527
+ dp[i][j] = a[i - 1] === b[j - 1]
528
+ ? dp[i - 1][j - 1]
529
+ : 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
530
+ }
531
+ }
532
+ return dp[m][n];
533
+ }
534
+ /**
535
+ * Nearest defined class within a bounded edit distance, or null when no known
536
+ * class is close enough to be a credible suggestion.
537
+ */
538
+ function nearestDefinedClass(name, definedClasses) {
539
+ let best = null;
540
+ let bestDist = Infinity;
541
+ for (const known of definedClasses) {
542
+ const dist = levenshteinDistance(name.toLowerCase(), known.toLowerCase(), 3);
543
+ if (dist < bestDist) {
544
+ bestDist = dist;
545
+ best = known;
546
+ }
547
+ }
548
+ return best;
549
+ }
550
+ async function initTailwindProbe(expander, cfg, report) {
551
+ await expander.init({
552
+ projectRoot: cfg?.projectRoot,
553
+ useProjectConfig: true,
554
+ customClasses: cfg?.tailwindClasses ? new Set(cfg.tailwindClasses) : undefined,
555
+ });
556
+ // Fail-open rule (Spec 22 R1.3): if the probe fails and Tailwind IS
557
+ // present, disable the undefined-class detector.
558
+ if (expander.configFailed && expander.hasTailwindConfig) {
559
+ return [report('', 0, `Tailwind probe unavailable (${expander.configFailureReason ?? 'unknown error'}) — ` +
560
+ `undefined-class detection skipped. Classes defined only in Tailwind ` +
561
+ `config will not be checked. Install tailwindcss in the project ` +
562
+ `for full class validation.`, { severity: 'suggestion', rule: 'styles/undefined-class-disabled' })];
563
+ }
564
+ return null;
565
+ }
566
+ function flagUnresolvedClasses(usageEntries, expander, report, catalog) {
567
+ return withRuleTiming('styles/undefined-class', () => {
568
+ const { names: definedClasses, locations: definedClassLocations } = catalog;
569
+ const violations = [];
570
+ for (const u of usageEntries) {
571
+ const resolved = expander.resolve(u.class_name);
572
+ if (resolved.valid)
573
+ continue;
574
+ const nearest = nearestDefinedClass(u.class_name, definedClasses);
575
+ const nearestFile = nearest ? definedClassLocations.get(nearest) : undefined;
576
+ violations.push(report(u.file_path, u.line, `Undefined CSS class: "${u.class_name}" has no matching definition ` +
577
+ `in any stylesheet, Tailwind utility set, or project config.`, {
578
+ severity: 'warning',
579
+ rule: 'styles/undefined-class',
580
+ resolution: {
581
+ action: nearest ? 'use-defined-class' : 'define-or-remove-class',
582
+ summary: nearest
583
+ ? `Rename "${u.class_name}" to the defined class "${nearest}"${nearestFile ? ` (defined in ${nearestFile})` : ''}.`
584
+ : `Define the class "${u.class_name}" in a stylesheet, or remove the usage.`,
585
+ symbols: nearest ? [nearest] : [u.class_name],
586
+ files: nearestFile ? [u.file_path, nearestFile] : [u.file_path],
587
+ lines: [u.line],
588
+ },
589
+ }));
590
+ }
591
+ return violations;
592
+ });
593
+ }
594
+ function normalizeForTokenMatch(raw) {
595
+ let v = raw.toLowerCase().trim();
596
+ v = v.replace(/,\s+/g, ',');
597
+ if (/^#[0-9a-f]{3}$/.test(v)) {
598
+ v = '#' + v[1] + v[1] + v[2] + v[2] + v[3] + v[3];
599
+ }
600
+ return v;
601
+ }
602
+ function parseNormalizedValueType(normalizedValue) {
603
+ if (!normalizedValue)
604
+ return null;
605
+ try {
606
+ const nv = JSON.parse(normalizedValue);
607
+ return nv.type;
608
+ }
609
+ catch {
610
+ return null;
611
+ }
612
+ }
613
+ function matchBypassToken(d, tokenValueMap, categoricalExclusions, scaleProps) {
614
+ if (d.token_ref)
615
+ return null;
616
+ if (d.property.startsWith('--') || d.property.startsWith('$'))
617
+ return null;
618
+ if (categoricalExclusions.has(d.property))
619
+ return null;
620
+ if (scaleProps.has(d.property))
621
+ return null;
622
+ const normalized = normalizeForTokenMatch(d.raw_value);
623
+ if (TRIVIAL_VALUES.has(normalized))
624
+ return null;
625
+ const tokenInfo = tokenValueMap.get(normalized);
626
+ if (!tokenInfo)
627
+ return null;
628
+ if (tokenInfo.valueType !== null) {
629
+ const declType = parseNormalizedValueType(d.normalized_value);
630
+ if (declType !== null && declType !== tokenInfo.valueType)
631
+ return null;
632
+ }
633
+ if (tokenInfo.valueType !== 'color')
634
+ return null;
635
+ return tokenInfo;
636
+ }
637
+ function flagPropertyValueFragmentation(declarations, minMechanisms, report) {
638
+ const violations = [];
639
+ const pvMap = new Map();
640
+ const pvSample = new Map();
641
+ for (const d of declarations) {
642
+ const key = `${d.property}::${d.normalized_value ?? d.raw_value}`;
643
+ const mechs = pvMap.get(key) || new Set();
644
+ mechs.add(d.mechanism);
645
+ pvMap.set(key, mechs);
646
+ if (!pvSample.has(key)) {
647
+ pvSample.set(key, d);
648
+ }
649
+ }
650
+ for (const [key, mechs] of pvMap) {
651
+ if (mechs.size < minMechanisms)
652
+ continue;
653
+ const sample = pvSample.get(key);
654
+ const [prop, value] = key.split('::');
655
+ violations.push(report(sample.file_path, sample.line, `Mechanism fragmentation: "${prop}: ${value}" is applied via ` +
656
+ `${mechs.size} different mechanisms (${[...mechs].sort().join(', ')}). ` +
657
+ `Consolidate to a single mechanism or design token.`, { severity: 'warning', rule: 'styles/mechanism-fragmentation' }));
658
+ }
659
+ return violations;
660
+ }
661
+ function flagFileMechanismMixing(declarations, minMechanisms, report) {
662
+ const violations = [];
663
+ const fileMechs = new Map();
664
+ for (const d of declarations) {
665
+ const mechs = fileMechs.get(d.file_path) || new Set();
666
+ mechs.add(d.mechanism);
667
+ fileMechs.set(d.file_path, mechs);
668
+ }
669
+ for (const [file, mechs] of fileMechs) {
670
+ if (mechs.size < minMechanisms)
671
+ continue;
672
+ violations.push(report(file, 1, `Mechanism mixing: ${file} uses ${mechs.size} different style ` +
673
+ `mechanisms (${[...mechs].sort().join(', ')}). ` +
674
+ `Consolidate to fewer mechanisms for maintainability.`, { severity: 'suggestion', rule: 'styles/mechanism-mixing' }));
675
+ }
676
+ return violations;
677
+ }
678
+ function buildDeclarationBlocks(declarations, minDeclarations) {
679
+ const blocks = new Map();
680
+ for (const d of declarations) {
681
+ if (!d.context)
682
+ continue;
683
+ const key = `${d.file_path}::${d.context}`;
684
+ const list = blocks.get(key) || [];
685
+ list.push(d);
686
+ blocks.set(key, list);
687
+ }
688
+ return [...blocks.entries()]
689
+ .map(([key, decls]) => {
690
+ const valueSet = new Set(decls.map(d => `${d.property}:${d.normalized_value ?? d.raw_value}`));
691
+ return {
692
+ key,
693
+ filePath: decls[0].file_path,
694
+ context: decls[0].context,
695
+ line: decls[0].line,
696
+ declCount: decls.length,
697
+ valueSet,
698
+ };
699
+ })
700
+ .filter(b => b.declCount >= minDeclarations);
701
+ }
702
+ function flagSimilarBlockPairs(blockEntries, threshold, report) {
703
+ const violations = [];
704
+ if (blockEntries.length < 2)
705
+ return violations;
706
+ const reported = new Set();
707
+ for (let i = 0; i < blockEntries.length; i++) {
708
+ for (let j = i + 1; j < blockEntries.length; j++) {
709
+ const a = blockEntries[i];
710
+ const b = blockEntries[j];
711
+ if (a.key === b.key)
712
+ continue;
713
+ const pairKey = [a.key, b.key].sort().join('::');
714
+ if (reported.has(pairKey))
715
+ continue;
716
+ reported.add(pairKey);
717
+ const intersection = new Set([...a.valueSet].filter(x => b.valueSet.has(x)));
718
+ const union = new Set([...a.valueSet, ...b.valueSet]);
719
+ const similarity = intersection.size / union.size;
720
+ if (similarity >= threshold) {
721
+ violations.push(report(a.filePath, a.line, `Declaration-set similarity: "${a.context}" and "${b.context}" ` +
722
+ `in ${b.filePath} share ${intersection.size} of ${union.size} ` +
723
+ `declarations (${(similarity * 100).toFixed(0)}%). ` +
724
+ `Consider consolidating these rules or extracting a shared mixin.`, { severity: 'suggestion', rule: 'styles/declaration-set-similarity' }));
725
+ }
726
+ }
727
+ }
728
+ return violations;
729
+ }
730
+ function buildTokenValueMap(tokens) {
731
+ const tokenValueMap = new Map();
732
+ for (const t of tokens) {
733
+ const normalizedTokenVal = normalizeValue(t.value, '__token__');
734
+ tokenValueMap.set(t.value, { name: t.name, valueType: normalizedTokenVal?.type ?? null });
735
+ }
736
+ return tokenValueMap;
737
+ }
738
+ function groupDeclarationsByProperty(declarations) {
739
+ const byProperty = new Map();
740
+ for (const d of declarations) {
741
+ const list = byProperty.get(d.property) || [];
742
+ list.push(d);
743
+ byProperty.set(d.property, list);
744
+ }
745
+ return byProperty;
746
+ }
747
+ function applySeverityOverrides(violations, config) {
748
+ const severityOverrides = config.severityOverrides ?? {};
749
+ if (Object.keys(severityOverrides).length === 0)
750
+ return;
751
+ for (const v of violations) {
752
+ const override = severityOverrides[v.rule];
753
+ if (override) {
754
+ v.severity = override;
755
+ }
756
+ }
757
+ }
758
+ function buildStylesResult(spec) {
759
+ return {
760
+ violations: spec.violations,
761
+ errors: spec.errors ?? [],
762
+ status: makeVisitorStatus(spec.fileCount),
763
+ executionTime: Date.now() - spec.startTime,
764
+ analyzerName: spec.name,
765
+ metrics: {
766
+ filesAnalyzed: spec.fileCount,
767
+ totalViolations: spec.violations.length,
768
+ executionTime: Date.now() - spec.startTime,
769
+ },
770
+ };
771
+ }
772
+ class StylesStructureDetectors {
773
+ makeViolation;
774
+ constructor(makeViolation) {
775
+ this.makeViolation = makeViolation;
776
+ }
392
777
  // -----------------------------------------------------------------------
393
778
  // Detector 3: Undefined Classes
394
779
  // -----------------------------------------------------------------------
@@ -404,104 +789,24 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
404
789
  */
405
790
  async detectUndefinedClasses(classUsage, _declarations, byProperty, cfg) {
406
791
  const violations = [];
407
- // Collect all defined class names from CSS declarations
408
- const definedClasses = new Set();
409
- for (const [, decls] of byProperty) {
410
- for (const d of decls) {
411
- const ctx = d.context;
412
- if (!ctx)
413
- continue;
414
- const matches = ctx.matchAll(/\.([a-zA-Z0-9_-]+)/g);
415
- for (const m of matches) {
416
- definedClasses.add(m[1]);
417
- }
418
- }
419
- }
420
- // ── Init compile-probe (async) ─────────────────────────────────
792
+ const definedClassCatalog = collectDefinedClassCatalog(byProperty);
421
793
  const expander = getTailwindExpander();
422
- await expander.init({
423
- projectRoot: cfg?.projectRoot,
424
- useProjectConfig: true,
425
- customClasses: cfg?.tailwindClasses ? new Set(cfg.tailwindClasses) : undefined,
426
- });
427
794
  // Fail-open rule (Spec 22 R1.3): if the probe fails and Tailwind IS
428
- // present, disable the undefined-class detector. Without a working probe
429
- // there is no way to validate Tailwind utility classes — flagging every
430
- // utility would produce ~875 false positives.
431
- //
432
- // When Tailwind is absent (no tailwindcss in node_modules), the probe
433
- // never runs and CSS-only detection is the correct fallback.
434
- if (expander.configFailed && expander.hasTailwindConfig) {
435
- violations.push(this.makeViolation('', 0, `Tailwind probe unavailable (${expander.configFailureReason ?? 'unknown error'}) — ` +
436
- `undefined-class detection skipped. Classes defined only in Tailwind ` +
437
- `config will not be checked. Install tailwindcss in the project ` +
438
- `for full class validation.`, 'suggestion', 'styles/undefined-class-disabled'));
439
- return violations;
440
- }
441
- // ── Collect candidate class names for batch probing ─────────────
442
- // Gather all potentially-unknown class names first, then batch-probe
443
- // them against the project's Tailwind compiler. This is more efficient
444
- // than probing one-at-a-time and lets the probe batch in groups.
445
- const seen = new Set();
446
- const candidates = [];
447
- const usageEntries = [];
448
- for (const u of classUsage) {
449
- const key = `${u.class_name}::${u.file_path}`;
450
- if (seen.has(key))
451
- continue;
452
- seen.add(key);
453
- // Skip unresolvable individual class usages
454
- if (u.unresolvable)
455
- continue;
456
- // Skip known classes from CSS definitions
457
- if (definedClasses.has(u.class_name))
458
- continue;
459
- // Skip PascalCase — likely a component
460
- if (/^[A-Z]/.test(u.class_name))
461
- continue;
462
- // Skip function-like
463
- if (u.class_name.includes('('))
464
- continue;
465
- // Skip numeric
466
- if (/^\d/.test(u.class_name))
467
- continue;
468
- // Skip classes that start/end with [] — not valid CSS class names
469
- if (u.class_name.startsWith('[') || u.class_name.startsWith(']'))
470
- continue;
471
- if (u.class_name.endsWith('[') || u.class_name.endsWith(']'))
472
- continue;
473
- // Skip extraction artifacts
474
- if (/['`"${}?;!@#%^&*+=<>|\\,~]/.test(u.class_name))
475
- continue;
476
- usageEntries.push(u);
477
- candidates.push(u.class_name);
478
- }
479
- // ── Batch-probe unknown classes ─────────────────────────────────
795
+ // present, disable the undefined-class detector.
796
+ const failOpen = await initTailwindProbe(expander, cfg, this.makeViolation.bind(this));
797
+ if (failOpen)
798
+ return failOpen;
799
+ // Gather all potentially-unknown class names first, then batch-probe.
800
+ const { candidates, usageEntries } = collectUndefinedClassCandidates(classUsage, definedClassCatalog.names);
480
801
  if (candidates.length > 0 && expander.probeReady) {
481
802
  await expander.validateBatch(candidates);
482
803
  }
483
- // ── Resolve each class from cache + structural patterns ─────────
484
- for (const u of usageEntries) {
485
- const resolved = expander.resolve(u.class_name);
486
- if (resolved.valid)
487
- continue;
488
- violations.push(this.makeViolation(u.file_path, u.line, `Undefined CSS class: "${u.class_name}" has no matching definition ` +
489
- `in any stylesheet, Tailwind utility set, or project config.`, 'warning', 'styles/undefined-class'));
490
- }
804
+ violations.push(...flagUnresolvedClasses(usageEntries, expander, this.makeViolation.bind(this), definedClassCatalog));
491
805
  return violations;
492
806
  }
493
807
  // -----------------------------------------------------------------------
494
808
  // Detector 4: Token Bypass
495
809
  // -----------------------------------------------------------------------
496
- /**
497
- * Spec 22 Item 1 — Values so common that coincidental token-name matches
498
- * are always noise. Filtered before any token comparison.
499
- */
500
- static TRIVIAL_VALUES = new Set([
501
- '0', '0px', '0rem', '0em', '0%', 'none', 'transparent',
502
- 'inherit', 'initial', 'unset', 'currentcolor', 'auto',
503
- '100%', '50%',
504
- ]);
505
810
  /**
506
811
  * Flag raw values that match a known design token's value but don't
507
812
  * reference the token via tokenRef.
@@ -524,74 +829,15 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
524
829
  const categoricalExclusions = new Set(cfg.categoricalPropertyExclusions ?? []);
525
830
  const scaleProps = new Set(cfg.scaleProperties ?? []);
526
831
  for (const d of declarations) {
527
- // Skip if already referencing a token (var(--...) reference)
528
- if (d.token_ref)
529
- continue;
530
- // Skip CSS custom-property definition sites — these define
531
- // token values, so literals are expected (Spec 22 R2.1).
532
- if (d.property.startsWith('--'))
533
- continue;
534
- // Skip SCSS variable definitions — these define token values
535
- // so literals are expected (Spec 22 R2.1).
536
- if (d.property.startsWith('$'))
537
- continue;
538
- // Spec 22 R2.2: skip categorical properties — they have a small set
539
- // of valid keyword values (display, position, etc.) and coincidental
540
- // token-value matches are noise.
541
- if (categoricalExclusions.has(d.property))
542
- continue;
543
- // Spec 22 R2.3: skip scale properties — values naturally vary along
544
- // a scale (font-size, margin, padding, etc.) and raw values at
545
- // different scale points are expected, not token bypasses.
546
- if (scaleProps.has(d.property))
547
- continue;
548
- // Normalize the raw value for comparison
549
- const normalized = this.normalizeForTokenMatch(d.raw_value);
550
- // Spec 22 Item 1: skip trivial values — they're so common that
551
- // coincidental token-name matches are always noise.
552
- if (UniversalStylesAnalyzer.TRIVIAL_VALUES.has(normalized))
553
- continue;
554
- const tokenInfo = tokenValueMap.get(normalized);
832
+ const tokenInfo = matchBypassToken(d, tokenValueMap, categoricalExclusions, scaleProps);
555
833
  if (!tokenInfo)
556
834
  continue;
557
- // Spec 22 Item 1: value-type equality gate — a token only
558
- // matches declarations whose normalized value type is the same.
559
- // E.g., --border-radius-lg: 8px (length) should not flag
560
- // max-width: 8px because the normalizer types them the same, but
561
- // --color-red: #22d3ee (color) should flag color: #22d3ee.
562
- if (tokenInfo.valueType !== null) {
563
- let declType = null;
564
- if (d.normalized_value) {
565
- try {
566
- const nv = JSON.parse(d.normalized_value);
567
- declType = nv.type;
568
- }
569
- catch { /* invalid JSON — proceed neutrally */ }
570
- }
571
- if (declType !== null && declType !== tokenInfo.valueType)
572
- continue;
573
- }
574
- // Only color-typed tokens are specific enough to flag;
575
- // lengths collide by nature (e.g., 6px matching border-radius tokens).
576
- if (tokenInfo.valueType !== 'color')
577
- continue;
578
835
  violations.push(this.makeViolation(d.file_path, d.line, `Token bypass: "${d.raw_value}" for "${d.property}" matches design ` +
579
836
  `token "${tokenInfo.name}" but was used as a raw value. ` +
580
- `Use the token reference instead to keep styles consistent.`, 'warning', 'styles/token-bypass'));
837
+ `Use the token reference instead to keep styles consistent.`, { severity: 'warning', rule: 'styles/token-bypass' }));
581
838
  }
582
839
  return violations;
583
840
  }
584
- /** Normalize a raw value for token-value comparison. */
585
- normalizeForTokenMatch(raw) {
586
- let v = raw.toLowerCase().trim();
587
- // Remove spaces after commas in functional notation
588
- v = v.replace(/,\s+/g, ',');
589
- // Expand shorthand hex: #fff → #ffffff
590
- if (/^#[0-9a-f]{3}$/.test(v)) {
591
- v = '#' + v[1] + v[1] + v[2] + v[2] + v[3] + v[3];
592
- }
593
- return v;
594
- }
595
841
  // -----------------------------------------------------------------------
596
842
  // Detector 5: Mechanism Fragmentation
597
843
  // -----------------------------------------------------------------------
@@ -602,41 +848,10 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
602
848
  */
603
849
  detectMechanismFragmentation(declarations, cfg) {
604
850
  const violations = [];
605
- // (A) Cross-file: same (property, raw_value) via ≥3 mechanisms
606
- const pvMap = new Map(); // "property::value" → set of mechanisms
607
- const pvSample = new Map(); // keep one sample for reporting
608
- for (const d of declarations) {
609
- const key = `${d.property}::${d.normalized_value ?? d.raw_value}`;
610
- const mechs = pvMap.get(key) || new Set();
611
- mechs.add(d.mechanism);
612
- pvMap.set(key, mechs);
613
- if (!pvSample.has(key)) {
614
- pvSample.set(key, d);
615
- }
616
- }
617
- for (const [key, mechs] of pvMap) {
618
- if (mechs.size < cfg.mechanismFragmentationMinMechanisms)
619
- continue;
620
- const sample = pvSample.get(key);
621
- const [prop, value] = key.split('::');
622
- violations.push(this.makeViolation(sample.file_path, sample.line, `Mechanism fragmentation: "${prop}: ${value}" is applied via ` +
623
- `${mechs.size} different mechanisms (${[...mechs].sort().join(', ')}). ` +
624
- `Consolidate to a single mechanism or design token.`, 'warning', 'styles/mechanism-fragmentation'));
625
- }
626
- // (B) Per-file: a single file mixing ≥3 mechanisms
627
- const fileMechs = new Map();
628
- for (const d of declarations) {
629
- const mechs = fileMechs.get(d.file_path) || new Set();
630
- mechs.add(d.mechanism);
631
- fileMechs.set(d.file_path, mechs);
632
- }
633
- for (const [file, mechs] of fileMechs) {
634
- if (mechs.size < cfg.mechanismFragmentationMinMechanisms)
635
- continue;
636
- violations.push(this.makeViolation(file, 1, `Mechanism mixing: ${file} uses ${mechs.size} different style ` +
637
- `mechanisms (${[...mechs].sort().join(', ')}). ` +
638
- `Consolidate to fewer mechanisms for maintainability.`, 'suggestion', 'styles/mechanism-mixing'));
639
- }
851
+ const report = this.makeViolation.bind(this);
852
+ const minMechanisms = cfg.mechanismFragmentationMinMechanisms;
853
+ violations.push(...flagPropertyValueFragmentation(declarations, minMechanisms, report));
854
+ violations.push(...flagFileMechanismMixing(declarations, minMechanisms, report));
640
855
  return violations;
641
856
  }
642
857
  // -----------------------------------------------------------------------
@@ -648,59 +863,8 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
648
863
  * This catches near-duplicate CSS rules that share most declarations.
649
864
  */
650
865
  detectDeclarationSetSimilarity(declarations, cfg) {
651
- const violations = [];
652
- // Group declarations by (file, context) to form rule blocks
653
- const blocks = new Map();
654
- for (const d of declarations) {
655
- if (!d.context)
656
- continue;
657
- const key = `${d.file_path}::${d.context}`;
658
- const list = blocks.get(key) || [];
659
- list.push(d);
660
- blocks.set(key, list);
661
- }
662
- // Convert blocks to declaration-value sets
663
- const blockEntries = [...blocks.entries()]
664
- .map(([key, decls]) => {
665
- const valueSet = new Set(decls.map(d => `${d.property}:${d.normalized_value ?? d.raw_value}`));
666
- return {
667
- key,
668
- filePath: decls[0].file_path,
669
- context: decls[0].context,
670
- line: decls[0].line,
671
- declCount: decls.length,
672
- valueSet,
673
- };
674
- })
675
- .filter(b => b.declCount >= cfg.declarationSetMinDeclarations);
676
- if (blockEntries.length < 2)
677
- return violations;
678
- // Compare pairs
679
- const reported = new Set(); // "keyA::keyB"
680
- for (let i = 0; i < blockEntries.length; i++) {
681
- for (let j = i + 1; j < blockEntries.length; j++) {
682
- const a = blockEntries[i];
683
- const b = blockEntries[j];
684
- // Skip same-file same-context (already the same block) or same key
685
- if (a.key === b.key)
686
- continue;
687
- const pairKey = [a.key, b.key].sort().join('::');
688
- if (reported.has(pairKey))
689
- continue;
690
- reported.add(pairKey);
691
- // Compute Jaccard similarity
692
- const intersection = new Set([...a.valueSet].filter(x => b.valueSet.has(x)));
693
- const union = new Set([...a.valueSet, ...b.valueSet]);
694
- const similarity = intersection.size / union.size;
695
- if (similarity >= cfg.declarationSetSimilarityThreshold) {
696
- violations.push(this.makeViolation(a.filePath, a.line, `Declaration-set similarity: "${a.context}" and "${b.context}" ` +
697
- `in ${b.filePath} share ${intersection.size} of ${union.size} ` +
698
- `declarations (${(similarity * 100).toFixed(0)}%). ` +
699
- `Consider consolidating these rules or extracting a shared mixin.`, 'suggestion', 'styles/declaration-set-similarity'));
700
- }
701
- }
702
- }
703
- return violations;
866
+ const blockEntries = buildDeclarationBlocks(declarations, cfg.declarationSetMinDeclarations);
867
+ return flagSimilarBlockPairs(blockEntries, cfg.declarationSetSimilarityThreshold, this.makeViolation.bind(this));
704
868
  }
705
869
  // -----------------------------------------------------------------------
706
870
  // Detector 7: Z-Index Inventory
@@ -732,139 +896,116 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
732
896
  const sample = values.get(sortedVals[0])[0];
733
897
  violations.push(this.makeViolation(sample.file_path, sample.line, `Z-index sprawl: ${values.size} distinct z-index values ` +
734
898
  `(${sortedVals.join(', ')}). Consider defining a z-index scale ` +
735
- `(e.g., $z-layers: (dropdown: 100, modal: 200, toast: 300)).`, 'warning', 'styles/z-index-sprawl'));
899
+ `(e.g., $z-layers: (dropdown: 100, modal: 200, toast: 300)).`, { severity: 'warning', rule: 'styles/z-index-sprawl' }));
736
900
  }
737
901
  // Flag singletons (z-index values used only once)
738
902
  for (const [val, list] of values) {
739
903
  if (list.length === 1 && values.size > 2) {
740
904
  const d = list[0];
741
905
  violations.push(this.makeViolation(d.file_path, d.line, `Singleton z-index: z-index: ${val} is used only once. ` +
742
- `Consider whether this value belongs in a shared z-index scale.`, 'suggestion', 'styles/z-index-singleton'));
906
+ `Consider whether this value belongs in a shared z-index scale.`, { severity: 'suggestion', rule: 'styles/z-index-singleton' }));
743
907
  }
744
908
  }
745
909
  return violations;
746
910
  }
747
- // -----------------------------------------------------------------------
748
- // Helpers
749
- // -----------------------------------------------------------------------
750
- makeViolation(filePath, line, message, severity, rule, symbol) {
751
- const v = {
752
- file: filePath,
753
- line,
754
- column: 1,
755
- severity,
756
- message,
757
- rule,
758
- analyzer: this.name,
759
- };
760
- if (symbol) {
761
- v.functionName = symbol;
911
+ }
912
+ // ---------------------------------------------------------------------------
913
+ // Orchestration layer: queries the style index and dispatches to the
914
+ // detectors. This is the only exported class in the chain.
915
+ // ---------------------------------------------------------------------------
916
+ /**
917
+ * Universal styles analyzer.
918
+ */
919
+ export class UniversalStylesAnalyzer extends UniversalStylesAnalyzerDetectors {
920
+ structure = new StylesStructureDetectors(this.makeViolation.bind(this));
921
+ /**
922
+ * Override analyze() to query the full style index in one pass instead
923
+ * of per-file AST processing. The base class analyze() loop is bypassed.
924
+ * @param config
925
+ * @param files
926
+ * @param options
927
+ * @returns
928
+ */
929
+ async analyze(files, config = {}, options = {}) {
930
+ const startTime = Date.now();
931
+ const cfg = { ...DEFAULT_STYLES_CONFIG, ...config };
932
+ // Use the IndexHandle passed through the pipeline; fall back if absent.
933
+ const indexHandle = config.indexHandle;
934
+ if (!indexHandle) {
935
+ return buildStylesResult({
936
+ violations: [],
937
+ errors: [{ file: '', error: 'No index handle available — style index not open' }],
938
+ fileCount: 0,
939
+ startTime,
940
+ name: this.name,
941
+ });
762
942
  }
763
- return v;
943
+ const declarations = this.queryDeclarations(indexHandle);
944
+ if (declarations.length === 0) {
945
+ return buildStylesResult({
946
+ violations: [],
947
+ fileCount: files.length,
948
+ startTime,
949
+ name: this.name,
950
+ });
951
+ }
952
+ const violations = await this.runDetectors({
953
+ byProperty: groupDeclarationsByProperty(declarations),
954
+ cfg,
955
+ declarations,
956
+ classUsage: this.queryClassUsage(indexHandle),
957
+ tokenValueMap: buildTokenValueMap(this.queryTokens(indexHandle)),
958
+ });
959
+ applySeverityOverrides(violations, config);
960
+ return buildStylesResult({
961
+ violations: violations.filter(v => v.severity !== 'off'),
962
+ fileCount: new Set(declarations.map(d => d.file_path)).size,
963
+ startTime,
964
+ name: this.name,
965
+ });
764
966
  }
765
- /** Parse a CSS color string to [R, G, B] or null. */
766
- parseColorToRGB(raw) {
967
+ /** Run all detectors and return their combined violations. */
968
+ async runDetectors(inputs) {
969
+ const { byProperty, cfg, declarations, classUsage, tokenValueMap } = inputs;
970
+ const violations = [];
971
+ violations.push(...this.detectValueDrift(byProperty, cfg, declarations));
972
+ violations.push(...this.detectOffScaleValues(byProperty, cfg));
973
+ violations.push(...await this.structure.detectUndefinedClasses(classUsage, declarations, byProperty, cfg));
974
+ violations.push(...this.structure.detectTokenBypass(declarations, tokenValueMap, cfg));
975
+ violations.push(...this.structure.detectMechanismFragmentation(declarations, cfg));
976
+ violations.push(...this.structure.detectDeclarationSetSimilarity(declarations, cfg));
977
+ violations.push(...this.structure.detectZIndexInventory(byProperty, cfg));
978
+ return violations;
979
+ }
980
+ /** Not used — we override analyze() directly. */
981
+ async analyzeAST(_ast, _adapter, _config, _sourceCode) {
982
+ return [];
983
+ }
984
+ // -----------------------------------------------------------------------
985
+ // Database queries
986
+ // -----------------------------------------------------------------------
987
+ queryDeclarations(indexHandle) {
767
988
  try {
768
- let v = raw.toLowerCase().trim();
769
- // Hex
770
- if (v.startsWith('#')) {
771
- if (v.length === 4) {
772
- // #rgb → #rrggbb
773
- v = '#' + v[1] + v[1] + v[2] + v[2] + v[3] + v[3];
774
- }
775
- if (v.length === 7) {
776
- return [
777
- parseInt(v.slice(1, 3), 16),
778
- parseInt(v.slice(3, 5), 16),
779
- parseInt(v.slice(5, 7), 16),
780
- ];
781
- }
782
- if (v.length === 9) {
783
- return [
784
- parseInt(v.slice(1, 3), 16),
785
- parseInt(v.slice(3, 5), 16),
786
- parseInt(v.slice(5, 7), 16),
787
- ];
788
- }
789
- }
790
- // rgb(r, g, b) or rgb(r g b)
791
- const rgbMatch = v.match(/rgb\(\s*(\d+)\s*,?\s*(\d+)\s*,?\s*(\d+)\s*\)/);
792
- if (rgbMatch) {
793
- return [
794
- parseInt(rgbMatch[1]),
795
- parseInt(rgbMatch[2]),
796
- parseInt(rgbMatch[3]),
797
- ];
798
- }
799
- // Named colors — minimal set for common use
800
- const named = {
801
- 'white': [255, 255, 255], 'black': [0, 0, 0],
802
- 'red': [255, 0, 0], 'blue': [0, 0, 255], 'green': [0, 128, 0],
803
- 'transparent': [0, 0, 0],
804
- };
805
- if (named[v])
806
- return named[v];
807
- return null;
989
+ return indexHandle.query('SELECT * FROM style_declarations ORDER BY property, file_path, line');
808
990
  }
809
991
  catch {
810
- return null;
992
+ return [];
811
993
  }
812
994
  }
813
- /** Compute delta-E (CIE76) between two RGB colors. */
814
- deltaE(a, b) {
815
- const dr = a[0] - b[0];
816
- const dg = a[1] - b[1];
817
- const db = a[2] - b[2];
818
- return Math.sqrt(dr * dr + dg * dg + db * db);
819
- }
820
- /**
821
- * Cluster colors by delta-E distance.
822
- * Simple greedy algorithm: each item joins the first cluster it's close enough to,
823
- * or starts a new cluster.
824
- */
825
- clusterByDeltaE(items, threshold) {
826
- const clusters = [];
827
- for (const item of items) {
828
- let placed = false;
829
- for (const cluster of clusters) {
830
- // Use the first item's RGB as cluster centroid
831
- const centroid = cluster[0].rgb;
832
- if (this.deltaE(item.rgb, centroid) < threshold) {
833
- cluster.push(item);
834
- placed = true;
835
- break;
836
- }
837
- }
838
- if (!placed) {
839
- clusters.push([item]);
840
- }
995
+ queryTokens(indexHandle) {
996
+ try {
997
+ return indexHandle.query('SELECT * FROM style_tokens');
998
+ }
999
+ catch {
1000
+ return [];
841
1001
  }
842
- return clusters;
843
1002
  }
844
- /** Parse a CSS length value to px-equivalent, or null if not parseable. */
845
- parseLengthToPx(raw) {
1003
+ queryClassUsage(indexHandle) {
846
1004
  try {
847
- const v = raw.trim().toLowerCase();
848
- if (v === '0' || v === '0px')
849
- return 0;
850
- const match = v.match(/^(-?\d+(?:\.\d+)?)\s*(px|rem|em|%|vh|vw|pt|cm|mm)?$/);
851
- if (!match)
852
- return null;
853
- const num = parseFloat(match[1]);
854
- const unit = match[2] || 'px';
855
- // Approximate conversions (assuming 16px base for rem/em)
856
- switch (unit) {
857
- case 'px': return num;
858
- case 'rem': return num * 16;
859
- case 'em': return num * 16;
860
- case 'pt': return num * 1.333;
861
- case 'cm': return num * 37.795;
862
- case 'mm': return num * 3.7795;
863
- default: return null; // can't convert %/vh/vw without context
864
- }
1005
+ return indexHandle.query('SELECT * FROM style_class_usage');
865
1006
  }
866
1007
  catch {
867
- return null;
1008
+ return [];
868
1009
  }
869
1010
  }
870
1011
  }