code-auditor-mcp 3.4.12 → 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 (245) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/dist/analyzers/__verify_self_probe.d.ts +2 -0
  3. package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
  4. package/dist/analyzers/__verify_self_probe.js +5 -0
  5. package/dist/analyzers/__verify_self_probe.js.map +1 -0
  6. package/dist/analyzers/analyzerUtils.d.ts +34 -1
  7. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  8. package/dist/analyzers/analyzerUtils.js +61 -22
  9. package/dist/analyzers/analyzerUtils.js.map +1 -1
  10. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
  11. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
  13. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  14. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
  15. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  16. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
  17. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  18. package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
  19. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  20. package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
  21. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  22. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
  23. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  24. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
  25. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  26. package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
  27. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  28. package/dist/analyzers/documentationAnalyzer.js +259 -198
  29. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  30. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  31. package/dist/analyzers/invariantsAnalyzer.js +57 -50
  32. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  33. package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
  34. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
  35. package/dist/analyzers/orm/adapterRegistry.js +10 -0
  36. package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
  37. package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
  38. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
  39. package/dist/analyzers/orm/drizzleAdapter.js +42 -44
  40. package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
  41. package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
  42. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
  43. package/dist/analyzers/orm/prismaAdapter.js +30 -0
  44. package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
  45. package/dist/analyzers/provenance.d.ts +62 -2
  46. package/dist/analyzers/provenance.d.ts.map +1 -1
  47. package/dist/analyzers/provenance.js +443 -349
  48. package/dist/analyzers/provenance.js.map +1 -1
  49. package/dist/analyzers/reactAnalyzer.d.ts +11 -0
  50. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  51. package/dist/analyzers/reactAnalyzer.js +188 -125
  52. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  53. package/dist/analyzers/ruleRegistry.d.ts +110 -1
  54. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  55. package/dist/analyzers/ruleRegistry.js +1714 -124
  56. package/dist/analyzers/ruleRegistry.js.map +1 -1
  57. package/dist/analyzers/ruleTiming.d.ts +54 -0
  58. package/dist/analyzers/ruleTiming.d.ts.map +1 -0
  59. package/dist/analyzers/ruleTiming.js +71 -0
  60. package/dist/analyzers/ruleTiming.js.map +1 -0
  61. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
  62. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  63. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
  64. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  65. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
  66. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  67. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
  68. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  69. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +17 -202
  70. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  71. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1073 -1121
  72. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  73. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
  74. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  75. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
  76. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  77. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
  78. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  79. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
  80. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  81. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -214
  82. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  83. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1521
  84. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  85. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
  86. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  87. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -480
  88. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  89. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
  90. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
  91. package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
  92. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
  93. package/dist/analyzers/universal/schema/config.d.ts +31 -0
  94. package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
  95. package/dist/analyzers/universal/schema/config.js +57 -0
  96. package/dist/analyzers/universal/schema/config.js.map +1 -0
  97. package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
  98. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
  99. package/dist/analyzers/universal/schema/discovery.js +616 -0
  100. package/dist/analyzers/universal/schema/discovery.js.map +1 -0
  101. package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
  102. package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
  103. package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
  104. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
  105. package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
  106. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
  107. package/dist/analyzers/universal/schema/migrations.js +238 -0
  108. package/dist/analyzers/universal/schema/migrations.js.map +1 -0
  109. package/dist/analyzers/universal/schema/types.d.ts +131 -0
  110. package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
  111. package/dist/analyzers/universal/schema/types.js +10 -0
  112. package/dist/analyzers/universal/schema/types.js.map +1 -0
  113. package/dist/analyzers/universal/schema/violations.d.ts +44 -0
  114. package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
  115. package/dist/analyzers/universal/schema/violations.js +54 -0
  116. package/dist/analyzers/universal/schema/violations.js.map +1 -0
  117. package/dist/auditRunner.d.ts.map +1 -1
  118. package/dist/auditRunner.js +89 -13
  119. package/dist/auditRunner.js.map +1 -1
  120. package/dist/cli.js +315 -40
  121. package/dist/cli.js.map +1 -1
  122. package/dist/config/configLoader.d.ts.map +1 -1
  123. package/dist/config/configLoader.js +9 -7
  124. package/dist/config/configLoader.js.map +1 -1
  125. package/dist/config/defaults.d.ts +12 -3
  126. package/dist/config/defaults.d.ts.map +1 -1
  127. package/dist/config/defaults.js +15 -5
  128. package/dist/config/defaults.js.map +1 -1
  129. package/dist/config/effectiveConfig.d.ts +75 -0
  130. package/dist/config/effectiveConfig.d.ts.map +1 -0
  131. package/dist/config/effectiveConfig.js +213 -0
  132. package/dist/config/effectiveConfig.js.map +1 -0
  133. package/dist/config/pathProfiles.d.ts +7 -4
  134. package/dist/config/pathProfiles.d.ts.map +1 -1
  135. package/dist/config/pathProfiles.js +8 -10
  136. package/dist/config/pathProfiles.js.map +1 -1
  137. package/dist/config/thresholdRationales.d.ts +45 -0
  138. package/dist/config/thresholdRationales.d.ts.map +1 -0
  139. package/dist/config/thresholdRationales.js +67 -0
  140. package/dist/config/thresholdRationales.js.map +1 -0
  141. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  142. package/dist/conventions/conventionMiner.js +6 -2
  143. package/dist/conventions/conventionMiner.js.map +1 -1
  144. package/dist/enforcement/diffGate.d.ts +55 -0
  145. package/dist/enforcement/diffGate.d.ts.map +1 -0
  146. package/dist/enforcement/diffGate.js +136 -0
  147. package/dist/enforcement/diffGate.js.map +1 -0
  148. package/dist/enforcement/gate.d.ts +36 -0
  149. package/dist/enforcement/gate.d.ts.map +1 -0
  150. package/dist/enforcement/gate.js +72 -0
  151. package/dist/enforcement/gate.js.map +1 -0
  152. package/dist/enforcement/suppressions.d.ts +41 -0
  153. package/dist/enforcement/suppressions.d.ts.map +1 -0
  154. package/dist/enforcement/suppressions.js +170 -0
  155. package/dist/enforcement/suppressions.js.map +1 -0
  156. package/dist/fingerprint.d.ts.map +1 -1
  157. package/dist/fingerprint.js +4 -1
  158. package/dist/fingerprint.js.map +1 -1
  159. package/dist/invariants/ruleEngine.d.ts +3 -4
  160. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  161. package/dist/invariants/ruleEngine.js +2 -2
  162. package/dist/invariants/ruleEngine.js.map +1 -1
  163. package/dist/languages/LanguageOrchestrator.d.ts +14 -0
  164. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  165. package/dist/languages/LanguageOrchestrator.js +93 -55
  166. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  167. package/dist/languages/LanguageRegistry.d.ts +8 -0
  168. package/dist/languages/LanguageRegistry.d.ts.map +1 -1
  169. package/dist/languages/LanguageRegistry.js +8 -0
  170. package/dist/languages/LanguageRegistry.js.map +1 -1
  171. package/dist/languages/RuntimeManager.d.ts +82 -60
  172. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  173. package/dist/languages/RuntimeManager.js +404 -349
  174. package/dist/languages/RuntimeManager.js.map +1 -1
  175. package/dist/languages/UniversalAnalyzer.d.ts +51 -6
  176. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  177. package/dist/languages/UniversalAnalyzer.js +127 -88
  178. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  179. package/dist/languages/adapterBridge.d.ts +36 -0
  180. package/dist/languages/adapterBridge.d.ts.map +1 -1
  181. package/dist/languages/adapterBridge.js +89 -35
  182. package/dist/languages/adapterBridge.js.map +1 -1
  183. package/dist/languages/go/GoAdapter.d.ts +66 -23
  184. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  185. package/dist/languages/go/GoAdapter.js +553 -474
  186. package/dist/languages/go/GoAdapter.js.map +1 -1
  187. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +33 -4
  188. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
  189. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +101 -66
  190. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  191. package/dist/languages/tree-sitter/converter.d.ts +18 -0
  192. package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
  193. package/dist/languages/tree-sitter/converter.js +18 -0
  194. package/dist/languages/tree-sitter/converter.js.map +1 -1
  195. package/dist/languages/tree-sitter/parser.d.ts +39 -2
  196. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  197. package/dist/languages/tree-sitter/parser.js +93 -12
  198. package/dist/languages/tree-sitter/parser.js.map +1 -1
  199. package/dist/languages/types.d.ts +67 -2
  200. package/dist/languages/types.d.ts.map +1 -1
  201. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -72
  202. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  203. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1419 -923
  204. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  205. package/dist/mcp-tools-shared.d.ts.map +1 -1
  206. package/dist/mcp-tools-shared.js.map +1 -1
  207. package/dist/pipeline.d.ts +13 -5
  208. package/dist/pipeline.d.ts.map +1 -1
  209. package/dist/pipeline.js +381 -200
  210. package/dist/pipeline.js.map +1 -1
  211. package/dist/pipelineAdapters.d.ts +3 -8
  212. package/dist/pipelineAdapters.d.ts.map +1 -1
  213. package/dist/pipelineAdapters.js +149 -94
  214. package/dist/pipelineAdapters.js.map +1 -1
  215. package/dist/presets/presets.d.ts +54 -0
  216. package/dist/presets/presets.d.ts.map +1 -0
  217. package/dist/presets/presets.js +181 -0
  218. package/dist/presets/presets.js.map +1 -0
  219. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  220. package/dist/reporting/sarifReportGenerator.js +0 -2
  221. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  222. package/dist/ruleAliases.d.ts +67 -0
  223. package/dist/ruleAliases.d.ts.map +1 -0
  224. package/dist/ruleAliases.js +79 -0
  225. package/dist/ruleAliases.js.map +1 -0
  226. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  227. package/dist/styles/cssAstExtractor.js +78 -18
  228. package/dist/styles/cssAstExtractor.js.map +1 -1
  229. package/dist/styles/styleIndexer.js +10 -1
  230. package/dist/styles/styleIndexer.js.map +1 -1
  231. package/dist/styles/tailwindConfigLoader.d.ts +8 -6
  232. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  233. package/dist/styles/tailwindConfigLoader.js +61 -15
  234. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  235. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  236. package/dist/styles/tailwindUtilityExpander.js +13 -1
  237. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  238. package/dist/types.d.ts +181 -1
  239. package/dist/types.d.ts.map +1 -1
  240. package/dist/types.js +8 -0
  241. package/dist/types.js.map +1 -1
  242. package/package.json +5 -3
  243. package/plugin/hooks/hooks.json +1 -1
  244. package/plugin/scripts/hook-audit.sh +25 -7
  245. 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,112 +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 CSS class SELECTORS (definitions, not usages).
