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
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import * as path from 'path';
16
16
  import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
17
+ import { withRuleTiming } from '../ruleTiming.js';
17
18
  import { makeVisitorStatus } from '../../pipeline.js';
18
19
  import { detectCase, detectErrorHandlingShape, detectExportForm, parseFileImports, hasNonLatinChars, } from '../../conventions/conventionMiner.js';
19
20
  // ---------------------------------------------------------------------------
@@ -28,6 +29,9 @@ export const DEFAULT_CONVENTIONS_CONFIG = {
28
29
  // ---------------------------------------------------------------------------
29
30
  // Analyzer
30
31
  // ---------------------------------------------------------------------------
32
+ /**
33
+ * Universal conventions analyzer.
34
+ */
31
35
  export class UniversalConventionsAnalyzer extends UniversalAnalyzer {
32
36
  name = 'conventions';
33
37
  description = 'Mines codebase conventions and flags deviations at suggestion severity';
@@ -35,53 +39,70 @@ export class UniversalConventionsAnalyzer extends UniversalAnalyzer {
35
39
  /**
36
40
  * Full override: query conventions from the DB and emit violations per domain.
37
41
  * The base-class per-file AST loop is bypassed.
42
+ * @param config
43
+ * @param files
44
+ * @param options
45
+ * @returns
38
46
  */
39
47
  async analyze(files, config = {}, options = {}) {
40
48
  const startTime = Date.now();
41
49
  const violations = [];
42
50
  const indexHandle = config.indexHandle;
43
51
  if (!indexHandle) {
44
- return {
45
- violations: [],
46
- errors: [{ file: '', error: 'No index handle available — code index not open' }],
47
- status: makeVisitorStatus(0),
48
- executionTime: Date.now() - startTime,
49
- analyzerName: this.name,
50
- metrics: { filesAnalyzed: 0, totalViolations: 0, executionTime: Date.now() - startTime },
51
- };
52
+ return this.makeResult([], 0, startTime, [
53
+ { file: '', error: 'No index handle available — code index not open' },
54
+ ]);
52
55
  }
53
56
  // Query all conventions
54
57
  const conventions = indexHandle.query('SELECT * FROM conventions ORDER BY domain, directory');
55
58
  if (conventions.length === 0) {
56
- return {
57
- violations: [],
58
- errors: [],
59
- status: makeVisitorStatus(files.length),
60
- executionTime: Date.now() - startTime,
61
- analyzerName: this.name,
62
- metrics: { filesAnalyzed: files.length, totalViolations: 0, executionTime: Date.now() - startTime },
63
- };
59
+ return this.makeResult([], files.length, startTime);
64
60
  }
65
- // Extract project root, source map, and exports map from config
61
+ // Extract project root, on-demand source reader, and exports map from config
66
62
  const projectRoot = config.projectRoot;
67
- const sourceMap = config.sourceMap;
63
+ const readSource = config.readSource;
68
64
  // B2: exportsMap comes from function-index visitor facts (AST-extracted)
69
65
  const exportsMap = config.exportsMap;
70
- // Group conventions by domain for efficient detection
71
- const byDomain = new Map();
72
- for (const c of conventions) {
73
- const list = byDomain.get(c.domain) ?? [];
74
- list.push(c);
75
- byDomain.set(c.domain, list);
76
- }
77
66
  // Detect violations per domain
67
+ const byDomain = groupConventionsByDomain(conventions);
68
+ const scanCtx = { projectRoot, readSource, exportsMap };
69
+ violations.push(...this.detectPerDomain(byDomain, indexHandle, scanCtx));
70
+ // Count unique files that conventions apply to (from the function index)
71
+ const uniqueFiles = countUniqueFiles(indexHandle, projectRoot);
72
+ return this.makeResult(violations, uniqueFiles, startTime);
73
+ }
74
+ /** Build a standard AnalyzerResult with shared metrics and timing fields. */
75
+ makeResult(violations, filesAnalyzed, startTime, errors = []) {
76
+ return {
77
+ violations,
78
+ errors,
79
+ status: makeVisitorStatus(filesAnalyzed),
80
+ executionTime: Date.now() - startTime,
81
+ analyzerName: this.name,
82
+ metrics: {
83
+ filesAnalyzed,
84
+ totalViolations: violations.length,
85
+ executionTime: Date.now() - startTime,
86
+ },
87
+ };
88
+ }
89
+ /** No-op — all detection is DB-based. */
90
+ async analyzeAST() {
91
+ return [];
92
+ }
93
+ /**
94
+ * Dispatch each convention domain to its detector.
95
+ */
96
+ detectPerDomain(byDomain, indexHandle, scanCtx) {
97
+ const { projectRoot, readSource, exportsMap } = scanCtx;
98
+ const violations = [];
78
99
  for (const [domain, domainConventions] of byDomain) {
79
100
  switch (domain) {
80
101
  case 'usage-pair':
81
102
  violations.push(...this.detectUsagePairViolations(indexHandle, domainConventions));
82
103
  break;
83
104
  case 'import-form':
84
- violations.push(...this.detectImportFormViolations(indexHandle, domainConventions, projectRoot, sourceMap));
105
+ violations.push(...this.detectImportFormViolations(indexHandle, domainConventions, projectRoot, readSource));
85
106
  break;
86
107
  case 'error-handling':
87
108
  violations.push(...this.detectErrorHandlingViolations(indexHandle, domainConventions));
@@ -94,28 +115,7 @@ export class UniversalConventionsAnalyzer extends UniversalAnalyzer {
94
115
  break;
95
116
  }
96
117
  }
97
- // Count unique files that conventions apply to (from the function index)
98
- const resolvedProjectRoot = projectRoot ? path.resolve(projectRoot) : undefined;
99
- const fileRows = indexHandle.query(resolvedProjectRoot
100
- ? 'SELECT COUNT(DISTINCT file_path) as cnt FROM functions WHERE file_path LIKE ?'
101
- : 'SELECT COUNT(DISTINCT file_path) as cnt FROM functions', resolvedProjectRoot ? [resolvedProjectRoot + '%'] : []);
102
- const uniqueFiles = fileRows[0]?.cnt ?? 0;
103
- return {
104
- violations,
105
- errors: [],
106
- status: makeVisitorStatus(uniqueFiles),
107
- executionTime: Date.now() - startTime,
108
- analyzerName: this.name,
109
- metrics: {
110
- filesAnalyzed: uniqueFiles,
111
- totalViolations: violations.length,
112
- executionTime: Date.now() - startTime,
113
- },
114
- };
115
- }
116
- /** No-op — all detection is DB-based. */
117
- async analyzeAST() {
118
- return [];
118
+ return violations;
119
119
  }
120
120
  // ── Usage-Pair Detection ──────────────────────────────────────────────
121
121
  /**
@@ -124,60 +124,20 @@ export class UniversalConventionsAnalyzer extends UniversalAnalyzer {
124
124
  */
125
125
  detectUsagePairViolations(indexHandle, conventions) {
126
126
  const violations = [];
127
- // Query all function calls once
127
+ // Query all function calls once and index both directions
128
128
  const allCalls = indexHandle.query('SELECT caller_id, callee_name FROM function_calls');
129
- // Build callerId Set<calleeName>
130
- const callerCalls = new Map();
131
- for (const fc of allCalls) {
132
- if (!callerCalls.has(fc.caller_id))
133
- callerCalls.set(fc.caller_id, new Set());
134
- callerCalls.get(fc.caller_id).add(fc.callee_name);
135
- }
136
- // antecedentName → Set<callerId>
137
- const antecedentCallers = new Map();
138
- for (const fc of allCalls) {
139
- if (!antecedentCallers.has(fc.callee_name)) {
140
- antecedentCallers.set(fc.callee_name, new Set());
141
- }
142
- antecedentCallers.get(fc.callee_name).add(fc.caller_id);
143
- }
129
+ const { callerCalls, antecedentCallers } = buildCallMaps(allCalls);
144
130
  // Query all functions for file/line info
145
131
  const funcRows = indexHandle.query('SELECT id, name, file_path, line_number FROM functions');
146
- const funcById = new Map();
147
- for (const f of funcRows) {
148
- funcById.set(f.id, f);
149
- }
132
+ const funcById = buildFuncById(funcRows);
133
+ const ctx = {
134
+ antecedentCallers,
135
+ callerCalls,
136
+ funcById,
137
+ analyzerName: this.name,
138
+ };
150
139
  for (const conv of conventions) {
151
- if (!conv.antecedent || !conv.consequent)
152
- continue;
153
- const antecedent = conv.antecedent;
154
- const consequent = conv.consequent;
155
- const callerIds = antecedentCallers.get(antecedent);
156
- if (!callerIds || callerIds.size === 0)
157
- continue;
158
- for (const cid of callerIds) {
159
- const callSet = callerCalls.get(cid);
160
- if (!callSet || !callSet.has(consequent)) {
161
- const func = funcById.get(cid);
162
- if (!func)
163
- continue;
164
- const exemplarRef = conv.exemplar_file
165
- ? ` (exemplar: ${conv.exemplar_file}${conv.exemplar_line ? `:${conv.exemplar_line}` : ''})`
166
- : '';
167
- const pct = Math.round(conv.confidence * 100);
168
- violations.push({
169
- file: func.file_path,
170
- line: func.line_number,
171
- column: 1,
172
- severity: 'suggestion',
173
- message: `${pct}% of \`${antecedent}\` callers also call \`${consequent}\` — ` +
174
- `this function calls \`${antecedent}\` without \`${consequent}\`${exemplarRef}`,
175
- rule: 'conventions/usage-pair',
176
- analyzer: this.name,
177
- functionName: func.name,
178
- });
179
- }
180
- }
140
+ violations.push(...withRuleTiming('conventions/usage-pair', () => detectUsagePairForConvention(conv, ctx)));
181
141
  }
182
142
  return violations;
183
143
  }
@@ -186,28 +146,10 @@ export class UniversalConventionsAnalyzer extends UniversalAnalyzer {
186
146
  * For each import-form convention (source → dominantForm in directory),
187
147
  * find imports of that source that use a minority form.
188
148
  */
189
- detectImportFormViolations(indexHandle, conventions, projectRoot, sourceMap) {
149
+ detectImportFormViolations(indexHandle, conventions, projectRoot, readSource) {
190
150
  const violations = [];
191
- // Use directory-key → Map<source, ImportConv>
192
- const dirImports = new Map();
193
- for (const conv of conventions) {
194
- const dir = conv.directory ?? '.';
195
- const source = conv.antecedent;
196
- if (!source)
197
- continue;
198
- const form = conv.consequent;
199
- if (!form)
200
- continue;
201
- if (!dirImports.has(dir))
202
- dirImports.set(dir, new Map());
203
- dirImports.get(dir).set(source, {
204
- source,
205
- form,
206
- confidence: conv.confidence,
207
- exemplar_file: conv.exemplar_file,
208
- exemplar_line: conv.exemplar_line,
209
- });
210
- }
151
+ // Build directory → Map<source, ImportConv>
152
+ const dirImports = buildDirImports(conventions);
211
153
  // Get unique file paths
212
154
  const fileRows = indexHandle.query('SELECT DISTINCT file_path FROM functions WHERE file_path IS NOT NULL');
213
155
  const seenFiles = new Set();
@@ -220,36 +162,10 @@ export class UniversalConventionsAnalyzer extends UniversalAnalyzer {
220
162
  if (!importConvs)
221
163
  continue;
222
164
  const fullPath = projectRoot ? path.join(projectRoot, fp) : fp;
223
- let content = sourceMap?.get(fullPath);
165
+ const content = readSource?.(fullPath);
224
166
  if (content === undefined)
225
167
  continue;
226
- const imports = parseFileImports(content);
227
- for (const imp of imports) {
228
- const conv = importConvs.get(imp.source);
229
- if (!conv || imp.form === conv.form)
230
- continue;
231
- const pct = Math.round(conv.confidence * 100);
232
- const exemplarRef = conv.exemplar_file
233
- ? ` (exemplar: ${conv.exemplar_file}${conv.exemplar_line ? `:${conv.exemplar_line}` : ''})`
234
- : '';
235
- violations.push({
236
- file: fp,
237
- line: imp.line,
238
- column: 1,
239
- severity: 'suggestion',
240
- message: `${pct}% of imports of \`${imp.source}\` in \`${directory}/\` ` +
241
- `use ${conv.form} import — this file uses ${imp.form}${exemplarRef}`,
242
- rule: 'conventions/import-form',
243
- analyzer: this.name,
244
- details: {
245
- source: imp.source,
246
- directory,
247
- conventionForm: conv.form,
248
- actualForm: imp.form,
249
- localNames: imp.localNames,
250
- },
251
- });
252
- }
168
+ violations.push(...detectImportFormForFile(fp, content, importConvs, this.name));
253
169
  }
254
170
  return violations;
255
171
  }
@@ -263,55 +179,14 @@ export class UniversalConventionsAnalyzer extends UniversalAnalyzer {
263
179
  detectErrorHandlingViolations(indexHandle, conventions) {
264
180
  const violations = [];
265
181
  // directory → dominantShape
266
- const dirShapes = new Map();
267
- for (const conv of conventions) {
268
- const dir = conv.directory ?? '.';
269
- const shape = conv.pattern;
270
- if (!shape)
271
- continue;
272
- dirShapes.set(dir, {
273
- shape,
274
- confidence: conv.confidence,
275
- exemplar_file: conv.exemplar_file,
276
- exemplar_line: conv.exemplar_line,
277
- });
278
- }
182
+ const dirShapes = buildDirShapes(conventions);
279
183
  const rows = indexHandle.query(`SELECT id, name, file_path, line_number, metadata_json
280
184
  FROM functions
281
185
  WHERE metadata_json IS NOT NULL`);
282
186
  for (const row of rows) {
283
- const directory = path.dirname(row.file_path) || '.';
284
- const conv = dirShapes.get(directory);
285
- if (!conv)
286
- continue;
287
- let metadata;
288
- try {
289
- metadata = JSON.parse(row.metadata_json);
290
- }
291
- catch {
292
- continue;
293
- }
294
- const body = metadata.body;
295
- const shape = detectErrorHandlingShape(body);
296
- if (!shape)
297
- continue; // no error handling → skip
298
- if (shape === conv.shape)
299
- continue; // matches convention
300
- const pct = Math.round(conv.confidence * 100);
301
- const exemplarRef = conv.exemplar_file
302
- ? ` (exemplar: ${conv.exemplar_file}${conv.exemplar_line ? `:${conv.exemplar_line}` : ''})`
303
- : '';
304
- violations.push({
305
- file: row.file_path,
306
- line: row.line_number,
307
- column: 1,
308
- severity: 'suggestion',
309
- message: `${pct}% of error-handling functions in \`${directory}/\` use ` +
310
- `\`${conv.shape}\` — this function uses \`${shape}\`${exemplarRef}`,
311
- rule: 'conventions/error-handling',
312
- analyzer: this.name,
313
- functionName: row.name,
314
- });
187
+ const violation = detectErrorHandlingForRow(row, dirShapes, this.name);
188
+ if (violation)
189
+ violations.push(violation);
315
190
  }
316
191
  return violations;
317
192
  }
@@ -323,49 +198,14 @@ export class UniversalConventionsAnalyzer extends UniversalAnalyzer {
323
198
  detectExportShapeViolations(indexHandle, conventions, exportsMap) {
324
199
  const violations = [];
325
200
  // directory → dominantForm
326
- const dirForms = new Map();
327
- for (const conv of conventions) {
328
- const dir = conv.directory ?? '.';
329
- const form = conv.pattern;
330
- if (!form)
331
- continue;
332
- dirForms.set(dir, {
333
- form,
334
- confidence: conv.confidence,
335
- exemplar_file: conv.exemplar_file,
336
- exemplar_line: conv.exemplar_line,
337
- });
338
- }
201
+ const dirForms = buildDirForms(conventions);
339
202
  const rows = indexHandle.query(`SELECT id, name, file_path, line_number, is_exported
340
203
  FROM functions
341
204
  WHERE is_exported = 1`);
342
205
  for (const row of rows) {
343
- const directory = path.dirname(row.file_path) || '.';
344
- const conv = dirForms.get(directory);
345
- if (!conv)
346
- continue;
347
- // B2: Use AST-extracted ExportInfo[] from function-index facts
348
- const fileExports = exportsMap?.get(row.file_path);
349
- if (!fileExports)
350
- continue;
351
- const form = detectExportForm(row.name, fileExports);
352
- if (!form || form === conv.form)
353
- continue;
354
- const pct = Math.round(conv.confidence * 100);
355
- const exemplarRef = conv.exemplar_file
356
- ? ` (exemplar: ${conv.exemplar_file}${conv.exemplar_line ? `:${conv.exemplar_line}` : ''})`
357
- : '';
358
- violations.push({
359
- file: row.file_path,
360
- line: row.line_number,
361
- column: 1,
362
- severity: 'suggestion',
363
- message: `${pct}% of exports in \`${directory}/\` use ${conv.form} export — ` +
364
- `\`${row.name}\` uses ${form}${exemplarRef}`,
365
- rule: 'conventions/export-shape',
366
- analyzer: this.name,
367
- functionName: row.name,
368
- });
206
+ const violation = detectExportShapeForRow(row, dirForms, exportsMap, this.name);
207
+ if (violation)
208
+ violations.push(violation);
369
209
  }
370
210
  return violations;
371
211
  }
@@ -378,69 +218,308 @@ export class UniversalConventionsAnalyzer extends UniversalAnalyzer {
378
218
  detectNamingViolations(indexHandle, conventions) {
379
219
  const violations = [];
380
220
  // directory → kind → convention
381
- const dirKindCases = new Map();
382
- for (const conv of conventions) {
383
- const dir = conv.directory ?? '.';
384
- const casing = conv.pattern;
385
- if (!casing)
386
- continue;
387
- const kind = conv.export_kind ?? 'function';
388
- if (!dirKindCases.has(dir))
389
- dirKindCases.set(dir, new Map());
390
- dirKindCases.get(dir).set(kind, {
391
- casing,
392
- confidence: conv.confidence,
393
- exemplar_file: conv.exemplar_file,
394
- exemplar_line: conv.exemplar_line,
395
- kind,
396
- });
397
- }
221
+ const dirKindCases = buildDirKindCases(conventions);
398
222
  const rows = indexHandle.query(`SELECT id, name, file_path, line_number, is_exported, entity_type, component_type
399
223
  FROM functions
400
224
  WHERE is_exported = 1`);
401
225
  for (const row of rows) {
402
- const directory = path.dirname(row.file_path) || '.';
403
- // Classify into export kind (same logic as mineNaming)
404
- let rowKind;
405
- if (row.entity_type === 'component' || row.component_type !== null) {
406
- rowKind = 'react-component';
407
- }
408
- else if (/^use[A-Z]/.test(row.name)) {
409
- rowKind = 'hook';
410
- }
411
- else {
412
- rowKind = 'function';
413
- }
414
- const kindConvs = dirKindCases.get(directory);
415
- if (!kindConvs)
416
- continue;
417
- // Try exact kind match first, fall back to 'function' for backward compat
418
- const conv = kindConvs.get(rowKind);
419
- if (!conv)
420
- continue;
421
- // Non-Latin skip (Spec 21 R5.4)
422
- if (hasNonLatinChars(row.name))
423
- continue;
424
- const casing = detectCase(row.name);
425
- if (!casing || casing === conv.casing)
226
+ const violation = detectNamingForRow(row, dirKindCases, this.name);
227
+ if (violation)
228
+ violations.push(violation);
229
+ }
230
+ return violations;
231
+ }
232
+ }
233
+ /** Build the `(exemplar: file:line)` suffix shared across convention messages. */
234
+ function exemplarRef(conv) {
235
+ return conv.exemplar_file
236
+ ? ` (exemplar: ${conv.exemplar_file}${conv.exemplar_line ? `:${conv.exemplar_line}` : ''})`
237
+ : '';
238
+ }
239
+ /** Group convention rows by their `domain` field. */
240
+ function groupConventionsByDomain(conventions) {
241
+ const byDomain = new Map();
242
+ for (const conv of conventions) {
243
+ const list = byDomain.get(conv.domain);
244
+ if (list)
245
+ list.push(conv);
246
+ else
247
+ byDomain.set(conv.domain, [conv]);
248
+ }
249
+ return byDomain;
250
+ }
251
+ /** Count distinct file paths in the function index (files conventions apply to). */
252
+ function countUniqueFiles(indexHandle, projectRoot) {
253
+ const rows = indexHandle.query('SELECT DISTINCT file_path FROM functions WHERE file_path IS NOT NULL');
254
+ return rows.length;
255
+ }
256
+ /** Index call rows in both directions: caller→callees and callee→callers. */
257
+ function buildCallMaps(allCalls) {
258
+ const callerCalls = new Map();
259
+ const antecedentCallers = new Map();
260
+ for (const call of allCalls) {
261
+ const callees = callerCalls.get(call.caller_id);
262
+ if (callees)
263
+ callees.add(call.callee_name);
264
+ else
265
+ callerCalls.set(call.caller_id, new Set([call.callee_name]));
266
+ const callers = antecedentCallers.get(call.callee_name);
267
+ if (callers)
268
+ callers.add(call.caller_id);
269
+ else
270
+ antecedentCallers.set(call.callee_name, new Set([call.caller_id]));
271
+ }
272
+ return { callerCalls, antecedentCallers };
273
+ }
274
+ /** Index function rows by id for caller→function lookups. */
275
+ function buildFuncById(funcRows) {
276
+ const map = new Map();
277
+ for (const row of funcRows)
278
+ map.set(row.id, row);
279
+ return map;
280
+ }
281
+ /** Emit usage-pair violations for a single convention row. */
282
+ function detectUsagePairForConvention(conv, ctx) {
283
+ const violations = [];
284
+ if (!conv.antecedent || !conv.consequent)
285
+ return violations;
286
+ const antecedent = conv.antecedent;
287
+ const consequent = conv.consequent;
288
+ const callerIds = ctx.antecedentCallers.get(antecedent);
289
+ if (!callerIds || callerIds.size === 0)
290
+ return violations;
291
+ for (const cid of callerIds) {
292
+ const callSet = ctx.callerCalls.get(cid);
293
+ if (!callSet || !callSet.has(consequent)) {
294
+ const func = ctx.funcById.get(cid);
295
+ if (!func)
426
296
  continue;
427
297
  const pct = Math.round(conv.confidence * 100);
428
- const exemplarRef = conv.exemplar_file
429
- ? ` (exemplar: ${conv.exemplar_file}${conv.exemplar_line ? `:${conv.exemplar_line}` : ''})`
430
- : '';
431
298
  violations.push({
432
- file: row.file_path,
433
- line: row.line_number,
299
+ file: func.file_path,
300
+ line: func.line_number,
434
301
  column: 1,
435
302
  severity: 'suggestion',
436
- message: `${pct}% of ${conv.kind} exports in \`${directory}/\` use ${conv.casing} — ` +
437
- `\`${row.name}\` uses ${casing}${exemplarRef}`,
438
- rule: 'conventions/naming',
439
- analyzer: this.name,
440
- functionName: row.name,
303
+ message: `${pct}% of \`${antecedent}\` callers also call \`${consequent}\` — ` +
304
+ `this function calls \`${antecedent}\` without \`${consequent}\`${exemplarRef(conv)}`,
305
+ rule: 'conventions/usage-pair',
306
+ analyzer: ctx.analyzerName,
307
+ functionName: func.name,
308
+ resolution: {
309
+ action: 'call-companion',
310
+ summary: `Add a call to the companion function \`${consequent}\` in \`${func.name}\` — ${pct}% of \`${antecedent}\` callers also call it.`,
311
+ symbols: [consequent, antecedent],
312
+ files: [func.file_path],
313
+ lines: [func.line_number],
314
+ },
441
315
  });
442
316
  }
443
- return violations;
444
317
  }
318
+ return violations;
319
+ }
320
+ /** Build directory → Map<source, ImportConv> from import-form convention rows. */
321
+ function buildDirImports(conventions) {
322
+ const dirImports = new Map();
323
+ for (const conv of conventions) {
324
+ const dir = conv.directory ?? '.';
325
+ const source = conv.antecedent;
326
+ if (!source)
327
+ continue;
328
+ const form = conv.consequent;
329
+ if (!form)
330
+ continue;
331
+ if (!dirImports.has(dir))
332
+ dirImports.set(dir, new Map());
333
+ dirImports.get(dir).set(source, {
334
+ source,
335
+ form,
336
+ confidence: conv.confidence,
337
+ exemplar_file: conv.exemplar_file,
338
+ exemplar_line: conv.exemplar_line,
339
+ });
340
+ }
341
+ return dirImports;
342
+ }
343
+ /** Detect import-form violations within one file's parsed imports. */
344
+ function detectImportFormForFile(fp, content, importConvs, analyzerName) {
345
+ const violations = [];
346
+ const directory = path.dirname(fp) || '.';
347
+ const imports = parseFileImports(content);
348
+ for (const imp of imports) {
349
+ const conv = importConvs.get(imp.source);
350
+ if (!conv || imp.form === conv.form)
351
+ continue;
352
+ const pct = Math.round(conv.confidence * 100);
353
+ violations.push({
354
+ file: fp,
355
+ line: imp.line,
356
+ column: 1,
357
+ severity: 'suggestion',
358
+ message: `${pct}% of imports of \`${imp.source}\` in \`${directory}/\` ` +
359
+ `use ${conv.form} import — this file uses ${imp.form}${exemplarRef(conv)}`,
360
+ rule: 'conventions/import-form',
361
+ analyzer: analyzerName,
362
+ details: {
363
+ source: imp.source,
364
+ directory,
365
+ conventionForm: conv.form,
366
+ actualForm: imp.form,
367
+ localNames: imp.localNames,
368
+ },
369
+ });
370
+ }
371
+ return violations;
372
+ }
373
+ /** Build directory → dominant error-handling shape from convention rows. */
374
+ function buildDirShapes(conventions) {
375
+ const dirShapes = new Map();
376
+ for (const conv of conventions) {
377
+ const dir = conv.directory ?? '.';
378
+ const shape = conv.pattern;
379
+ if (!shape)
380
+ continue;
381
+ dirShapes.set(dir, {
382
+ shape,
383
+ confidence: conv.confidence,
384
+ exemplar_file: conv.exemplar_file,
385
+ exemplar_line: conv.exemplar_line,
386
+ });
387
+ }
388
+ return dirShapes;
389
+ }
390
+ /** Detect an error-handling-shape deviation for one function row, if any. */
391
+ function detectErrorHandlingForRow(row, dirShapes, analyzerName) {
392
+ const directory = path.dirname(row.file_path) || '.';
393
+ const conv = dirShapes.get(directory);
394
+ if (!conv)
395
+ return null;
396
+ let metadata;
397
+ try {
398
+ metadata = JSON.parse(row.metadata_json);
399
+ }
400
+ catch {
401
+ return null;
402
+ }
403
+ const body = metadata.body;
404
+ const shape = detectErrorHandlingShape(body);
405
+ if (!shape)
406
+ return null; // no error handling → skip
407
+ if (shape === conv.shape)
408
+ return null; // matches convention
409
+ const pct = Math.round(conv.confidence * 100);
410
+ return {
411
+ file: row.file_path,
412
+ line: row.line_number,
413
+ column: 1,
414
+ severity: 'suggestion',
415
+ message: `${pct}% of error-handling functions in \`${directory}/\` use ` +
416
+ `\`${conv.shape}\` — this function uses \`${shape}\`${exemplarRef(conv)}`,
417
+ rule: 'conventions/error-handling',
418
+ analyzer: analyzerName,
419
+ functionName: row.name,
420
+ };
421
+ }
422
+ /** Build directory → dominant export form from convention rows. */
423
+ function buildDirForms(conventions) {
424
+ const dirForms = new Map();
425
+ for (const conv of conventions) {
426
+ const dir = conv.directory ?? '.';
427
+ const form = conv.pattern;
428
+ if (!form)
429
+ continue;
430
+ dirForms.set(dir, {
431
+ form,
432
+ confidence: conv.confidence,
433
+ exemplar_file: conv.exemplar_file,
434
+ exemplar_line: conv.exemplar_line,
435
+ });
436
+ }
437
+ return dirForms;
438
+ }
439
+ /** Detect an export-shape deviation for one function row, if any. */
440
+ function detectExportShapeForRow(row, dirForms, exportsMap, analyzerName) {
441
+ const directory = path.dirname(row.file_path) || '.';
442
+ const conv = dirForms.get(directory);
443
+ if (!conv)
444
+ return null;
445
+ // B2: Use AST-extracted ExportInfo[] from function-index facts
446
+ const fileExports = exportsMap?.get(row.file_path);
447
+ if (!fileExports)
448
+ return null;
449
+ const form = detectExportForm(row.name, fileExports);
450
+ if (!form || form === conv.form)
451
+ return null;
452
+ const pct = Math.round(conv.confidence * 100);
453
+ return {
454
+ file: row.file_path,
455
+ line: row.line_number,
456
+ column: 1,
457
+ severity: 'suggestion',
458
+ message: `${pct}% of exports in \`${directory}/\` use ${conv.form} export — ` +
459
+ `\`${row.name}\` uses ${form}${exemplarRef(conv)}`,
460
+ rule: 'conventions/export-shape',
461
+ analyzer: analyzerName,
462
+ functionName: row.name,
463
+ };
464
+ }
465
+ /** Build directory → kind → convention from naming convention rows. */
466
+ function buildDirKindCases(conventions) {
467
+ const dirKindCases = new Map();
468
+ for (const conv of conventions) {
469
+ const dir = conv.directory ?? '.';
470
+ const casing = conv.pattern;
471
+ if (!casing)
472
+ continue;
473
+ const kind = conv.export_kind ?? 'function';
474
+ if (!dirKindCases.has(dir))
475
+ dirKindCases.set(dir, new Map());
476
+ dirKindCases.get(dir).set(kind, {
477
+ casing,
478
+ confidence: conv.confidence,
479
+ exemplar_file: conv.exemplar_file,
480
+ exemplar_line: conv.exemplar_line,
481
+ kind,
482
+ });
483
+ }
484
+ return dirKindCases;
485
+ }
486
+ /** Classify an exported function into a naming kind (same logic as mineNaming). */
487
+ function classifyExportKind(row) {
488
+ if (row.entity_type === 'component' || row.component_type !== null) {
489
+ return 'react-component';
490
+ }
491
+ if (/^use[A-Z]/.test(row.name)) {
492
+ return 'hook';
493
+ }
494
+ return 'function';
495
+ }
496
+ /** Detect a naming-casing deviation for one function row, if any. */
497
+ function detectNamingForRow(row, dirKindCases, analyzerName) {
498
+ const directory = path.dirname(row.file_path) || '.';
499
+ const kindConvs = dirKindCases.get(directory);
500
+ if (!kindConvs)
501
+ return null;
502
+ const rowKind = classifyExportKind(row);
503
+ const conv = kindConvs.get(rowKind);
504
+ if (!conv)
505
+ return null;
506
+ // Non-Latin skip (Spec 21 R5.4)
507
+ if (hasNonLatinChars(row.name))
508
+ return null;
509
+ const casing = detectCase(row.name);
510
+ if (!casing || casing === conv.casing)
511
+ return null;
512
+ const pct = Math.round(conv.confidence * 100);
513
+ return {
514
+ file: row.file_path,
515
+ line: row.line_number,
516
+ column: 1,
517
+ severity: 'suggestion',
518
+ message: `${pct}% of ${conv.kind} exports in \`${directory}/\` use ${conv.casing} — ` +
519
+ `\`${row.name}\` uses ${casing}${exemplarRef(conv)}`,
520
+ rule: 'conventions/naming',
521
+ analyzer: analyzerName,
522
+ functionName: row.name,
523
+ };
445
524
  }
446
525
  //# sourceMappingURL=UniversalConventionsAnalyzer.js.map