code-auditor-mcp 3.4.11 → 3.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +119 -1
  3. package/dist/analyzers/__verify_self_probe.d.ts +2 -0
  4. package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
  5. package/dist/analyzers/__verify_self_probe.js +5 -0
  6. package/dist/analyzers/__verify_self_probe.js.map +1 -0
  7. package/dist/analyzers/analyzerUtils.d.ts +34 -1
  8. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  9. package/dist/analyzers/analyzerUtils.js +61 -22
  10. package/dist/analyzers/analyzerUtils.js.map +1 -1
  11. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  13. package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
  14. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  15. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
  16. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  17. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
  18. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  19. package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
  20. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  21. package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
  22. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  23. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
  24. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  25. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
  26. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  27. package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
  28. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  29. package/dist/analyzers/documentationAnalyzer.js +259 -198
  30. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  31. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  32. package/dist/analyzers/invariantsAnalyzer.js +59 -46
  33. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  34. package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
  35. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
  36. package/dist/analyzers/orm/adapterRegistry.js +10 -0
  37. package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
  38. package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
  39. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
  40. package/dist/analyzers/orm/drizzleAdapter.js +42 -44
  41. package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
  42. package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
  43. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
  44. package/dist/analyzers/orm/prismaAdapter.js +30 -0
  45. package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
  46. package/dist/analyzers/provenance.d.ts +72 -2
  47. package/dist/analyzers/provenance.d.ts.map +1 -1
  48. package/dist/analyzers/provenance.js +462 -338
  49. package/dist/analyzers/provenance.js.map +1 -1
  50. package/dist/analyzers/reactAnalyzer.d.ts +11 -0
  51. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  52. package/dist/analyzers/reactAnalyzer.js +188 -125
  53. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  54. package/dist/analyzers/ruleRegistry.d.ts +118 -1
  55. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  56. package/dist/analyzers/ruleRegistry.js +1717 -104
  57. package/dist/analyzers/ruleRegistry.js.map +1 -1
  58. package/dist/analyzers/ruleTiming.d.ts +54 -0
  59. package/dist/analyzers/ruleTiming.d.ts.map +1 -0
  60. package/dist/analyzers/ruleTiming.js +71 -0
  61. package/dist/analyzers/ruleTiming.js.map +1 -0
  62. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
  63. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  64. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
  65. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  66. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
  67. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  68. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
  69. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  70. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +24 -186
  71. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  72. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1081 -1061
  73. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  74. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
  75. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  76. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
  77. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  78. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
  79. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  80. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
  81. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  82. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -212
  83. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  84. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1518
  85. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  86. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
  87. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  88. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -472
  89. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  90. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
  91. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
  92. package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
  93. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
  94. package/dist/analyzers/universal/schema/config.d.ts +31 -0
  95. package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
  96. package/dist/analyzers/universal/schema/config.js +57 -0
  97. package/dist/analyzers/universal/schema/config.js.map +1 -0
  98. package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
  99. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
  100. package/dist/analyzers/universal/schema/discovery.js +616 -0
  101. package/dist/analyzers/universal/schema/discovery.js.map +1 -0
  102. package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
  103. package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
  104. package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
  105. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
  106. package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
  107. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
  108. package/dist/analyzers/universal/schema/migrations.js +238 -0
  109. package/dist/analyzers/universal/schema/migrations.js.map +1 -0
  110. package/dist/analyzers/universal/schema/types.d.ts +131 -0
  111. package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
  112. package/dist/analyzers/universal/schema/types.js +10 -0
  113. package/dist/analyzers/universal/schema/types.js.map +1 -0
  114. package/dist/analyzers/universal/schema/violations.d.ts +44 -0
  115. package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
  116. package/dist/analyzers/universal/schema/violations.js +54 -0
  117. package/dist/analyzers/universal/schema/violations.js.map +1 -0
  118. package/dist/auditRunner.d.ts.map +1 -1
  119. package/dist/auditRunner.js +101 -39
  120. package/dist/auditRunner.js.map +1 -1
  121. package/dist/cli.js +348 -40
  122. package/dist/cli.js.map +1 -1
  123. package/dist/config/configLoader.d.ts.map +1 -1
  124. package/dist/config/configLoader.js +9 -7
  125. package/dist/config/configLoader.js.map +1 -1
  126. package/dist/config/defaults.d.ts +12 -3
  127. package/dist/config/defaults.d.ts.map +1 -1
  128. package/dist/config/defaults.js +15 -5
  129. package/dist/config/defaults.js.map +1 -1
  130. package/dist/config/effectiveConfig.d.ts +75 -0
  131. package/dist/config/effectiveConfig.d.ts.map +1 -0
  132. package/dist/config/effectiveConfig.js +213 -0
  133. package/dist/config/effectiveConfig.js.map +1 -0
  134. package/dist/config/pathProfiles.d.ts +7 -4
  135. package/dist/config/pathProfiles.d.ts.map +1 -1
  136. package/dist/config/pathProfiles.js +8 -10
  137. package/dist/config/pathProfiles.js.map +1 -1
  138. package/dist/config/thresholdRationales.d.ts +45 -0
  139. package/dist/config/thresholdRationales.d.ts.map +1 -0
  140. package/dist/config/thresholdRationales.js +67 -0
  141. package/dist/config/thresholdRationales.js.map +1 -0
  142. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  143. package/dist/conventions/conventionMiner.js +6 -2
  144. package/dist/conventions/conventionMiner.js.map +1 -1
  145. package/dist/enforcement/diffGate.d.ts +55 -0
  146. package/dist/enforcement/diffGate.d.ts.map +1 -0
  147. package/dist/enforcement/diffGate.js +136 -0
  148. package/dist/enforcement/diffGate.js.map +1 -0
  149. package/dist/enforcement/gate.d.ts +36 -0
  150. package/dist/enforcement/gate.d.ts.map +1 -0
  151. package/dist/enforcement/gate.js +72 -0
  152. package/dist/enforcement/gate.js.map +1 -0
  153. package/dist/enforcement/suppressions.d.ts +41 -0
  154. package/dist/enforcement/suppressions.d.ts.map +1 -0
  155. package/dist/enforcement/suppressions.js +170 -0
  156. package/dist/enforcement/suppressions.js.map +1 -0
  157. package/dist/fingerprint.d.ts.map +1 -1
  158. package/dist/fingerprint.js +4 -1
  159. package/dist/fingerprint.js.map +1 -1
  160. package/dist/grammars/tree-sitter-scss.wasm +0 -0
  161. package/dist/invariants/ruleEngine.d.ts +3 -4
  162. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  163. package/dist/invariants/ruleEngine.js +2 -2
  164. package/dist/invariants/ruleEngine.js.map +1 -1
  165. package/dist/languages/LanguageOrchestrator.d.ts +14 -0
  166. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  167. package/dist/languages/LanguageOrchestrator.js +93 -55
  168. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  169. package/dist/languages/LanguageRegistry.d.ts +8 -0
  170. package/dist/languages/LanguageRegistry.d.ts.map +1 -1
  171. package/dist/languages/LanguageRegistry.js +8 -0
  172. package/dist/languages/LanguageRegistry.js.map +1 -1
  173. package/dist/languages/RuntimeManager.d.ts +82 -60
  174. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  175. package/dist/languages/RuntimeManager.js +404 -349
  176. package/dist/languages/RuntimeManager.js.map +1 -1
  177. package/dist/languages/UniversalAnalyzer.d.ts +51 -6
  178. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  179. package/dist/languages/UniversalAnalyzer.js +127 -88
  180. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  181. package/dist/languages/adapterBridge.d.ts +36 -0
  182. package/dist/languages/adapterBridge.d.ts.map +1 -1
  183. package/dist/languages/adapterBridge.js +89 -35
  184. package/dist/languages/adapterBridge.js.map +1 -1
  185. package/dist/languages/go/GoAdapter.d.ts +66 -23
  186. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  187. package/dist/languages/go/GoAdapter.js +553 -474
  188. package/dist/languages/go/GoAdapter.js.map +1 -1
  189. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +38 -9
  190. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
  191. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +111 -74
  192. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  193. package/dist/languages/tree-sitter/converter.d.ts +18 -0
  194. package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
  195. package/dist/languages/tree-sitter/converter.js +18 -0
  196. package/dist/languages/tree-sitter/converter.js.map +1 -1
  197. package/dist/languages/tree-sitter/parser.d.ts +39 -2
  198. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  199. package/dist/languages/tree-sitter/parser.js +95 -12
  200. package/dist/languages/tree-sitter/parser.js.map +1 -1
  201. package/dist/languages/types.d.ts +67 -2
  202. package/dist/languages/types.d.ts.map +1 -1
  203. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -62
  204. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  205. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1461 -791
  206. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  207. package/dist/mcp-tools/whitelistTools.d.ts +1 -1
  208. package/dist/mcp-tools-shared.d.ts.map +1 -1
  209. package/dist/mcp-tools-shared.js +1 -0
  210. package/dist/mcp-tools-shared.js.map +1 -1
  211. package/dist/mcpAuditJobs.d.ts.map +1 -1
  212. package/dist/mcpAuditJobs.js +1 -0
  213. package/dist/mcpAuditJobs.js.map +1 -1
  214. package/dist/pipeline.d.ts +30 -1
  215. package/dist/pipeline.d.ts.map +1 -1
  216. package/dist/pipeline.js +527 -203
  217. package/dist/pipeline.js.map +1 -1
  218. package/dist/pipelineAdapters.d.ts +5 -12
  219. package/dist/pipelineAdapters.d.ts.map +1 -1
  220. package/dist/pipelineAdapters.js +175 -100
  221. package/dist/pipelineAdapters.js.map +1 -1
  222. package/dist/presets/presets.d.ts +54 -0
  223. package/dist/presets/presets.d.ts.map +1 -0
  224. package/dist/presets/presets.js +181 -0
  225. package/dist/presets/presets.js.map +1 -0
  226. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  227. package/dist/reporting/sarifReportGenerator.js +0 -2
  228. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  229. package/dist/ruleAliases.d.ts +67 -0
  230. package/dist/ruleAliases.d.ts.map +1 -0
  231. package/dist/ruleAliases.js +79 -0
  232. package/dist/ruleAliases.js.map +1 -0
  233. package/dist/styles/cssAstExtractor.d.ts +20 -5
  234. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  235. package/dist/styles/cssAstExtractor.js +247 -9
  236. package/dist/styles/cssAstExtractor.js.map +1 -1
  237. package/dist/styles/styleIndexer.js +13 -4
  238. package/dist/styles/styleIndexer.js.map +1 -1
  239. package/dist/styles/tailwindConfigLoader.d.ts +8 -6
  240. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  241. package/dist/styles/tailwindConfigLoader.js +61 -15
  242. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  243. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  244. package/dist/styles/tailwindUtilityExpander.js +13 -1
  245. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  246. package/dist/types.d.ts +201 -1
  247. package/dist/types.d.ts.map +1 -1
  248. package/dist/types.js +8 -0
  249. package/dist/types.js.map +1 -1
  250. package/package.json +7 -4
  251. package/plugin/.claude-plugin/plugin.json +1 -1
  252. package/plugin/hooks/hooks.json +1 -1
  253. package/plugin/scripts/hook-audit.sh +25 -7
  254. package/plugin/skills/code-auditor/SKILL.md +5 -5