457
- // CSS/SCSS files: mechanism 'class' = class_selector node = ".some-class { }"
458
- // These DEFINE a class — they are not usages of one. Only usages through
459
- // other mechanisms (className, apply, class in HTML) signal consumption.
460
- // Without this guard, every CSS selector without a direct declaration
461
- // (SCSS @include-only blocks, nested rule_sets) is falsely flagged.
462
- if (u.mechanism === 'class' && /\.(css|scss)$/i.test(u.file_path))
463
- continue;
464
- // Skip known classes from CSS declarations
465
- if (definedClasses.has(u.class_name))
466
- continue;
467
- // Skip PascalCase — likely a component
468
- if (/^[A-Z]/.test(u.class_name))
469
- continue;
470
- // Skip function-like
471
- if (u.class_name.includes('('))
472
- continue;
473
- // Skip numeric
474
- if (/^\d/.test(u.class_name))
475
- continue;
476
- // Skip classes that start/end with [] — not valid CSS class names
477
- if (u.class_name.startsWith('[') || u.class_name.startsWith(']'))
478
- continue;
479
- if (u.class_name.endsWith('[') || u.class_name.endsWith(']'))
480
- continue;
481
- // Skip extraction artifacts
482
- if (/['`"${}?;!@#%^&*+=<>|\\,~]/.test(u.class_name))
483
- continue;
484
- usageEntries.push(u);
485
- candidates.push(u.class_name);
486
- }
487
- // ── 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);
488
801
  if (candidates.length > 0 && expander.probeReady) {
489
802
  await expander.validateBatch(candidates);
490
803
  }
491
- // ── Resolve each class from cache + structural patterns ─────────
492
- for (const u of usageEntries) {
493
- const resolved = expander.resolve(u.class_name);
494
- if (resolved.valid)
495
- continue;
496
- violations.push(this.makeViolation(u.file_path, u.line, `Undefined CSS class: "${u.class_name}" has no matching definition ` +
497
- `in any stylesheet, Tailwind utility set, or project config.`, 'warning', 'styles/undefined-class'));
498
- }
804
+ violations.push(...flagUnresolvedClasses(usageEntries, expander, this.makeViolation.bind(this), definedClassCatalog));
499
805
  return violations;
500
806
  }
501
807
  // -----------------------------------------------------------------------
502
808
  // Detector 4: Token Bypass
503
809
  // -----------------------------------------------------------------------
504
- /**
505
- * Spec 22 Item 1 — Values so common that coincidental token-name matches
506
- * are always noise. Filtered before any token comparison.
507
- */
508
- static TRIVIAL_VALUES = new Set([
509
- '0', '0px', '0rem', '0em', '0%', 'none', 'transparent',
510
- 'inherit', 'initial', 'unset', 'currentcolor', 'auto',
511
- '100%', '50%',
512
- ]);
513
810
  /**
514
811
  * Flag raw values that match a known design token's value but don't
515
812
  * reference the token via tokenRef.
@@ -532,74 +829,15 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
532
829
  const categoricalExclusions = new Set(cfg.categoricalPropertyExclusions ?? []);
533
830
  const scaleProps = new Set(cfg.scaleProperties ?? []);
534
831
  for (const d of declarations) {
535
- // Skip if already referencing a token (var(--...) reference)
536
- if (d.token_ref)
537
- continue;
538
- // Skip CSS custom-property definition sites — these define
539
- // token values, so literals are expected (Spec 22 R2.1).
540
- if (d.property.startsWith('--'))
541
- continue;
542
- // Skip SCSS variable definitions — these define token values
543
- // so literals are expected (Spec 22 R2.1).
544
- if (d.property.startsWith('$'))
545
- continue;
546
- // Spec 22 R2.2: skip categorical properties — they have a small set
547
- // of valid keyword values (display, position, etc.) and coincidental
548
- // token-value matches are noise.
549
- if (categoricalExclusions.has(d.property))
550
- continue;
551
- // Spec 22 R2.3: skip scale properties — values naturally vary along
552
- // a scale (font-size, margin, padding, etc.) and raw values at
553
- // different scale points are expected, not token bypasses.
554
- if (scaleProps.has(d.property))
555
- continue;
556
- // Normalize the raw value for comparison
557
- const normalized = this.normalizeForTokenMatch(d.raw_value);
558
- // Spec 22 Item 1: skip trivial values — they're so common that
559
- // coincidental token-name matches are always noise.
560
- if (UniversalStylesAnalyzer.TRIVIAL_VALUES.has(normalized))
561
- continue;
562
- const tokenInfo = tokenValueMap.get(normalized);
832
+ const tokenInfo = matchBypassToken(d, tokenValueMap, categoricalExclusions, scaleProps);
563
833
  if (!tokenInfo)
564
834
  continue;
565
- // Spec 22 Item 1: value-type equality gate — a token only
566
- // matches declarations whose normalized value type is the same.
567
- // E.g., --border-radius-lg: 8px (length) should not flag
568
- // max-width: 8px because the normalizer types them the same, but
569
- // --color-red: #22d3ee (color) should flag color: #22d3ee.
570
- if (tokenInfo.valueType !== null) {
571
- let declType = null;
572
- if (d.normalized_value) {
573
- try {
574
- const nv = JSON.parse(d.normalized_value);
575
- declType = nv.type;
576
- }
577
- catch { /* invalid JSON — proceed neutrally */ }
578
- }
579
- if (declType !== null && declType !== tokenInfo.valueType)
580
- continue;
581
- }
582
- // Only color-typed tokens are specific enough to flag;
583
- // lengths collide by nature (e.g., 6px matching border-radius tokens).
584
- if (tokenInfo.valueType !== 'color')
585
- continue;
586
835
  violations.push(this.makeViolation(d.file_path, d.line, `Token bypass: "${d.raw_value}" for "${d.property}" matches design ` +
587
836
  `token "${tokenInfo.name}" but was used as a raw value. ` +
588
- `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' }));
589
838
  }
590
839
  return violations;
591
840
  }
592
- /** Normalize a raw value for token-value comparison. */
593
- normalizeForTokenMatch(raw) {
594
- let v = raw.toLowerCase().trim();
595
- // Remove spaces after commas in functional notation
596
- v = v.replace(/,\s+/g, ',');
597
- // Expand shorthand hex: #fff → #ffffff
598
- if (/^#[0-9a-f]{3}$/.test(v)) {
599
- v = '#' + v[1] + v[1] + v[2] + v[2] + v[3] + v[3];
600
- }
601
- return v;
602
- }
603
841
  // -----------------------------------------------------------------------
604
842
  // Detector 5: Mechanism Fragmentation
605
843
  // -----------------------------------------------------------------------
@@ -610,41 +848,10 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
610
848
  */
611
849
  detectMechanismFragmentation(declarations, cfg) {
612
850
  const violations = [];
613
- // (A) Cross-file: same (property, raw_value) via ≥3 mechanisms
614
- const pvMap = new Map(); // "property::value" → set of mechanisms
615
- const pvSample = new Map(); // keep one sample for reporting
616
- for (const d of declarations) {
617
- const key = `${d.property}::${d.normalized_value ?? d.raw_value}`;
618
- const mechs = pvMap.get(key) || new Set();
619
- mechs.add(d.mechanism);
620
- pvMap.set(key, mechs);
621
- if (!pvSample.has(key)) {
622
- pvSample.set(key, d);
623
- }
624
- }
625
- for (const [key, mechs] of pvMap) {
626
- if (mechs.size < cfg.mechanismFragmentationMinMechanisms)
627
- continue;
628
- const sample = pvSample.get(key);
629
- const [prop, value] = key.split('::');
630
- violations.push(this.makeViolation(sample.file_path, sample.line, `Mechanism fragmentation: "${prop}: ${value}" is applied via ` +
631
- `${mechs.size} different mechanisms (${[...mechs].sort().join(', ')}). ` +
632
- `Consolidate to a single mechanism or design token.`, 'warning', 'styles/mechanism-fragmentation'));
633
- }
634
- // (B) Per-file: a single file mixing ≥3 mechanisms
635
- const fileMechs = new Map();
636
- for (const d of declarations) {
637
- const mechs = fileMechs.get(d.file_path) || new Set();
638
- mechs.add(d.mechanism);
639
- fileMechs.set(d.file_path, mechs);
640
- }
641
- for (const [file, mechs] of fileMechs) {
642
- if (mechs.size < cfg.mechanismFragmentationMinMechanisms)
643
- continue;
644
- violations.push(this.makeViolation(file, 1, `Mechanism mixing: ${file} uses ${mechs.size} different style ` +
645
- `mechanisms (${[...mechs].sort().join(', ')}). ` +
646
- `Consolidate to fewer mechanisms for maintainability.`, 'suggestion', 'styles/mechanism-mixing'));
647
- }
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));
648
855
  return violations;
649
856
  }
650
857
  // -----------------------------------------------------------------------
@@ -656,59 +863,8 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
656
863
  * This catches near-duplicate CSS rules that share most declarations.
657
864
  */
658
865
  detectDeclarationSetSimilarity(declarations, cfg) {
659
- const violations = [];
660
- // Group declarations by (file, context) to form rule blocks
661
- const blocks = new Map();
662
- for (const d of declarations) {
663
- if (!d.context)
664
- continue;
665
- const key = `${d.file_path}::${d.context}`;
666
- const list = blocks.get(key) || [];
667
- list.push(d);
668
- blocks.set(key, list);
669
- }
670
- // Convert blocks to declaration-value sets
671
- const blockEntries = [...blocks.entries()]
672
- .map(([key, decls]) => {
673
- const valueSet = new Set(decls.map(d => `${d.property}:${d.normalized_value ?? d.raw_value}`));
674
- return {
675
- key,
676
- filePath: decls[0].file_path,
677
- context: decls[0].context,
678
- line: decls[0].line,
679
- declCount: decls.length,
680
- valueSet,
681
- };
682
- })
683
- .filter(b => b.declCount >= cfg.declarationSetMinDeclarations);
684
- if (blockEntries.length < 2)
685
- return violations;
686
- // Compare pairs
687
- const reported = new Set(); // "keyA::keyB"
688
- for (let i = 0; i < blockEntries.length; i++) {
689
- for (let j = i + 1; j < blockEntries.length; j++) {
690
- const a = blockEntries[i];
691
- const b = blockEntries[j];
692
- // Skip same-file same-context (already the same block) or same key
693
- if (a.key === b.key)
694
- continue;
695
- const pairKey = [a.key, b.key].sort().join('::');
696
- if (reported.has(pairKey))
697
- continue;
698
- reported.add(pairKey);
699
- // Compute Jaccard similarity
700
- const intersection = new Set([...a.valueSet].filter(x => b.valueSet.has(x)));
701
- const union = new Set([...a.valueSet, ...b.valueSet]);
702
- const similarity = intersection.size / union.size;
703
- if (similarity >= cfg.declarationSetSimilarityThreshold) {
704
- violations.push(this.makeViolation(a.filePath, a.line, `Declaration-set similarity: "${a.context}" and "${b.context}" ` +
705
- `in ${b.filePath} share ${intersection.size} of ${union.size} ` +
706
- `declarations (${(similarity * 100).toFixed(0)}%). ` +
707
- `Consider consolidating these rules or extracting a shared mixin.`, 'suggestion', 'styles/declaration-set-similarity'));
708
- }
709
- }
710
- }
711
- return violations;
866
+ const blockEntries = buildDeclarationBlocks(declarations, cfg.declarationSetMinDeclarations);
867
+ return flagSimilarBlockPairs(blockEntries, cfg.declarationSetSimilarityThreshold, this.makeViolation.bind(this));
712
868
  }
713
869
  // -----------------------------------------------------------------------
714
870
  // Detector 7: Z-Index Inventory
@@ -740,139 +896,116 @@ export class UniversalStylesAnalyzer extends UniversalAnalyzer {
740
896
  const sample = values.get(sortedVals[0])[0];
741
897
  violations.push(this.makeViolation(sample.file_path, sample.line, `Z-index sprawl: ${values.size} distinct z-index values ` +
742
898
  `(${sortedVals.join(', ')}). Consider defining a z-index scale ` +
743
- `(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' }));
744
900
  }
745
901
  // Flag singletons (z-index values used only once)
746
902
  for (const [val, list] of values) {
747
903
  if (list.length === 1 && values.size > 2) {
748
904
  const d = list[0];
749
905
  violations.push(this.makeViolation(d.file_path, d.line, `Singleton z-index: z-index: ${val} is used only once. ` +
750
- `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' }));
751
907
  }
752
908
  }
753
909
  return violations;
754
910
  }
755
- // -----------------------------------------------------------------------
756
- // Helpers
757
- // -----------------------------------------------------------------------
758
- makeViolation(filePath, line, message, severity, rule, symbol) {
759
- const v = {
760
- file: filePath,
761
- line,
762
- column: 1,
763
- severity,
764
- message,
765
- rule,
766
- analyzer: this.name,
767
- };
768
- if (symbol) {
769
- 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
+ });
770
942
  }
771
- 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
+ });
772
966
  }
773
- /** Parse a CSS color string to [R, G, B] or null. */
774
- 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) {
775
988
  try {
776
- let v = raw.toLowerCase().trim();
777
- // Hex
778
- if (v.startsWith('#')) {
779
- if (v.length === 4) {
780
- // #rgb → #rrggbb
781
- v = '#' + v[1] + v[1] + v[2] + v[2] + v[3] + v[3];
782
- }
783
- if (v.length === 7) {
784
- return [
785
- parseInt(v.slice(1, 3), 16),
786
- parseInt(v.slice(3, 5), 16),
787
- parseInt(v.slice(5, 7), 16),
788
- ];
789
- }
790
- if (v.length === 9) {
791
- return [
792
- parseInt(v.slice(1, 3), 16),
793
- parseInt(v.slice(3, 5), 16),
794
- parseInt(v.slice(5, 7), 16),
795
- ];
796
- }
797
- }
798
- // rgb(r, g, b) or rgb(r g b)
799
- const rgbMatch = v.match(/rgb\(\s*(\d+)\s*,?\s*(\d+)\s*,?\s*(\d+)\s*\)/);
800
- if (rgbMatch) {
801
- return [
802
- parseInt(rgbMatch[1]),
803
- parseInt(rgbMatch[2]),
804
- parseInt(rgbMatch[3]),
805
- ];
806
- }
807
- // Named colors — minimal set for common use
808
- const named = {
809
- 'white': [255, 255, 255], 'black': [0, 0, 0],
810
- 'red': [255, 0, 0], 'blue': [0, 0, 255], 'green': [0, 128, 0],
811
- 'transparent': [0, 0, 0],
812
- };
813
- if (named[v])
814
- return named[v];
815
- return null;
989
+ return indexHandle.query('SELECT * FROM style_declarations ORDER BY property, file_path, line');
816
990
  }
817
991
  catch {
818
- return null;
992
+ return [];
819
993
  }
820
994
  }
821
- /** Compute delta-E (CIE76) between two RGB colors. */
822
- deltaE(a, b) {
823
- const dr = a[0] - b[0];
824
- const dg = a[1] - b[1];
825
- const db = a[2] - b[2];
826
- return Math.sqrt(dr * dr + dg * dg + db * db);
827
- }
828
- /**
829
- * Cluster colors by delta-E distance.
830
- * Simple greedy algorithm: each item joins the first cluster it's close enough to,
831
- * or starts a new cluster.
832
- */
833
- clusterByDeltaE(items, threshold) {
834
- const clusters = [];
835
- for (const item of items) {
836
- let placed = false;
837
- for (const cluster of clusters) {
838
- // Use the first item's RGB as cluster centroid
839
- const centroid = cluster[0].rgb;
840
- if (this.deltaE(item.rgb, centroid) < threshold) {
841
- cluster.push(item);
842
- placed = true;
843
- break;
844
- }
845
- }
846
- if (!placed) {
847
- clusters.push([item]);
848
- }
995
+ queryTokens(indexHandle) {
996
+ try {
997
+ return indexHandle.query('SELECT * FROM style_tokens');
998
+ }
999
+ catch {
1000
+ return [];
849
1001
  }
850
- return clusters;
851
1002
  }
852
- /** Parse a CSS length value to px-equivalent, or null if not parseable. */
853
- parseLengthToPx(raw) {
1003
+ queryClassUsage(indexHandle) {
854
1004
  try {
855
- const v = raw.trim().toLowerCase();
856
- if (v === '0' || v === '0px')
857
- return 0;
858
- const match = v.match(/^(-?\d+(?:\.\d+)?)\s*(px|rem|em|%|vh|vw|pt|cm|mm)?$/);
859
- if (!match)
860
- return null;
861
- const num = parseFloat(match[1]);
862
- const unit = match[2] || 'px';
863
- // Approximate conversions (assuming 16px base for rem/em)
864
- switch (unit) {
865
- case 'px': return num;
866
- case 'rem': return num * 16;
867
- case 'em': return num * 16;
868
- case 'pt': return num * 1.333;
869
- case 'cm': return num * 37.795;
870
- case 'mm': return num * 3.7795;
871
- default: return null; // can't convert %/vh/vw without context
872
- }
1005
+ return indexHandle.query('SELECT * FROM style_class_usage');
873
1006
  }
874
1007
  catch {
875
- return null;
1008
+ return [];
876
1009
  }
877
1010
  }
878
1011
  }