@@ -24,20 +24,135 @@ import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
24
24
  import { VALIDATOR_PACKAGES } from '../provenance.js';
25
25
  import { makeVisitorStatus } from '../../pipeline.js';
26
26
  // ---------------------------------------------------------------------------
27
+ // Call-graph helpers — pure functions with no `this` state, kept off the
28
+ // analyzer class to bound its aggregate cyclomatic complexity.
29
+ // ---------------------------------------------------------------------------
30
+ /**
31
+ * BFS through the call graph (graph_cache) up to maxDepth to check if
32
+ * any path from startFuncId reaches a validator function ID.
33
+ */
34
+ function bfsReachesValidator(indexHandle, startFuncId, validatorIds, maxDepth) {
35
+ const visited = new Set();
36
+ let currentLevel = [startFuncId];
37
+ for (let d = 0; d < maxDepth; d++) {
38
+ const nextLevel = [];
39
+ for (const funcId of currentLevel) {
40
+ if (validatorIds.has(funcId))
41
+ return true;
42
+ if (visited.has(funcId))
43
+ continue;
44
+ visited.add(funcId);
45
+ // Get callees from graph_cache call edges
46
+ const callees = indexHandle
47
+ .query(`SELECT neighbor_key FROM graph_cache
48
+ WHERE graph_type = 'call' AND node_key = ?`, [String(funcId)]);
49
+ for (const callee of callees) {
50
+ const calleeId = parseInt(callee.neighbor_key, 10);
51
+ if (!isNaN(calleeId) && !visited.has(calleeId)) {
52
+ nextLevel.push(calleeId);
53
+ }
54
+ }
55
+ }
56
+ currentLevel = nextLevel;
57
+ }
58
+ // Check any remaining nodes at the final level
59
+ for (const funcId of currentLevel) {
60
+ if (validatorIds.has(funcId))
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ /**
66
+ * Expand a writer's directly-written table set with the tables written by
67
+ * functions it calls (depth-1 callee expansion via graph_cache). Requires the
68
+ * call-graph infrastructure (graph_cache + functions tables, populated by
69
+ * code-audit index sync); without it, returns the direct write set unchanged.
70
+ */
71
+ function expandWrittenTables(indexHandle, key, initialTables, graph) {
72
+ const allTables = new Set(initialTables);
73
+ const { fnIdLookup, hasGraphData } = graph;
74
+ if (!fnIdLookup || !hasGraphData)
75
+ return allTables;
76
+ const funcId = fnIdLookup.get(key);
77
+ if (funcId === undefined)
78
+ return allTables;
79
+ const calleeRows = indexHandle
80
+ .query(`SELECT neighbor_key FROM graph_cache
81
+ WHERE graph_type = 'call' AND node_key = ?`, [String(funcId)]);
82
+ for (const callee of calleeRows) {
83
+ const calleeFuncs = indexHandle
84
+ .query(`SELECT name, file_path FROM functions WHERE id = ?`, [parseInt(callee.neighbor_key, 10)]);
85
+ for (const cf of calleeFuncs) {
86
+ const calleeTables = indexHandle
87
+ .query(`SELECT DISTINCT table_name FROM schema_usage
88
+ WHERE usage_type IN ('insert', 'update', 'delete', 'create')
89
+ AND function_name = ? AND file_path = ?`, [cf.name, cf.file_path]);
90
+ for (const ct of calleeTables) {
91
+ allTables.add(ct.table_name);
92
+ }
93
+ }
94
+ }
95
+ return allTables;
96
+ }
97
+ /**
98
+ * BFS outward from test-file function IDs through the call graph (graph_cache)
99
+ * up to maxDepth, collecting every function ID reached. Used for the
100
+ * static-reach fallback: a high-risk function not in this set is uncovered.
101
+ */
102
+ function collectReachableIds(indexHandle, startIds, maxDepth) {
103
+ const reachableIds = new Set();
104
+ for (const startId of startIds) {
105
+ const visited = new Set();
106
+ let currentLevel = [startId];
107
+ for (let d = 0; d < maxDepth; d++) {
108
+ const nextLevel = [];
109
+ for (const funcId of currentLevel) {
110
+ if (visited.has(funcId))
111
+ continue;
112
+ visited.add(funcId);
113
+ reachableIds.add(funcId);
114
+ const callees = indexHandle
115
+ .query(`SELECT neighbor_key FROM graph_cache
116
+ WHERE graph_type = 'call' AND node_key = ?`, [String(funcId)]);
117
+ for (const callee of callees) {
118
+ const calleeId = parseInt(callee.neighbor_key, 10);
119
+ if (!isNaN(calleeId) && !visited.has(calleeId)) {
120
+ nextLevel.push(calleeId);
121
+ }
122
+ }
123
+ }
124
+ currentLevel = nextLevel;
125
+ }
126
+ // Check the final level's nodes that never got expanded.
127
+ for (const funcId of currentLevel) {
128
+ if (!reachableIds.has(funcId))
129
+ reachableIds.add(funcId);
130
+ }
131
+ }
132
+ return reachableIds;
133
+ }
134
+ // ---------------------------------------------------------------------------
27
135
  // Analyzer
28
136
  // ---------------------------------------------------------------------------
137
+ const ANALYZER_NAME = 'cross-domain';
138
+ /**
139
+ * Cross domain analyzer.
140
+ */
29
141
  export class CrossDomainAnalyzer extends UniversalAnalyzer {
30
- name = 'cross-domain';
142
+ name = ANALYZER_NAME;
31
143
  description = 'Detects cross-domain issues (schema lifecycle, validation bypass, coverage gaps)';
32
144
  category = 'architecture';
33
145
  /**
34
146
  * Full override: query the code index DB for cross-domain findings.
35
147
  * The base-class per-file AST loop is bypassed — all detection is
36
148
  * post-analysis across the indexed data.
149
+ * @param config
150
+ * @param files
151
+ * @param options
152
+ * @returns
37
153
  */
38
154
  async analyze(files, config = {}, options = {}) {
39
155
  const startTime = Date.now();
40
- const violations = [];
41
156
  const indexHandle = config.indexHandle;
42
157
  if (!indexHandle) {
43
158
  return {
@@ -45,586 +160,568 @@ export class CrossDomainAnalyzer extends UniversalAnalyzer {
45
160
  errors: [{ file: '', error: 'Failed to open code index database' }],
46
161
  status: makeVisitorStatus(0),
47
162
  executionTime: Date.now() - startTime,
48
- analyzerName: 'cross-domain',
163
+ analyzerName: ANALYZER_NAME,
49
164
  metrics: { filesAnalyzed: 0, totalViolations: 0, executionTime: Date.now() - startTime },
50
165
  };
51
166
  }
52
- if (!indexHandle) {
53
- return {
54
- violations: [],
55
- errors: [],
56
- status: makeVisitorStatus(files.length),
167
+ const filePathClause = this.resolveFilePathClause(config);
168
+ const violations = this.runDetectors(indexHandle, config, filePathClause);
169
+ const uniqueFiles = this.countDistinctFiles(indexHandle, filePathClause);
170
+ return {
171
+ violations,
172
+ errors: [],
173
+ status: makeVisitorStatus(uniqueFiles || files.length),
174
+ executionTime: Date.now() - startTime,
175
+ analyzerName: ANALYZER_NAME,
176
+ metrics: {
177
+ filesAnalyzed: uniqueFiles || files.length,
178
+ totalViolations: violations.length,
57
179
  executionTime: Date.now() - startTime,
58
- analyzerName: 'cross-domain',
59
- metrics: { filesAnalyzed: files.length, totalViolations: 0, executionTime: Date.now() - startTime },
60
- };
61
- }
62
- // Project root scoping: in test environments the in-memory DB singleton
63
- // is shared across tests, so schema_usage entries from previous test
64
- // cases leak into subsequent queries. Filter to the current project root.
180
+ },
181
+ };
182
+ }
183
+ /**
184
+ * Project root scoping: in test environments the in-memory DB singleton is
185
+ * shared across tests, so schema_usage entries from previous test cases leak
186
+ * into subsequent queries. Build a file-path LIKE clause scoped to root.
187
+ */
188
+ resolveFilePathClause(config) {
65
189
  const projectRoot = config.projectRoot;
66
190
  const resolvedRoot = projectRoot ? path.resolve(projectRoot) : undefined;
67
- const filePathClause = resolvedRoot
68
- ? { prefix: resolvedRoot, clause: 'AND file_path LIKE ?', param: `${resolvedRoot}%` }
191
+ return resolvedRoot
192
+ ? { clause: 'AND file_path LIKE ?', param: `${resolvedRoot}%` }
69
193
  : undefined;
194
+ }
195
+ /** Run the R1/R3/R4 detectors and collect their violations. */
196
+ runDetectors(indexHandle, config, filePathClause) {
197
+ const violations = [];
70
198
  // R1 — Schema lifecycle detectors
71
199
  const lifecycle = config.schemaLifecycle ?? {};
72
200
  if (lifecycle.enableWrittenNeverRead !== false) {
73
- violations.push(...this.detectWrittenNeverRead(indexHandle, filePathClause));
201
+ violations.push(...detectWrittenNeverRead(indexHandle, filePathClause));
74
202
  }
75
203
  if (lifecycle.enableReadNeverWritten !== false) {
76
- violations.push(...this.detectReadNeverWritten(indexHandle, filePathClause));
204
+ violations.push(...detectReadNeverWritten(indexHandle, filePathClause));
77
205
  }
78
206
  if (lifecycle.enableTransactionBoundaryRisk !== false) {
79
207
  const txnTableMax = lifecycle.txnTableMax ?? 4;
80
- violations.push(...this.detectTransactionBoundaryRisk(indexHandle, txnTableMax, filePathClause));
208
+ violations.push(...detectTransactionBoundaryRisk(indexHandle, txnTableMax, filePathClause));
81
209
  }
82
210
  // R3 — Validation-bypass detection
83
211
  const bypass = config.validatorBypass;
84
212
  if (bypass) {
85
- violations.push(...this.detectValidationBypass(indexHandle, bypass, filePathClause));
213
+ violations.push(...detectValidationBypass(indexHandle, bypass, filePathClause));
86
214
  }
87
215
  // R4 — Coverage by importance
88
216
  const coverage = config.coverage;
89
217
  if (coverage) {
90
- violations.push(...this.detectUncoveredRisk(indexHandle, coverage, filePathClause));
218
+ violations.push(...detectUncoveredRisk(indexHandle, coverage, filePathClause));
91
219
  }
92
- // Count distinct files with schema_usage entries
93
- const fileRows = indexHandle
220
+ return violations;
221
+ }
222
+ /** Count distinct files with schema_usage entries. */
223
+ countDistinctFiles(indexHandle, filePathClause) {
224
+ const rows = indexHandle
94
225
  .query(filePathClause
95
226
  ? `SELECT COUNT(DISTINCT file_path) as cnt FROM schema_usage WHERE 1=1 ${filePathClause.clause}`
96
227
  : 'SELECT COUNT(DISTINCT file_path) as cnt FROM schema_usage', (filePathClause ? [filePathClause.param] : []));
97
- const uniqueFiles = fileRows[0]?.cnt ?? 0;
98
- return {
99
- violations,
100
- errors: [],
101
- status: makeVisitorStatus(uniqueFiles || files.length),
102
- executionTime: Date.now() - startTime,
103
- analyzerName: 'cross-domain',
104
- metrics: {
105
- filesAnalyzed: uniqueFiles || files.length,
106
- totalViolations: violations.length,
107
- executionTime: Date.now() - startTime,
108
- },
109
- };
228
+ return rows[0]?.cnt ?? 0;
110
229
  }
111
230
  /** No-op — all detection is DB-based. */
112
231
  async analyzeAST() {
113
232
  return [];
114
233
  }
115
- // ── R1: Written-Never-Read ──────────────────────────────────────────────
116
- /**
117
- * Detect tables that are written to (INSERT/UPDATE/DELETE/CREATE) but
118
- * never read from (SELECT). These might be dead writes or missed read paths.
119
- */
120
- detectWrittenNeverRead(indexHandle, filePath) {
121
- const violations = [];
122
- const fpWhere = filePath ? filePath.clause : '';
123
- const rows = indexHandle
124
- .query(`SELECT DISTINCT table_name, file_path, function_name, line, usage_type
125
- FROM schema_usage
126
- WHERE usage_type IN ('insert', 'update', 'delete', 'create')
127
- ${fpWhere}
128
- AND table_name NOT IN (
129
- SELECT DISTINCT table_name FROM schema_usage WHERE usage_type = 'select' ${fpWhere}
130
- )
131
- ORDER BY table_name, file_path`, (filePath ? [filePath.param, filePath.param] : []));
132
- // Deduplicate by table_name one violation per table, anchored to
133
- // the first writing file encountered.
134
- const seen = new Set();
135
- for (const row of rows) {
136
- if (seen.has(row.table_name))
137
- continue;
138
- seen.add(row.table_name);
139
- violations.push({
140
- file: row.file_path,
234
+ }
235
+ // ── R1: Written-Never-Read ──────────────────────────────────────────────
236
+ /**
237
+ * Detect tables that are written to (INSERT/UPDATE/DELETE/CREATE) but
238
+ * never read from (SELECT). These might be dead writes or missed read paths.
239
+ */
240
+ function detectWrittenNeverRead(indexHandle, filePath) {
241
+ const violations = [];
242
+ const fpWhere = filePath ? filePath.clause : '';
243
+ const rows = indexHandle
244
+ .query(`SELECT DISTINCT table_name, file_path, function_name, line, usage_type
245
+ FROM schema_usage
246
+ WHERE usage_type IN ('insert', 'update', 'delete', 'create')
247
+ ${fpWhere}
248
+ AND table_name NOT IN (
249
+ SELECT DISTINCT table_name FROM schema_usage WHERE usage_type = 'select' ${fpWhere}
250
+ )
251
+ ORDER BY table_name, file_path`, (filePath ? [filePath.param, filePath.param] : []));
252
+ // Deduplicate by table_name one violation per table, anchored to
253
+ // the first writing file encountered.
254
+ const seen = new Set();
255
+ for (const row of rows) {
256
+ if (seen.has(row.table_name))
257
+ continue;
258
+ seen.add(row.table_name);
259
+ violations.push({
260
+ file: row.file_path,
261
+ line: row.line,
262
+ column: 0,
263
+ severity: 'suggestion',
264
+ message: `Table '${row.table_name}' is written (${row.usage_type}) but never read (SELECT). Consider removing unused writes or adding read paths.`,
265
+ rule: 'cross-domain/written-never-read',
266
+ analyzer: ANALYZER_NAME,
267
+ functionName: row.function_name,
268
+ });
269
+ }
270
+ return violations;
271
+ }
272
+ // ── R1: Read-Never-Written ──────────────────────────────────────────────
273
+ /**
274
+ * Detect tables that are read from (SELECT) but never written to
275
+ * (INSERT/UPDATE/DELETE/CREATE). These may be external/managed tables
276
+ * or indicate missing write coverage.
277
+ */
278
+ function detectReadNeverWritten(indexHandle, filePath) {
279
+ const violations = [];
280
+ const fpWhere = filePath ? filePath.clause : '';
281
+ const rows = indexHandle
282
+ .query(`SELECT DISTINCT table_name, file_path, function_name, line, usage_type
283
+ FROM schema_usage
284
+ WHERE usage_type = 'select'
285
+ ${fpWhere}
286
+ AND table_name NOT IN (
287
+ SELECT DISTINCT table_name FROM schema_usage
288
+ WHERE usage_type IN ('insert', 'update', 'delete', 'create') ${fpWhere}
289
+ )
290
+ ORDER BY table_name, file_path`, (filePath ? [filePath.param, filePath.param] : []));
291
+ const seen = new Set();
292
+ for (const row of rows) {
293
+ if (seen.has(row.table_name))
294
+ continue;
295
+ seen.add(row.table_name);
296
+ violations.push({
297
+ file: row.file_path,
298
+ line: row.line,
299
+ column: 0,
300
+ severity: 'suggestion',
301
+ message: `Table '${row.table_name}' is read (SELECT) but never written (INSERT/UPDATE/DELETE). This may be an external/managed table, or indicate missing write coverage.`,
302
+ rule: 'cross-domain/read-never-written',
303
+ analyzer: ANALYZER_NAME,
304
+ functionName: row.function_name,
305
+ });
306
+ }
307
+ return violations;
308
+ }
309
+ // ── R1: Transaction-Boundary Risk ───────────────────────────────────────
310
+ /**
311
+ * Detect functions that write to ≥ txnTableMax distinct tables, including
312
+ * tables written by depth-1 callees via the call graph (graph_cache).
313
+ *
314
+ * This surfaces functions that may have transaction-boundary risk:
315
+ * writing to too many tables in a single logical operation can lead to
316
+ * long-running transactions, lock contention, and partial-failure complexity.
317
+ */
318
+ function detectTransactionBoundaryRisk(indexHandle, txnTableMax, filePath) {
319
+ const fpWhere = filePath ? filePath.clause : '';
320
+ const fpParams = filePath ? [filePath.param] : [];
321
+ // Query schema_usage directly (no JOIN on functions) so this detector
322
+ // works whether or not deepSync has populated the functions table.
323
+ const writerRows = indexHandle
324
+ .query(`SELECT su.function_name, su.file_path, su.table_name, MIN(su.line) as line
325
+ FROM schema_usage su
326
+ WHERE su.usage_type IN ('insert', 'update', 'delete', 'create')
327
+ ${fpWhere}
328
+ GROUP BY su.function_name, su.file_path, su.table_name
329
+ ORDER BY su.function_name, su.file_path`, fpParams);
330
+ if (writerRows.length === 0)
331
+ return [];
332
+ const funcWrites = groupWriterTables(writerRows);
333
+ const graph = resolveCallGraphContext(indexHandle);
334
+ return flagTransactionBoundaryWrites(funcWrites, indexHandle, graph, txnTableMax);
335
+ }
336
+ /** Group written tables by (file_path, function_name) key. */
337
+ function groupWriterTables(writerRows) {
338
+ const funcWrites = new Map();
339
+ for (const row of writerRows) {
340
+ const key = `${row.file_path}::${row.function_name}`;
341
+ const entry = funcWrites.get(key);
342
+ if (entry) {
343
+ entry.tables.add(row.table_name);
344
+ }
345
+ else {
346
+ funcWrites.set(key, {
347
+ filePath: row.file_path,
141
348
  line: row.line,
142
- column: 0,
143
- severity: 'suggestion',
144
- message: `Table '${row.table_name}' is written (${row.usage_type}) but never read (SELECT). Consider removing unused writes or adding read paths.`,
145
- rule: 'cross-domain/written-never-read',
146
- analyzer: this.name,
147
- functionName: row.function_name,
349
+ tables: new Set([row.table_name]),
148
350
  });
149
351
  }
150
- return violations;
151
352
  }
152
- // ── R1: Read-Never-Written ──────────────────────────────────────────────
153
- /**
154
- * Detect tables that are read from (SELECT) but never written to
155
- * (INSERT/UPDATE/DELETE/CREATE). These may be external/managed tables
156
- * or indicate missing write coverage.
157
- */
158
- detectReadNeverWritten(indexHandle, filePath) {
159
- const violations = [];
160
- const fpWhere = filePath ? filePath.clause : '';
161
- const rows = indexHandle
162
- .query(`SELECT DISTINCT table_name, file_path, function_name, line, usage_type
163
- FROM schema_usage
164
- WHERE usage_type = 'select'
165
- ${fpWhere}
166
- AND table_name NOT IN (
167
- SELECT DISTINCT table_name FROM schema_usage
168
- WHERE usage_type IN ('insert', 'update', 'delete', 'create') ${fpWhere}
169
- )
170
- ORDER BY table_name, file_path`, (filePath ? [filePath.param, filePath.param] : []));
171
- const seen = new Set();
172
- for (const row of rows) {
173
- if (seen.has(row.table_name))
174
- continue;
175
- seen.add(row.table_name);
353
+ return funcWrites;
354
+ }
355
+ /**
356
+ * Flag functions whose depth-1-expanded write set reaches txnTableMax.
357
+ * When graph_cache is unpopulated, expandWrittenTables degrades gracefully
358
+ * to reporting direct writes only.
359
+ */
360
+ function flagTransactionBoundaryWrites(funcWrites, indexHandle, graph, txnTableMax) {
361
+ const violations = [];
362
+ for (const [key, funcData] of funcWrites) {
363
+ const allTables = expandWrittenTables(indexHandle, key, funcData.tables, graph);
364
+ if (allTables.size >= txnTableMax) {
365
+ const tableList = [...allTables].sort().join(', ');
176
366
  violations.push({
177
- file: row.file_path,
178
- line: row.line,
367
+ file: funcData.filePath,
368
+ line: funcData.line,
179
369
  column: 0,
180
370
  severity: 'suggestion',
181
- message: `Table '${row.table_name}' is read (SELECT) but never written (INSERT/UPDATE/DELETE). This may be an external/managed table, or indicate missing write coverage.`,
182
- rule: 'cross-domain/read-never-written',
183
- analyzer: this.name,
184
- functionName: row.function_name,
371
+ message: `Function writes to ${allTables.size} distinct tables (threshold: ${txnTableMax}): ${tableList}. This may indicate transaction-boundary risk consider splitting writes across smaller transactional scopes.`,
372
+ rule: 'cross-domain/transaction-boundary',
373
+ analyzer: ANALYZER_NAME,
374
+ functionName: key.split('::')[1],
185
375
  });
186
376
  }
187
- return violations;
188
377
  }
189
- // ── R1: Transaction-Boundary Risk ───────────────────────────────────────
190
- /**
191
- * Detect functions that write to ≥ txnTableMax distinct tables, including
192
- * tables written by depth-1 callees via the call graph (graph_cache).
193
- *
194
- * This surfaces functions that may have transaction-boundary risk:
195
- * writing to too many tables in a single logical operation can lead to
196
- * long-running transactions, lock contention, and partial-failure complexity.
197
- */
198
- detectTransactionBoundaryRisk(indexHandle, txnTableMax, filePath) {
199
- const violations = [];
200
- const fpWhere = filePath ? filePath.clause : '';
201
- const fpParams = filePath ? [filePath.param] : [];
202
- // ── 1. Group writes by (function_name, file_path) no JOIN on functions.
203
- // The functions table is only populated during deepSync (code-audit index
204
- // sync), not during normal audit. We query schema_usage directly so this
205
- // detector works in both modes.
206
- const writerRows = indexHandle
207
- .query(`SELECT su.function_name, su.file_path, su.table_name, MIN(su.line) as line
208
- FROM schema_usage su
209
- WHERE su.usage_type IN ('insert', 'update', 'delete', 'create')
210
- ${fpWhere}
211
- GROUP BY su.function_name, su.file_path, su.table_name
212
- ORDER BY su.function_name, su.file_path`, fpParams);
213
- if (writerRows.length === 0)
214
- return violations;
215
- const funcWrites = new Map();
216
- for (const row of writerRows) {
217
- const key = `${row.file_path}::${row.function_name}`;
218
- const entry = funcWrites.get(key);
219
- if (entry) {
220
- entry.tables.add(row.table_name);
221
- }
222
- else {
223
- funcWrites.set(key, {
224
- filePath: row.file_path,
225
- line: row.line,
226
- tables: new Set([row.table_name]),
227
- });
228
- }
229
- }
230
- // ── 2. Attempt callee expansion via graph_cache when the infrastructure
231
- // is populated (requires code-audit index sync). When the functions or
232
- // graph_cache tables are empty/missing, we fall back to reporting on
233
- // direct writes only — the basic signal is still useful without the
234
- // call-graph context.
235
- const hasGraphData = (() => {
236
- try {
237
- const row = indexHandle.query("SELECT COUNT(*) AS n FROM graph_cache WHERE graph_type = 'call'");
238
- const cnt = row[0];
239
- return (cnt?.n ?? 0) > 0;
240
- }
241
- catch {
242
- return false;
243
- }
244
- })();
245
- // Build a lookup of function IDs from the functions table (only if it
246
- // has data — i.e., deepSync has been run).
247
- let fnIdLookup = null;
248
- if (hasGraphData) {
249
- try {
250
- const fnRows = indexHandle.query('SELECT id, name, file_path FROM functions');
251
- if (fnRows.length > 0) {
252
- fnIdLookup = new Map();
253
- for (const r of fnRows) {
254
- fnIdLookup.set(`${r.file_path}::${r.name}`, r.id);
255
- }
378
+ return violations;
379
+ }
380
+ /**
381
+ * Resolve the call-graph infrastructure available for callee expansion:
382
+ * whether graph_cache has any 'call' edges, and a function key → id lookup
383
+ * built from the functions table (populated only by deepSync). Any failure
384
+ * degrades gracefully to direct-write-only detection.
385
+ */
386
+ function resolveCallGraphContext(indexHandle) {
387
+ let hasGraphData = false;
388
+ try {
389
+ const row = indexHandle.query("SELECT COUNT(*) AS n FROM graph_cache WHERE graph_type = 'call'");
390
+ const cnt = row[0];
391
+ hasGraphData = (cnt?.n ?? 0) > 0;
392
+ }
393
+ catch {
394
+ hasGraphData = false;
395
+ }
396
+ let fnIdLookup = null;
397
+ if (hasGraphData) {
398
+ try {
399
+ const fnRows = indexHandle.query('SELECT id, name, file_path FROM functions');
400
+ if (fnRows.length > 0) {
401
+ fnIdLookup = new Map();
402
+ for (const r of fnRows) {
403
+ fnIdLookup.set(`${r.file_path}::${r.name}`, r.id);
256
404
  }
257
405
  }
258
- catch {
259
- // functions table might not exist or be unpopulated
260
- }
261
406
  }
262
- for (const [key, funcData] of funcWrites) {
263
- const allTables = new Set(funcData.tables);
264
- // Depth-1 callee expansion: only when graph_cache and functions tables
265
- // are both populated. Without deepSync, this is skipped gracefully.
266
- if (fnIdLookup && hasGraphData) {
267
- const funcId = fnIdLookup.get(key);
268
- if (funcId !== undefined) {
269
- const calleeRows = indexHandle
270
- .query(`SELECT neighbor_key FROM graph_cache
271
- WHERE graph_type = 'call' AND node_key = ?`, [String(funcId)]);
272
- for (const callee of calleeRows) {
273
- const calleeFuncs = indexHandle
274
- .query(`SELECT name, file_path FROM functions WHERE id = ?`, [parseInt(callee.neighbor_key, 10)]);
275
- for (const cf of calleeFuncs) {
276
- const calleeTables = indexHandle
277
- .query(`SELECT DISTINCT table_name FROM schema_usage
278
- WHERE usage_type IN ('insert', 'update', 'delete', 'create')
279
- AND function_name = ? AND file_path = ?`, [cf.name, cf.file_path]);
280
- for (const ct of calleeTables) {
281
- allTables.add(ct.table_name);
282
- }
283
- }
284
- }
285
- }
286
- }
287
- if (allTables.size >= txnTableMax) {
288
- const tableList = [...allTables].sort().join(', ');
289
- violations.push({
290
- file: funcData.filePath,
291
- line: funcData.line,
292
- column: 0,
293
- severity: 'suggestion',
294
- message: `Function writes to ${allTables.size} distinct tables (threshold: ${txnTableMax}): ${tableList}. This may indicate transaction-boundary risk — consider splitting writes across smaller transactional scopes.`,
295
- rule: 'cross-domain/transaction-boundary',
296
- analyzer: this.name,
297
- functionName: key.split('::')[1],
298
- });
299
- }
407
+ catch {
408
+ // functions table might not exist or be unpopulated
300
409
  }
410
+ }
411
+ return { hasGraphData, fnIdLookup };
412
+ }
413
+ // ── R3: Validation-Bypass ────────────────────────────────────────────────
414
+ /**
415
+ * Detect write functions that don't reach a validator, when a majority of
416
+ * peer writers in the same directory do (BFS ≤ configurable depth).
417
+ *
418
+ * Validator identification (priority order):
419
+ * 1. User-configured validators list
420
+ * 2. Provenanced: exported functions in files importing VALIDATOR_PACKAGES
421
+ * 3. Heuristic fallback: name GLOB 'validate*' OR 'assert*' (when both
422
+ * above are silent)
423
+ */
424
+ function detectValidationBypass(indexHandle, config, filePath) {
425
+ const violations = [];
426
+ const { validators: userValidators = [], modeShare = 0.8, minCorpus = 20, depth = 3, } = config;
427
+ const validatorIds = buildValidatorIds(indexHandle, userValidators);
428
+ if (validatorIds.size === 0)
429
+ return violations;
430
+ const fpAliasWhere = filePath ? `AND su.${filePath.clause.slice(4)}` : '';
431
+ const writers = indexHandle
432
+ .query(`SELECT DISTINCT su.function_name, su.file_path, su.line, f.id as function_id
433
+ FROM schema_usage su
434
+ JOIN functions f ON f.name = su.function_name
435
+ AND f.file_path = su.file_path
436
+ WHERE su.usage_type IN ('insert', 'update', 'delete', 'create')
437
+ ${fpAliasWhere}
438
+ ORDER BY su.file_path, su.function_name`, (filePath ? [filePath.param] : []));
439
+ if (writers.length === 0)
301
440
  return violations;
441
+ const writerCoverage = computeWriterCoverage(writers, validatorIds, indexHandle, depth);
442
+ violations.push(...flagUncoveredWriters(writers, writerCoverage, { minCorpus, modeShare, depth }));
443
+ return violations;
444
+ }
445
+ /** BFS from each writer to check validator reach, deduplicated by key. */
446
+ function computeWriterCoverage(writers, validatorIds, indexHandle, depth) {
447
+ const writerCoverage = new Map();
448
+ for (const w of writers) {
449
+ const key = `${w.file_path}::${w.function_name}`;
450
+ if (writerCoverage.has(key))
451
+ continue; // deduplicate
452
+ const covered = bfsReachesValidator(indexHandle, w.function_id, validatorIds, depth);
453
+ writerCoverage.set(key, {
454
+ covered,
455
+ line: w.line,
456
+ funcName: w.function_name,
457
+ });
302
458
  }
303
- // ── R3: Validation-Bypass ────────────────────────────────────────────────
304
- /**
305
- * Detect write functions that don't reach a validator, when a majority of
306
- * peer writers in the same directory do (BFS configurable depth).
307
- *
308
- * Validator identification (priority order):
309
- * 1. User-configured validators list
310
- * 2. Provenanced: exported functions in files importing VALIDATOR_PACKAGES
311
- * 3. Heuristic fallback: name GLOB 'validate*' OR 'assert*' (when both
312
- * above are silent)
313
- */
314
- detectValidationBypass(indexHandle, config, filePath) {
315
- const violations = [];
316
- const { validators: userValidators = [], modeShare = 0.8, minCorpus = 20, depth = 3, } = config;
317
- // ── 1. Build the validator function ID set ─────────────────────────────
318
- const validatorIds = new Set();
319
- // 1a. User-configured validators (format: "funcName" or "path#funcName")
320
- for (const v of userValidators) {
321
- const hashIdx = v.indexOf('#');
322
- if (hashIdx >= 0) {
323
- const vPath = v.substring(0, hashIdx);
324
- const vName = v.substring(hashIdx + 1);
325
- const rows = indexHandle
326
- .query('SELECT id FROM functions WHERE name = ? AND file_path = ?', [vName, vPath]);
327
- for (const r of rows)
328
- validatorIds.add(r.id);
329
- }
330
- else {
331
- const rows = indexHandle
332
- .query('SELECT id FROM functions WHERE name = ?', [v]);
333
- for (const r of rows)
334
- validatorIds.add(r.id);
335
- }
336
- }
337
- // 1b. Provenanced validators: exported functions whose OWN used_imports
338
- // includes a validator package. Per-identifier check — a function in
339
- // a zod-importing file only qualifies if its own body uses the
340
- // validator package, not just because it cohabits the file.
341
- if (validatorIds.size === 0) {
342
- const likeClauses = [...VALIDATOR_PACKAGES].map(() => 'used_imports LIKE ?');
343
- const likeParams = [...VALIDATOR_PACKAGES].map((pkg) => `%"${pkg}"%`);
344
- const validatorFuncs = indexHandle
345
- .query(`SELECT id FROM functions
346
- WHERE used_imports IS NOT NULL
347
- AND (${likeClauses.join(' OR ')})
348
- AND is_exported = 1`, likeParams);
349
- for (const f of validatorFuncs)
350
- validatorIds.add(f.id);
351
- }
352
- // 1c. Heuristic fallback: name-based matching (only when provenance
353
- // found nothing AND no user-configured validators exist).
354
- if (validatorIds.size === 0 && userValidators.length === 0) {
355
- const heuristicFuncs = indexHandle
356
- .query(`SELECT id FROM functions
357
- WHERE (name GLOB 'validate*' OR name GLOB 'assert*')
358
- AND is_exported = 1`);
359
- for (const f of heuristicFuncs)
360
- validatorIds.add(f.id);
361
- }
362
- if (validatorIds.size === 0)
363
- return violations;
364
- // ── 2. Find all writer functions from schema_usage ─────────────────────
365
- const fpAliasWhere = filePath
366
- ? `AND su.${filePath.clause.slice(4)}`
367
- : '';
368
- const writers = indexHandle
369
- .query(`SELECT DISTINCT su.function_name, su.file_path, su.line, f.id as function_id
370
- FROM schema_usage su
371
- JOIN functions f ON f.name = su.function_name
372
- AND f.file_path = su.file_path
373
- WHERE su.usage_type IN ('insert', 'update', 'delete', 'create')
374
- ${fpAliasWhere}
375
- ORDER BY su.file_path, su.function_name`, (filePath ? [filePath.param] : []));
376
- if (writers.length === 0)
377
- return violations;
378
- const writerCoverage = new Map();
379
- for (const w of writers) {
380
- const key = `${w.file_path}::${w.function_name}`;
381
- if (writerCoverage.has(key))
382
- continue; // deduplicate
383
- const covered = this.bfsReachesValidator(indexHandle, w.function_id, validatorIds, depth);
384
- writerCoverage.set(key, {
385
- covered,
386
- line: w.line,
387
- funcName: w.function_name,
388
- });
389
- }
390
- const dirWriters = new Map();
391
- const dirSeen = new Set(); // deduplicate writers with multiple schema_usage rows
392
- for (const w of writers) {
393
- const dir = path.dirname(w.file_path);
394
- const key = `${w.file_path}::${w.function_name}`;
395
- const dirKey = `${dir}::${key}`;
396
- if (dirSeen.has(dirKey))
397
- continue;
398
- dirSeen.add(dirKey);
399
- const cov = writerCoverage.get(key);
400
- if (!cov)
401
- continue;
402
- if (!dirWriters.has(dir))
403
- dirWriters.set(dir, []);
404
- dirWriters.get(dir).push({
405
- key,
406
- covered: cov.covered,
407
- line: cov.line,
408
- funcName: cov.funcName,
409
- filePath: w.file_path,
410
- });
459
+ return writerCoverage;
460
+ }
461
+ /**
462
+ * Build the validator function ID set in priority order: user-configured
463
+ * validators, then provenanced validators (exported functions whose own
464
+ * used_imports includes a validator package), then a name-based heuristic
465
+ * fallback only when both prior sources are silent.
466
+ */
467
+ function buildValidatorIds(indexHandle, userValidators) {
468
+ const validatorIds = new Set();
469
+ // 1a. User-configured validators (format: "funcName" or "path#funcName")
470
+ for (const v of userValidators) {
471
+ const hashIdx = v.indexOf('#');
472
+ if (hashIdx >= 0) {
473
+ const vPath = v.substring(0, hashIdx);
474
+ const vName = v.substring(hashIdx + 1);
475
+ const rows = indexHandle
476
+ .query('SELECT id FROM functions WHERE name = ? AND file_path = ?', [vName, vPath]);
477
+ for (const r of rows)
478
+ validatorIds.add(r.id);
411
479
  }
412
- // ── 5. Flag uncovered writers in validator-dense directories ───────────
413
- for (const [dir, dirWriterList] of dirWriters) {
414
- if (dirWriterList.length < minCorpus)
415
- continue;
416
- const coveredCount = dirWriterList.filter((w) => w.covered).length;
417
- const ratio = coveredCount / dirWriterList.length;
418
- if (ratio >= modeShare) {
419
- for (const w of dirWriterList) {
420
- if (w.covered)
421
- continue;
422
- violations.push({
423
- file: w.filePath,
424
- line: w.line,
425
- column: 0,
426
- severity: 'suggestion',
427
- message: `Function '${w.funcName}' is not validated. ` +
428
- `${coveredCount}/${dirWriterList.length} peer writers in '${dir}' ` +
429
- `reach a validator but this function does not (BFS depth ≤ ${depth}). ` +
430
- `Consider adding input validation.`,
431
- rule: 'cross-domain/validation-bypass',
432
- analyzer: this.name,
433
- functionName: w.funcName,
434
- });
435
- }
436
- }
480
+ else {
481
+ const rows = indexHandle
482
+ .query('SELECT id FROM functions WHERE name = ?', [v]);
483
+ for (const r of rows)
484
+ validatorIds.add(r.id);
437
485
  }
438
- return violations;
439
486
  }
440
- // ── R4: Coverage by Importance ──────────────────────────────────────────
441
- /**
442
- * Detect exported, high-risk functions with no test coverage.
443
- * Uses the getUntestedTopDecile() query that joins functions with
444
- * hotspot_scores and coverage_data to find untested high-risk functions.
445
- *
446
- * Static-reach fallback: when no measured coverage exists, falls back
447
- * to static reach analysis from test files (BFS from test-file functions
448
- * into the call graph).
449
- */
450
- detectUncoveredRisk(indexHandle, coverage, filePath) {
451
- const violations = [];
452
- const topRiskDecile = coverage.topRiskDecile ?? 0.1;
453
- // Check if any measured coverage exists
454
- const measuredCount = indexHandle
455
- .query("SELECT COUNT(*) AS cnt FROM coverage_data WHERE basis = 'measured'")[0].cnt;
456
- if (measuredCount > 0) {
457
- // Use measured coverage data from lcov/istanbul imports
458
- const untested = indexHandle.getUntestedTopDecile(topRiskDecile);
459
- // Determine source format from existing coverage entries
460
- const sourceRow = indexHandle
461
- .query("SELECT source, imported_at FROM coverage_data WHERE basis = 'measured' LIMIT 1")[0];
462
- const sourceFormat = sourceRow?.source ?? 'unknown';
463
- const importedAt = sourceRow?.imported_at ?? null;
464
- // Stale-import detection: measured coverage predates last full index sync
465
- let staleWarning = null;
466
- if (importedAt) {
467
- const lastSync = indexHandle.getMeta?.('last_full_sync_timestamp') ?? null;
468
- if (lastSync && importedAt < lastSync) {
469
- staleWarning =
470
- ` — WARNING: this coverage data may be stale (imported ${importedAt}, ` +
471
- `last index sync was ${lastSync}). ` +
472
- `Re-import with 'code-audit coverage --import <path>' for accurate results.`;
473
- }
474
- }
475
- for (const fn of untested) {
487
+ // 1b. Provenanced validators: exported functions whose OWN used_imports
488
+ // includes a validator package. Per-identifier check — a function in
489
+ // a zod-importing file only qualifies if its own body uses the
490
+ // validator package, not just because it cohabits the file.
491
+ if (validatorIds.size === 0) {
492
+ const likeClauses = [...VALIDATOR_PACKAGES].map(() => 'used_imports LIKE ?');
493
+ const likeParams = [...VALIDATOR_PACKAGES].map((pkg) => `%"${pkg}"%`);
494
+ const validatorFuncs = indexHandle
495
+ .query(`SELECT id FROM functions
496
+ WHERE used_imports IS NOT NULL
497
+ AND (${likeClauses.join(' OR ')})
498
+ AND is_exported = 1`, likeParams);
499
+ for (const f of validatorFuncs)
500
+ validatorIds.add(f.id);
501
+ }
502
+ // 1c. Heuristic fallback: name-based matching (only when provenance
503
+ // found nothing AND no user-configured validators exist).
504
+ if (validatorIds.size === 0 && userValidators.length === 0) {
505
+ const heuristicFuncs = indexHandle
506
+ .query(`SELECT id FROM functions
507
+ WHERE (name GLOB 'validate*' OR name GLOB 'assert*')
508
+ AND is_exported = 1`);
509
+ for (const f of heuristicFuncs)
510
+ validatorIds.add(f.id);
511
+ }
512
+ return validatorIds;
513
+ }
514
+ /**
515
+ * Group writers by directory, then flag uncovered writers only in
516
+ * directories where a mode-share of peers reach a validator.
517
+ */
518
+ function flagUncoveredWriters(writers, writerCoverage, opts) {
519
+ const { minCorpus, modeShare, depth } = opts;
520
+ const dirWriters = groupWritersByDirectory(writers, writerCoverage);
521
+ return flagUnvalidatedWriters(dirWriters, minCorpus, modeShare, depth);
522
+ }
523
+ /** Group writers by directory, deduplicating multi-row schema_usage entries. */
524
+ function groupWritersByDirectory(writers, writerCoverage) {
525
+ const dirWriters = new Map();
526
+ const dirSeen = new Set();
527
+ for (const w of writers) {
528
+ const dir = path.dirname(w.file_path);
529
+ const key = `${w.file_path}::${w.function_name}`;
530
+ const dirKey = `${dir}::${key}`;
531
+ if (dirSeen.has(dirKey))
532
+ continue;
533
+ dirSeen.add(dirKey);
534
+ const cov = writerCoverage.get(key);
535
+ if (!cov)
536
+ continue;
537
+ if (!dirWriters.has(dir))
538
+ dirWriters.set(dir, []);
539
+ dirWriters.get(dir).push({
540
+ key,
541
+ covered: cov.covered,
542
+ line: cov.line,
543
+ funcName: cov.funcName,
544
+ filePath: w.file_path,
545
+ });
546
+ }
547
+ return dirWriters;
548
+ }
549
+ /** Flag uncovered writers in validator-dense directories. */
550
+ function flagUnvalidatedWriters(dirWriters, minCorpus, modeShare, depth) {
551
+ const violations = [];
552
+ for (const [dir, dirWriterList] of dirWriters) {
553
+ if (dirWriterList.length < minCorpus)
554
+ continue;
555
+ const coveredCount = dirWriterList.filter((w) => w.covered).length;
556
+ const ratio = coveredCount / dirWriterList.length;
557
+ if (ratio >= modeShare) {
558
+ for (const w of dirWriterList) {
559
+ if (w.covered)
560
+ continue;
476
561
  violations.push({
477
- file: fn.filePath,
478
- line: fn.lineNumber ?? 1,
562
+ file: w.filePath,
563
+ line: w.line,
479
564
  column: 0,
480
565
  severity: 'suggestion',
481
- message: `Exported function '${fn.functionName}' (risk ${fn.riskScore.toFixed(3)}) has no measured test coverage. ` +
482
- `Top imported functions should have test coverage. Import coverage data with 'code-audit coverage --import <path>'.` +
483
- (staleWarning ?? ''),
484
- rule: 'cross-domain/uncovered-risk',
485
- analyzer: this.name,
486
- functionName: fn.functionName,
487
- basis: fn.basis,
488
- sourceFormat,
489
- ...(staleWarning ? { staleImport: true } : {}),
566
+ message: `Function '${w.funcName}' is not validated. ` +
567
+ `${coveredCount}/${dirWriterList.length} peer writers in '${dir}' ` +
568
+ `reach a validator but this function does not (BFS depth ≤ ${depth}). ` +
569
+ `Consider adding input validation.`,
570
+ rule: 'cross-domain/validation-bypass',
571
+ analyzer: ANALYZER_NAME,
572
+ functionName: w.funcName,
490
573
  });
491
574
  }
492
575
  }
493
- else {
494
- // Static-reach fallback: use the call graph to determine which
495
- // exported high-risk functions are reachable from test files
496
- const fpWhere = filePath ? 'AND f.file_path LIKE ?' : '';
497
- const params = [topRiskDecile];
498
- if (filePath)
499
- params.push(filePath.param);
500
- const highRiskFns = indexHandle.query(`WITH ranked AS (
501
- SELECT
502
- f.name,
503
- f.file_path,
504
- f.line_number,
505
- f.id,
506
- COALESCE(hs.score, 0.0) as risk_score,
507
- PERCENT_RANK() OVER (ORDER BY COALESCE(hs.score, 0.0) DESC) as pct
508
- FROM functions f
509
- LEFT JOIN hotspot_scores hs ON hs.target = (f.file_path || ':' || f.name)
510
- AND hs.type = 'function'
511
- WHERE f.is_exported = 1
512
- ${fpWhere}
513
- )
514
- SELECT id, name, file_path, line_number, risk_score
515
- FROM ranked
516
- WHERE pct <= ?
517
- ORDER BY risk_score DESC`, params);
518
- if (highRiskFns.length === 0)
519
- return violations;
520
- // Find test-file functions to use as BFS starting points.
521
- const testGlobs = coverage.testGlobs ?? ['**/*.test.*', '**/*.spec.*', '**/__tests__/**'];
522
- const testGlobPatterns = testGlobs.map((g) => g.replace(/\*\*/g, '%').replace(/\*/g, '%'));
523
- let testFileClause = '';
524
- const testFileParams = [];
525
- if (testGlobPatterns.length > 0) {
526
- testFileClause = testGlobPatterns
527
- .map((_p, i) => `file_path LIKE ?`)
528
- .join(' OR ');
529
- for (const p of testGlobPatterns)
530
- testFileParams.push(p);
531
- }
532
- const testFuncIds = new Set();
533
- if (testFileClause) {
534
- const testFunctions = indexHandle
535
- .query(`SELECT id FROM functions WHERE ${testFileClause}`, testFileParams);
536
- for (const tf of testFunctions)
537
- testFuncIds.add(tf.id);
538
- }
539
- // BFS from each test-file function through call graph (reverse: test
540
- // calls code under test). We traverse the call edges: if testFunc → X,
541
- // then X is reachable.
542
- const reachableIds = new Set();
543
- const maxDepth = coverage.staticReachDepth ?? 2;
544
- if (testFuncIds.size > 0) {
545
- for (const startId of testFuncIds) {
546
- const visited = new Set();
547
- let currentLevel = [startId];
548
- for (let d = 0; d < maxDepth; d++) {
549
- const nextLevel = [];
550
- for (const funcId of currentLevel) {
551
- if (visited.has(funcId))
552
- continue;
553
- visited.add(funcId);
554
- reachableIds.add(funcId);
555
- const callees = indexHandle
556
- .query(`SELECT neighbor_key FROM graph_cache
557
- WHERE graph_type = 'call' AND node_key = ?`, [String(funcId)]);
558
- for (const callee of callees) {
559
- const calleeId = parseInt(callee.neighbor_key, 10);
560
- if (!isNaN(calleeId) && !visited.has(calleeId)) {
561
- nextLevel.push(calleeId);
562
- }
563
- }
564
- }
565
- currentLevel = nextLevel;
566
- }
567
- // Check last level
568
- for (const funcId of currentLevel) {
569
- if (!reachableIds.has(funcId))
570
- reachableIds.add(funcId);
571
- }
572
- }
573
- }
574
- // Flag high-risk functions not in the reachable set
575
- for (const fn of highRiskFns) {
576
- if (!reachableIds.has(fn.id)) {
577
- violations.push({
578
- file: fn.file_path,
579
- line: fn.line_number,
580
- column: 0,
581
- severity: 'suggestion',
582
- message: `Exported function '${fn.name}' (risk ${fn.risk_score.toFixed(3)}) is not reachable from known test files. ` +
583
- `Add test coverage or import measured coverage with 'code-audit coverage --import <path>'.`,
584
- rule: 'cross-domain/uncovered-risk',
585
- analyzer: this.name,
586
- functionName: fn.name,
587
- basis: 'static-reach',
588
- });
589
- }
590
- }
591
- }
592
- return violations;
593
576
  }
594
- /**
595
- * BFS through the call graph (graph_cache) up to maxDepth to check if
596
- * any path from startFuncId reaches a validator function ID.
597
- */
598
- bfsReachesValidator(indexHandle, startFuncId, validatorIds, maxDepth) {
599
- const visited = new Set();
600
- let currentLevel = [startFuncId];
601
- for (let d = 0; d < maxDepth; d++) {
602
- const nextLevel = [];
603
- for (const funcId of currentLevel) {
604
- if (validatorIds.has(funcId))
605
- return true;
606
- if (visited.has(funcId))
607
- continue;
608
- visited.add(funcId);
609
- // Get callees from graph_cache call edges
610
- const callees = indexHandle
611
- .query(`SELECT neighbor_key FROM graph_cache
612
- WHERE graph_type = 'call' AND node_key = ?`, [String(funcId)]);
613
- for (const callee of callees) {
614
- const calleeId = parseInt(callee.neighbor_key, 10);
615
- if (!isNaN(calleeId) && !visited.has(calleeId)) {
616
- nextLevel.push(calleeId);
617
- }
618
- }
619
- }
620
- currentLevel = nextLevel;
621
- }
622
- // Check any remaining nodes at the final level
623
- for (const funcId of currentLevel) {
624
- if (validatorIds.has(funcId))
625
- return true;
577
+ return violations;
578
+ }
579
+ // ── R4: Coverage by Importance ──────────────────────────────────────────
580
+ /**
581
+ * Detect exported, high-risk functions with no test coverage.
582
+ * Uses the getUntestedTopDecile() query that joins functions with
583
+ * hotspot_scores and coverage_data to find untested high-risk functions.
584
+ *
585
+ * Static-reach fallback: when no measured coverage exists, falls back
586
+ * to static reach analysis from test files (BFS from test-file functions
587
+ * into the call graph).
588
+ */
589
+ function detectUncoveredRisk(indexHandle, coverage, filePath) {
590
+ const topRiskDecile = coverage.topRiskDecile ?? 0.1;
591
+ // Check if any measured coverage exists
592
+ const measuredCount = indexHandle
593
+ .query("SELECT COUNT(*) AS cnt FROM coverage_data WHERE basis = 'measured'")[0].cnt;
594
+ if (measuredCount > 0) {
595
+ return detectMeasuredUncovered(indexHandle, topRiskDecile);
596
+ }
597
+ return detectStaticReachUncovered(indexHandle, coverage, filePath);
598
+ }
599
+ /**
600
+ * Flag exported high-risk functions with no measured test coverage, using
601
+ * lcov/istanbul-imported data with stale-import detection.
602
+ */
603
+ function detectMeasuredUncovered(indexHandle, topRiskDecile) {
604
+ const violations = [];
605
+ const untested = indexHandle.getUntestedTopDecile(topRiskDecile);
606
+ // Determine source format from existing coverage entries
607
+ const sourceRow = indexHandle
608
+ .query("SELECT source, imported_at FROM coverage_data WHERE basis = 'measured' LIMIT 1")[0];
609
+ const sourceFormat = sourceRow?.source ?? 'unknown';
610
+ const importedAt = sourceRow?.imported_at ?? null;
611
+ // Stale-import detection: measured coverage predates last full index sync
612
+ let staleWarning = null;
613
+ if (importedAt) {
614
+ const lastSync = indexHandle.getMeta?.('last_full_sync_timestamp') ?? null;
615
+ if (lastSync && importedAt < lastSync) {
616
+ staleWarning =
617
+ ` — WARNING: this coverage data may be stale (imported ${importedAt}, ` +
618
+ `last index sync was ${lastSync}). ` +
619
+ `Re-import with 'code-audit coverage --import <path>' for accurate results.`;
626
620
  }
627
- return false;
628
621
  }
622
+ for (const fn of untested) {
623
+ violations.push({
624
+ file: fn.filePath,
625
+ line: fn.lineNumber ?? 1,
626
+ column: 0,
627
+ severity: 'suggestion',
628
+ message: `Exported function '${fn.functionName}' (risk ${fn.riskScore.toFixed(3)}) has no measured test coverage. ` +
629
+ `Top imported functions should have test coverage. Import coverage data with 'code-audit coverage --import <path>'.` +
630
+ (staleWarning ?? ''),
631
+ rule: 'cross-domain/uncovered-risk',
632
+ analyzer: ANALYZER_NAME,
633
+ functionName: fn.functionName,
634
+ basis: fn.basis,
635
+ sourceFormat,
636
+ ...(staleWarning ? { staleImport: true } : {}),
637
+ });
638
+ }
639
+ return violations;
640
+ }
641
+ /**
642
+ * Static-reach fallback: flag exported high-risk functions that are not
643
+ * reachable from known test files via BFS through the call graph.
644
+ */
645
+ function detectStaticReachUncovered(indexHandle, coverage, filePath) {
646
+ const topRiskDecile = coverage.topRiskDecile ?? 0.1;
647
+ const highRiskFns = queryHighRiskFunctions(indexHandle, topRiskDecile, filePath);
648
+ if (highRiskFns.length === 0)
649
+ return [];
650
+ const reachableIds = computeTestReachableIds(indexHandle, coverage);
651
+ return flagUnreachedHighRisk(highRiskFns, reachableIds);
652
+ }
653
+ /** Rank exported functions by hotspot score and take the top decile. */
654
+ function queryHighRiskFunctions(indexHandle, topRiskDecile, filePath) {
655
+ const fpWhere = filePath ? 'AND f.file_path LIKE ?' : '';
656
+ const params = [topRiskDecile];
657
+ if (filePath)
658
+ params.push(filePath.param);
659
+ return indexHandle.query(`WITH ranked AS (
660
+ SELECT
661
+ f.name,
662
+ f.file_path,
663
+ f.line_number,
664
+ f.id,
665
+ COALESCE(hs.score, 0.0) as risk_score,
666
+ PERCENT_RANK() OVER (ORDER BY COALESCE(hs.score, 0.0) DESC) as pct
667
+ FROM functions f
668
+ LEFT JOIN hotspot_scores hs ON hs.target = (f.file_path || ':' || f.name)
669
+ AND hs.type = 'function'
670
+ WHERE f.is_exported = 1
671
+ ${fpWhere}
672
+ )
673
+ SELECT id, name, file_path, line_number, risk_score
674
+ FROM ranked
675
+ WHERE pct <= ?
676
+ ORDER BY risk_score DESC`, params);
677
+ }
678
+ /** Flag high-risk functions not in the reachable set. */
679
+ function flagUnreachedHighRisk(highRiskFns, reachableIds) {
680
+ const violations = [];
681
+ for (const fn of highRiskFns) {
682
+ if (reachableIds.has(fn.id))
683
+ continue;
684
+ violations.push({
685
+ file: fn.file_path,
686
+ line: fn.line_number,
687
+ column: 0,
688
+ severity: 'suggestion',
689
+ message: `Exported function '${fn.name}' (risk ${fn.risk_score.toFixed(3)}) is not reachable from known test files. ` +
690
+ `Add test coverage or import measured coverage with 'code-audit coverage --import <path>'.`,
691
+ rule: 'cross-domain/uncovered-risk',
692
+ analyzer: ANALYZER_NAME,
693
+ functionName: fn.name,
694
+ basis: 'static-reach',
695
+ });
696
+ }
697
+ return violations;
698
+ }
699
+ /**
700
+ * Compute the set of function IDs reachable from test-file functions via
701
+ * BFS through the call graph (reverse direction: test → code under test).
702
+ */
703
+ function computeTestReachableIds(indexHandle, coverage) {
704
+ const testFuncIds = collectTestFuncIds(indexHandle, coverage);
705
+ if (testFuncIds.size === 0)
706
+ return new Set();
707
+ const maxDepth = coverage.staticReachDepth ?? 2;
708
+ return collectReachableIds(indexHandle, testFuncIds, maxDepth);
709
+ }
710
+ /** Find test-file function IDs to use as BFS starting points. */
711
+ function collectTestFuncIds(indexHandle, coverage) {
712
+ const testGlobs = coverage.testGlobs ?? ['**/*.test.*', '**/*.spec.*', '**/__tests__/**'];
713
+ const testGlobPatterns = testGlobs.map((g) => g.replace(/\*\*/g, '%').replace(/\*/g, '%'));
714
+ if (testGlobPatterns.length === 0)
715
+ return new Set();
716
+ const testFileClause = testGlobPatterns
717
+ .map(() => `file_path LIKE ?`)
718
+ .join(' OR ');
719
+ const testFileParams = [...testGlobPatterns];
720
+ const testFuncIds = new Set();
721
+ const testFunctions = indexHandle
722
+ .query(`SELECT id FROM functions WHERE ${testFileClause}`, testFileParams);
723
+ for (const tf of testFunctions)
724
+ testFuncIds.add(tf.id);
725
+ return testFuncIds;
629
726
  }
630
727
  //# sourceMappingURL=CrossDomainAnalyzer.js.map