code-auditor-mcp 3.4.9 → 3.4.11

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 (130) hide show
  1. package/.claude-plugin/marketplace.json +10 -3
  2. package/CHANGELOG.md +81 -0
  3. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  4. package/dist/analyzers/analyzerUtils.js +2 -1
  5. package/dist/analyzers/analyzerUtils.js.map +1 -1
  6. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +0 -2
  7. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  8. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +74 -99
  9. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  10. package/dist/analyzers/documentationAnalyzer.d.ts +1 -5
  11. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  12. package/dist/analyzers/documentationAnalyzer.js +13 -69
  13. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  14. package/dist/analyzers/invariantsAnalyzer.d.ts +1 -6
  15. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  16. package/dist/analyzers/invariantsAnalyzer.js +16 -41
  17. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  18. package/dist/analyzers/reactAnalyzer.d.ts +20 -3
  19. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  20. package/dist/analyzers/reactAnalyzer.js +4 -165
  21. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  22. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  23. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +43 -69
  24. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  25. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +62 -0
  26. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  27. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +357 -3
  28. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  29. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +81 -69
  30. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  31. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +319 -325
  32. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  33. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  34. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +32 -37
  35. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  36. package/dist/auditRunner.d.ts +2 -7
  37. package/dist/auditRunner.d.ts.map +1 -1
  38. package/dist/auditRunner.js +306 -434
  39. package/dist/auditRunner.js.map +1 -1
  40. package/dist/cli.js +27 -5
  41. package/dist/cli.js.map +1 -1
  42. package/dist/codeIndexDB.d.ts +22 -0
  43. package/dist/codeIndexDB.d.ts.map +1 -1
  44. package/dist/codeIndexDB.js +92 -44
  45. package/dist/codeIndexDB.js.map +1 -1
  46. package/dist/config/defaults.js +3 -3
  47. package/dist/config/defaults.js.map +1 -1
  48. package/dist/conventions/conventionMiner.d.ts +5 -4
  49. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  50. package/dist/conventions/conventionMiner.js +65 -25
  51. package/dist/conventions/conventionMiner.js.map +1 -1
  52. package/dist/dataPaths.js +1 -1
  53. package/dist/dataPaths.js.map +1 -1
  54. package/dist/invariants/index.d.ts +1 -1
  55. package/dist/invariants/index.d.ts.map +1 -1
  56. package/dist/invariants/index.js +1 -1
  57. package/dist/invariants/index.js.map +1 -1
  58. package/dist/invariants/ruleEngine.d.ts +38 -3
  59. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  60. package/dist/invariants/ruleEngine.js +42 -174
  61. package/dist/invariants/ruleEngine.js.map +1 -1
  62. package/dist/languages/UniversalAnalyzer.d.ts +2 -2
  63. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  64. package/dist/languages/UniversalAnalyzer.js +3 -1
  65. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  66. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
  67. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  68. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +60 -16
  69. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  70. package/dist/mcp-tools-shared.d.ts.map +1 -1
  71. package/dist/mcp-tools-shared.js +10 -6
  72. package/dist/mcp-tools-shared.js.map +1 -1
  73. package/dist/mcp.js +0 -0
  74. package/dist/mcpAuditJobs.d.ts.map +1 -1
  75. package/dist/mcpAuditJobs.js +4 -1
  76. package/dist/mcpAuditJobs.js.map +1 -1
  77. package/dist/pipeline.d.ts +74 -0
  78. package/dist/pipeline.d.ts.map +1 -0
  79. package/dist/pipeline.js +620 -0
  80. package/dist/pipeline.js.bak +465 -0
  81. package/dist/pipeline.js.bak2 +465 -0
  82. package/dist/pipeline.js.map +1 -0
  83. package/dist/pipelineAdapters.d.ts +116 -0
  84. package/dist/pipelineAdapters.d.ts.map +1 -0
  85. package/dist/pipelineAdapters.js +1146 -0
  86. package/dist/pipelineAdapters.js.map +1 -0
  87. package/dist/pipelineTypes.d.ts +44 -0
  88. package/dist/pipelineTypes.d.ts.map +1 -0
  89. package/dist/pipelineTypes.js +118 -0
  90. package/dist/pipelineTypes.js.map +1 -0
  91. package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
  92. package/dist/reporting/csvReportGenerator.js +2 -1
  93. package/dist/reporting/csvReportGenerator.js.map +1 -1
  94. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  95. package/dist/reporting/jsonReportGenerator.js +4 -2
  96. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  97. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  98. package/dist/reporting/sarifReportGenerator.js +2 -1
  99. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  100. package/dist/styles/cssAstExtractor.d.ts +46 -0
  101. package/dist/styles/cssAstExtractor.d.ts.map +1 -0
  102. package/dist/styles/cssAstExtractor.js +439 -0
  103. package/dist/styles/cssAstExtractor.js.map +1 -0
  104. package/dist/styles/styleExtractor.d.ts +2 -2
  105. package/dist/styles/styleExtractor.d.ts.map +1 -1
  106. package/dist/styles/styleExtractor.js +69 -16
  107. package/dist/styles/styleExtractor.js.map +1 -1
  108. package/dist/styles/styleIndexer.d.ts.map +1 -1
  109. package/dist/styles/styleIndexer.js +4 -0
  110. package/dist/styles/styleIndexer.js.map +1 -1
  111. package/dist/styles/tailwindProbe.d.ts +2 -0
  112. package/dist/styles/tailwindProbe.d.ts.map +1 -1
  113. package/dist/styles/tailwindProbe.js +5 -5
  114. package/dist/styles/tailwindProbe.js.map +1 -1
  115. package/dist/styles/tailwindUtilityExpander.d.ts +3 -0
  116. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  117. package/dist/styles/tailwindUtilityExpander.js +7 -0
  118. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  119. package/dist/types.d.ts +216 -11
  120. package/dist/types.d.ts.map +1 -1
  121. package/dist/types.js.map +1 -1
  122. package/dist/utils/fileDiscovery.d.ts +4 -0
  123. package/dist/utils/fileDiscovery.d.ts.map +1 -1
  124. package/dist/utils/fileDiscovery.js +7 -1
  125. package/dist/utils/fileDiscovery.js.map +1 -1
  126. package/dist/utils/reactDetection.d.ts.map +1 -1
  127. package/dist/utils/reactDetection.js +32 -0
  128. package/dist/utils/reactDetection.js.map +1 -1
  129. package/package.json +2 -4
  130. package/plugin/.claude-plugin/plugin.json +1 -1
@@ -0,0 +1,1146 @@
1
+ /**
2
+ * Pipeline adapter factories (Spec 24).
3
+ *
4
+ * Each factory converts an existing analyzer into a Stage2Visitor,
5
+ * Stage3Reducer, or Stage4Reducer for the pipeline. Lazy-loads analyzer
6
+ * classes via dynamic import() to avoid circular dependency issues.
7
+ *
8
+ * Key patterns:
9
+ * - Per-file (solid, data-access, doc, schema): wrap analyzeAST()
10
+ * - DRY: wraps analyzeAST(), accumulates dryPairs internally →
11
+ * factory returns { visitor, getDryPairs } for post-pipeline extraction
12
+ * - React: wraps scanFile() per file, accumulates scan results →
13
+ * factory returns { visitor, finalizeCrossComponent }
14
+ * - DB-based (styles, conventions, cross-domain): reducers call
15
+ * analyzer.analyze([]) with rawDb injected through config
16
+ * - Invariants: Stage 3 reducer — receives full file list, runs rule engine
17
+ */
18
+ import { createHash } from 'crypto';
19
+ import path from 'path';
20
+ import { RULE_REGISTRY } from './analyzers/ruleRegistry.js';
21
+ import { walkAST, isExported, getLineAndColumn, calculateComplexity, getFunctionBody, } from './languages/adapterBridge.js';
22
+ import { buildImportMap, extractFunctionCalls, } from './utils/dependencyExtractor.js';
23
+ import { isReactComponent, detectComponentType, getComponentName, } from './utils/reactDetection.js';
24
+ import { ALL_EXTENSIONS } from './utils/fileDiscovery.js';
25
+ // ── Rule ID helpers ──────────────────────────────────────────────────────────
26
+ function getRuleIdsFor(name) {
27
+ return Object.entries(RULE_REGISTRY)
28
+ .filter(([, entry]) => entry.analyzer === name)
29
+ .map(([id]) => id);
30
+ }
31
+ // ── Lazy import singleton ────────────────────────────────────────────────────
32
+ function lazySingleton(loader) {
33
+ let promise = null;
34
+ return () => {
35
+ if (!promise)
36
+ promise = loader();
37
+ return promise;
38
+ };
39
+ }
40
+ // ── File-sources infrastructure visitor ──────────────────────────────────────
41
+ /**
42
+ * Infrastructure visitor that records every file's source code so Stage 3
43
+ * reducers can access it without calling readFileSync(). Always registered;
44
+ * declares all known extensions so it covers both parsed and raw tuples.
45
+ */
46
+ export function createFileSourcesVisitor() {
47
+ return {
48
+ name: 'file-sources',
49
+ stage: 'visitor',
50
+ extensions: ALL_EXTENSIONS,
51
+ getRuleIds: () => [],
52
+ async visit(_ast, _adapter, context, sourceCode) {
53
+ return {
54
+ violations: [],
55
+ facts: { [context.filePath]: sourceCode },
56
+ };
57
+ },
58
+ defaultConfig: {},
59
+ description: 'Records every file source for Stage 3 reducers (infrastructure)',
60
+ category: 'infrastructure',
61
+ };
62
+ }
63
+ // ── SOLID visitor ────────────────────────────────────────────────────────────
64
+ export function createSolidVisitor() {
65
+ const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalSOLIDAnalyzer.js').then((m) => new m.UniversalSOLIDAnalyzer()));
66
+ return {
67
+ name: 'solid',
68
+ stage: 'visitor',
69
+ getRuleIds: () => getRuleIdsFor('solid'),
70
+ async visit(ast, adapter, context, sourceCode) {
71
+ const a = await getAnalyzer();
72
+ const violations = await a.analyzeAST(ast, adapter, context.config, sourceCode);
73
+ return {
74
+ violations,
75
+ facts: {},
76
+ };
77
+ },
78
+ defaultConfig: {},
79
+ description: 'Detects violations of SOLID principles',
80
+ category: 'architecture',
81
+ };
82
+ }
83
+ export function createDryVisitor(fullFunctionIndex) {
84
+ const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalDRYAnalyzer.js').then((m) => new m.UniversalDRYAnalyzer()));
85
+ const visitor = {
86
+ name: 'dry',
87
+ stage: 'visitor',
88
+ getRuleIds: () => getRuleIdsFor('dry'),
89
+ async visit(ast, adapter, context, sourceCode) {
90
+ const a = await getAnalyzer();
91
+ const config = fullFunctionIndex
92
+ ? { ...context.config, fullFunctionIndex }
93
+ : context.config;
94
+ const violations = await a.analyzeAST(ast, adapter, config, sourceCode);
95
+ return { violations, facts: {} };
96
+ },
97
+ defaultConfig: {},
98
+ description: 'Detects code duplication across the codebase',
99
+ category: 'maintainability',
100
+ };
101
+ return {
102
+ visitor,
103
+ getDryPairs: async () => {
104
+ try {
105
+ const a = await getAnalyzer();
106
+ return a.dryPairs ?? [];
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ },
112
+ };
113
+ }
114
+ // ── Data-Access visitor ──────────────────────────────────────────────────────
115
+ export function createDataAccessVisitor() {
116
+ const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalDataAccessAnalyzer.js').then((m) => new m.UniversalDataAccessAnalyzer()));
117
+ return {
118
+ name: 'data-access',
119
+ stage: 'visitor',
120
+ getRuleIds: () => getRuleIdsFor('data-access'),
121
+ async visit(ast, adapter, context, sourceCode) {
122
+ const a = await getAnalyzer();
123
+ const violations = await a.analyzeAST(ast, adapter, context.config, sourceCode);
124
+ return { violations, facts: {} };
125
+ },
126
+ defaultConfig: {},
127
+ description: 'Analyzes database access patterns and data layer interactions',
128
+ category: 'security',
129
+ };
130
+ }
131
+ // ── Documentation visitor ────────────────────────────────────────────────────
132
+ export function createDocumentationVisitor() {
133
+ const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalDocumentationAnalyzer.js').then((m) => new m.UniversalDocumentationAnalyzer()));
134
+ return {
135
+ name: 'documentation',
136
+ stage: 'visitor',
137
+ getRuleIds: () => getRuleIdsFor('documentation'),
138
+ async visit(ast, adapter, context, sourceCode) {
139
+ const a = await getAnalyzer();
140
+ const violations = await a.analyzeAST(ast, adapter, context.config, sourceCode);
141
+ return { violations, facts: {} };
142
+ },
143
+ defaultConfig: {},
144
+ description: 'Checks documentation completeness',
145
+ category: 'style',
146
+ };
147
+ }
148
+ // ── Function-Index visitor (infrastructure) ──────────────────────────────────
149
+ // Always-on Stage 2 visitor that populates the `functions` table with
150
+ // per-file definitions so the conventions and cross-domain reducers have data
151
+ // to mine — even on a cold run with no prior `index sync`.
152
+ // The function_calls table is rebuilt post-pipeline after functions rows have
153
+ // their auto-increment IDs assigned.
154
+ function getLanguageFromPath(filePath) {
155
+ const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
156
+ switch (ext) {
157
+ case '.ts':
158
+ case '.tsx':
159
+ return 'typescript';
160
+ case '.js':
161
+ case '.jsx':
162
+ return 'javascript';
163
+ default:
164
+ return 'unknown';
165
+ }
166
+ }
167
+ function computeContentHash(body, signature) {
168
+ const normalized = (body ?? '').replace(/\s+/g, ' ').trim() + '|' + (signature ?? '').trim();
169
+ return createHash('sha256').update(normalized).digest('hex');
170
+ }
171
+ /** Raw text from an ASTNode (tree-sitter node stored on `.raw`). */
172
+ function rawText(node) {
173
+ return node.raw?.text ?? '';
174
+ }
175
+ export function createFunctionIndexVisitor() {
176
+ return {
177
+ name: 'function-index',
178
+ stage: 'visitor',
179
+ getRuleIds: () => [],
180
+ async visit(ast, _adapter, context, sourceCode) {
181
+ const root = ast.root;
182
+ const filePath = context.filePath;
183
+ if (!filePath)
184
+ return { violations: [], facts: {} };
185
+ const indexFacts = [];
186
+ // Only index TS/JS files
187
+ const lang = getLanguageFromPath(filePath);
188
+ if (lang === 'unknown')
189
+ return { violations: [], facts: {} };
190
+ // Clear existing entries for this file
191
+ indexFacts.push({
192
+ table: 'functions',
193
+ data: { _action: 'clear-by-file', file_path: filePath },
194
+ });
195
+ // Build import map once per file for resolving call targets
196
+ const importMap = buildImportMap(root);
197
+ // Collect function-like nodes in two passes:
198
+ // 1) function_declaration + method_definition nodes
199
+ // 2) arrow functions assigned to variables (variable_declarator children)
200
+ const fnEntries = [];
201
+ // Pass 1 — named function declarations and methods
202
+ walkAST(root, (node) => {
203
+ if (node.type === 'function_declaration') {
204
+ const nameNode = node.children?.find((c) => c.type === 'identifier');
205
+ if (!nameNode)
206
+ return;
207
+ const name = rawText(nameNode);
208
+ if (!name)
209
+ return;
210
+ const { line } = getLineAndColumn(node);
211
+ const raw = node.raw;
212
+ const endLine = line + (raw?.endPosition?.row ?? raw?.startPosition?.row ?? 0) - (raw?.startPosition?.row ?? line) + 1;
213
+ const body = getFunctionBody(node, sourceCode);
214
+ const calls = extractFunctionCalls(node, sourceCode, importMap);
215
+ const callNames = [...new Set(calls.map((c) => c.callee))];
216
+ fnEntries.push({
217
+ name,
218
+ line,
219
+ endLine,
220
+ entityType: 'function',
221
+ componentType: null,
222
+ isExported: isExported(node),
223
+ complexity: calculateComplexity(node),
224
+ body,
225
+ functionCalls: callNames,
226
+ });
227
+ }
228
+ // Class methods
229
+ if (node.type === 'method_definition') {
230
+ const nameNode = node.children?.find((c) => c.type === 'identifier');
231
+ if (!nameNode)
232
+ return;
233
+ const methodName = rawText(nameNode);
234
+ if (!methodName)
235
+ return;
236
+ // Walk up to find class name
237
+ let parent = node.parent;
238
+ let className = 'AnonymousClass';
239
+ while (parent) {
240
+ if (parent.type === 'class_declaration') {
241
+ const cn = parent.children?.find((c) => c.type === 'identifier');
242
+ if (cn)
243
+ className = rawText(cn);
244
+ break;
245
+ }
246
+ parent = parent.parent;
247
+ }
248
+ const { line } = getLineAndColumn(node);
249
+ const raw = node.raw;
250
+ const endLine = line + (raw?.endPosition?.row ?? raw?.startPosition?.row ?? 0) - (raw?.startPosition?.row ?? line) + 1;
251
+ const body = getFunctionBody(node, sourceCode);
252
+ const calls = extractFunctionCalls(node, sourceCode, importMap);
253
+ const callNames = [...new Set(calls.map((c) => c.callee))];
254
+ fnEntries.push({
255
+ name: `${className}.${methodName}`,
256
+ line,
257
+ endLine,
258
+ entityType: 'method',
259
+ componentType: null,
260
+ isExported: isExported(node),
261
+ complexity: calculateComplexity(node),
262
+ body,
263
+ functionCalls: callNames,
264
+ });
265
+ }
266
+ });
267
+ // Pass 2 — arrow functions assigned to variables
268
+ walkAST(root, (node) => {
269
+ if (node.type !== 'variable_declarator')
270
+ return;
271
+ const nameNode = node.children?.find((c) => c.type === 'identifier');
272
+ const arrowFunc = node.children?.find((c) => c.type === 'arrow_function');
273
+ if (!nameNode || !arrowFunc)
274
+ return;
275
+ const name = rawText(nameNode);
276
+ if (!name)
277
+ return;
278
+ // Don't duplicate if already covered as function_declaration
279
+ // (shouldn't happen — function_declaration is a different node type)
280
+ const { line } = getLineAndColumn(arrowFunc);
281
+ const raw = arrowFunc.raw;
282
+ const endLine = line + (raw?.endPosition?.row ?? raw?.startPosition?.row ?? 0) - (raw?.startPosition?.row ?? line) + 1;
283
+ const body = getFunctionBody(arrowFunc, sourceCode);
284
+ const calls = extractFunctionCalls(arrowFunc, sourceCode, importMap);
285
+ const callNames = [...new Set(calls.map((c) => c.callee))];
286
+ fnEntries.push({
287
+ name,
288
+ line,
289
+ endLine,
290
+ entityType: 'function',
291
+ componentType: null,
292
+ isExported: isExported(node),
293
+ complexity: calculateComplexity(arrowFunc),
294
+ body,
295
+ functionCalls: callNames,
296
+ });
297
+ });
298
+ // Pass 3 — React component detection
299
+ // Detect JSX-returning functions to set entity_type='component' and
300
+ // component_type, matching what functionScanner's deepSync produces.
301
+ // The convention miner's classifyExportKind() uses these columns to
302
+ // partition naming conventions.
303
+ const hasReactImport = [...importMap.values()].some((v) => v.modulePath === 'react');
304
+ if (filePath.endsWith('.tsx') ||
305
+ filePath.endsWith('.jsx') ||
306
+ (filePath.endsWith('.js') && hasReactImport)) {
307
+ walkAST(root, (node) => {
308
+ if (!isReactComponent(node))
309
+ return;
310
+ const ct = detectComponentType(node);
311
+ if (!ct)
312
+ return;
313
+ const cName = getComponentName(node);
314
+ if (!cName || cName === 'AnonymousComponent')
315
+ return;
316
+ const existing = fnEntries.find((f) => f.name === cName);
317
+ if (existing) {
318
+ // Upgrade existing function_declaration or arrow-function entry
319
+ existing.entityType = 'component';
320
+ existing.componentType = ct;
321
+ }
322
+ else {
323
+ // New entry — class component, function_expression, or memo/forwardRef
324
+ // wrapper not already captured by passes 1 or 2.
325
+ const { line } = getLineAndColumn(node);
326
+ const raw = node.raw;
327
+ const endLine = line +
328
+ (raw?.endPosition?.row ?? raw?.startPosition?.row ?? 0) -
329
+ (raw?.startPosition?.row ?? line) +
330
+ 1;
331
+ const body = getFunctionBody(node, sourceCode);
332
+ fnEntries.push({
333
+ name: cName,
334
+ line,
335
+ endLine,
336
+ entityType: 'component',
337
+ componentType: ct,
338
+ isExported: isExported(node),
339
+ complexity: calculateComplexity(node),
340
+ body,
341
+ functionCalls: [],
342
+ });
343
+ }
344
+ });
345
+ }
346
+ // Build IndexFactsEntry for each function
347
+ for (const fn of fnEntries) {
348
+ const metadata = {
349
+ entityType: fn.entityType,
350
+ isExported: fn.isExported,
351
+ complexity: fn.complexity,
352
+ functionCalls: fn.functionCalls,
353
+ body: fn.body,
354
+ };
355
+ const contentHash = computeContentHash(fn.body, '');
356
+ const now = new Date().toISOString();
357
+ indexFacts.push({
358
+ table: 'functions',
359
+ data: {
360
+ name: fn.name,
361
+ file_path: filePath,
362
+ line_number: fn.line,
363
+ start_line: fn.line,
364
+ end_line: fn.endLine,
365
+ language: lang,
366
+ entity_type: fn.entityType,
367
+ component_type: fn.componentType,
368
+ signature: '',
369
+ return_type: null,
370
+ complexity: fn.complexity,
371
+ is_exported: fn.isExported ? 1 : 0,
372
+ has_jsdoc: 0,
373
+ jsdoc_description: '',
374
+ parameters: null,
375
+ body: fn.body ?? null,
376
+ content_hash: contentHash,
377
+ last_modified: now,
378
+ metadata_json: JSON.stringify(metadata),
379
+ },
380
+ conflictKey: 'name, file_path, line_number',
381
+ });
382
+ }
383
+ // ── Extract imports and exports from AST for downstream consumers ──
384
+ // B1: Replaces regex-based extractImports()/extractExportedSymbols() in
385
+ // ruleEngine.ts. Also consumed by conventions (B2: detectExportForm).
386
+ const langAdapter = _adapter;
387
+ const langAst = ast;
388
+ // Static imports via the adapter's canonical extractImports()
389
+ const staticImportInfos = langAdapter.extractImports(langAst);
390
+ const staticImports = staticImportInfos.map((imp) => ({
391
+ moduleSpecifier: imp.source,
392
+ isStatic: true,
393
+ isDynamic: false,
394
+ isRequire: false,
395
+ line: imp.location.start.line,
396
+ }));
397
+ // Dynamic import() and require() — first attempt with pure NodePattern
398
+ // Plan note: import keyword is an anonymous tree-sitter node, so
399
+ // hasChild cannot see it. We use `custom` with raw node access.
400
+ const dynamicCallNodes = langAdapter.findNodes(langAst, {
401
+ type: 'call_expression',
402
+ custom: (node) => {
403
+ const raw = node.raw;
404
+ const fn = raw?.firstChild;
405
+ return (fn?.type === 'import') ||
406
+ (fn?.type === 'identifier' && fn.text === 'require');
407
+ },
408
+ });
409
+ const dynamicImports = [];
410
+ for (const node of dynamicCallNodes) {
411
+ const raw = node.raw;
412
+ const fn = raw?.firstChild;
413
+ const isImport = fn?.type === 'import';
414
+ const isRequire = !isImport && (fn?.type === 'identifier' && fn.text === 'require');
415
+ // Walk the raw tree to find the string argument
416
+ const argsNode = raw?.children?.find((c) => c.type === 'arguments');
417
+ const stringNode = argsNode?.children?.find((c) => c.type === 'string');
418
+ if (stringNode) {
419
+ const text = stringNode.text;
420
+ if (text.length >= 2) {
421
+ dynamicImports.push({
422
+ moduleSpecifier: text.slice(1, -1), // strip quotes
423
+ isStatic: false,
424
+ isDynamic: isImport,
425
+ isRequire,
426
+ line: node.location.start.line,
427
+ });
428
+ }
429
+ }
430
+ }
431
+ // Exports via the adapter's canonical extractExports()
432
+ // Returns ExportInfo[] with isDefault — used by both invariants and conventions (B2)
433
+ const exportInfos = langAdapter.extractExports(langAst);
434
+ return {
435
+ violations: [],
436
+ facts: {
437
+ [filePath]: {
438
+ imports: [...staticImports, ...dynamicImports],
439
+ exports: exportInfos,
440
+ },
441
+ },
442
+ indexFacts,
443
+ };
444
+ },
445
+ defaultConfig: {},
446
+ description: 'Indexes function definitions and extracts imports/exports for downstream analyzers',
447
+ category: 'infrastructure',
448
+ };
449
+ }
450
+ export function createReactVisitor() {
451
+ const scanResults = [];
452
+ const visitor = {
453
+ name: 'react',
454
+ stage: 'visitor',
455
+ getRuleIds: () => getRuleIdsFor('react'),
456
+ async visit(_ast, _adapter, context, _sourceCode) {
457
+ const filePath = context.filePath;
458
+ if (!filePath)
459
+ return { violations: [], facts: {} };
460
+ try {
461
+ const { scanFile } = await import('./componentScanner.js');
462
+ const { analyzeComponent: analyzeComp, DEFAULT_REACT_CONFIG } = await import('./analyzers/reactAnalyzer.js');
463
+ // Merge with defaults so partial configs get all required fields
464
+ const cfg = { ...DEFAULT_REACT_CONFIG, ...context.config };
465
+ const sr = await scanFile(filePath, {
466
+ includeTests: false,
467
+ includeStories: false,
468
+ extractProps: true,
469
+ extractHooks: cfg.checkHooksRules,
470
+ extractImports: true,
471
+ detectComplexity: true,
472
+ });
473
+ scanResults.push(sr);
474
+ if (sr.parseErrors && sr.parseErrors.length > 0) {
475
+ return { violations: [], facts: {} };
476
+ }
477
+ // Per-component checks — same logic as reactAnalyzer.analyze()
478
+ const violations = [];
479
+ for (const comp of sr.components ?? []) {
480
+ violations.push(...analyzeComp(comp, cfg, sr));
481
+ }
482
+ return { violations, facts: {} };
483
+ }
484
+ catch {
485
+ return { violations: [], facts: {} };
486
+ }
487
+ },
488
+ defaultConfig: {},
489
+ description: 'Analyzes React components for best practices and performance',
490
+ category: 'frontend',
491
+ };
492
+ return {
493
+ visitor,
494
+ finalizeCrossComponent: async (config) => {
495
+ if (scanResults.length === 0)
496
+ return [];
497
+ const violations = [];
498
+ try {
499
+ const { checkCircularDependencies, checkErrorBoundaryUsage, checkRawElements, } = await import('./analyzers/reactAnalyzer.js');
500
+ // Build component tree and check for circular dependencies
501
+ const { buildComponentTree } = await import('./componentScanner.js');
502
+ const tree = buildComponentTree(scanResults);
503
+ violations.push(...checkCircularDependencies(tree));
504
+ // Check for missing error boundaries
505
+ if (config.requireErrorBoundaries !== false) {
506
+ violations.push(...checkErrorBoundaryUsage(scanResults));
507
+ }
508
+ // Check for raw element usage (Spec 10 R4)
509
+ if (config.rawElementCheck !== false) {
510
+ violations.push(...checkRawElements(scanResults, config));
511
+ }
512
+ }
513
+ catch {
514
+ // Cross-component checks are advisory
515
+ }
516
+ return violations;
517
+ },
518
+ };
519
+ }
520
+ // ── Styles reducer (stage 3) ─────────────────────────────────────────────────
521
+ // ── Styles CSS visitor (Spec 26 Phase 2) ───────────────────────────────────────
522
+ /**
523
+ * Styles CSS visitor — extracts declarations, tokens, and class usage from
524
+ * tree-sitter-css parsed ASTs. Replaces the regex-based CSS extraction in
525
+ * styleExtractor.ts for .css files only.
526
+ *
527
+ * tree-sitter-css is NOT an SCSS grammar — .scss files stay on the regex path.
528
+ */
529
+ export function createStylesCssVisitor() {
530
+ return {
531
+ name: 'styles-css',
532
+ stage: 'visitor',
533
+ extensions: ['.css'],
534
+ getRuleIds: () => [],
535
+ async visit(ast, adapter, context, sourceCode) {
536
+ // Lazy-load to avoid circular dependency issues at module load time
537
+ const { extractDeclarationsFromCSSAst, extractTokensFromCSSAst, extractClassUsageFromCSSAst } = await import('./styles/cssAstExtractor.js');
538
+ const cssAst = ast;
539
+ const cssAdapter = adapter;
540
+ const filePath = context.filePath;
541
+ return {
542
+ violations: [],
543
+ facts: {
544
+ [filePath]: {
545
+ declarations: extractDeclarationsFromCSSAst(cssAst, cssAdapter, filePath, sourceCode),
546
+ tokens: extractTokensFromCSSAst(cssAst, cssAdapter, filePath),
547
+ classUsage: extractClassUsageFromCSSAst(cssAst, cssAdapter, filePath),
548
+ },
549
+ },
550
+ };
551
+ },
552
+ defaultConfig: {},
553
+ description: 'Extracts CSS declarations from tree-sitter-css ASTs (.css only)',
554
+ category: 'style',
555
+ };
556
+ }
557
+ /**
558
+ * Styles reducer — runs all style detectors on existing style_* tables.
559
+ * Uses the analyzer.analyze([]) pattern: passes an empty file list so the
560
+ * analyzer skips file iteration and only queries the DB.
561
+ *
562
+ * Spec 26 Phase 2: CSS facts from the styles-css visitor are inserted into
563
+ * the DB before the analyzer runs. For .css files, the AST extraction replaces
564
+ * the regex-based styleIndexer path (which now skips .css files).
565
+ */
566
+ export function createStylesReducer() {
567
+ return {
568
+ name: 'styles',
569
+ stage: 'reducer',
570
+ consumes: [],
571
+ getRuleIds: () => getRuleIdsFor('styles'),
572
+ async reduce(_allFacts, context) {
573
+ if (!context.indexHandle)
574
+ return { violations: [], facts: {} };
575
+ try {
576
+ // ── Insert CSS visitor facts into style_* tables ──────────────────────
577
+ const cssFacts = _allFacts['styles-css'];
578
+ if (cssFacts) {
579
+ const run = context.indexHandle.run.bind(context.indexHandle);
580
+ for (const [filePath, facts] of Object.entries(cssFacts)) {
581
+ // Delete old entries for this file (replaces the styleIndexer path)
582
+ run('DELETE FROM style_declarations WHERE file_path = ?', [filePath]);
583
+ run('DELETE FROM style_tokens WHERE file_path = ?', [filePath]);
584
+ run('DELETE FROM style_class_usage WHERE file_path = ?', [filePath]);
585
+ // Compute content hash for the file
586
+ const contentStr = JSON.stringify({ declarations: facts.declarations.length, tokens: facts.tokens.length, classUsage: facts.classUsage.length });
587
+ const contentHash = createHash('sha256').update(contentStr).digest('hex').slice(0, 16);
588
+ // Insert declarations
589
+ for (const decl of facts.declarations) {
590
+ run('INSERT INTO style_declarations (property, raw_value, normalized_value, mechanism, file_path, line, context, variant_context, token_ref, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [decl.property, decl.rawValue, JSON.stringify(decl.normalizedValue), decl.mechanism, decl.filePath, decl.line, decl.context, decl.variantContext, decl.tokenRef, contentHash]);
591
+ }
592
+ // Insert tokens
593
+ for (const tok of facts.tokens) {
594
+ run('INSERT INTO style_tokens (name, value, file_path, mechanism) VALUES (?, ?, ?, ?)', [tok.name, tok.value, tok.filePath, tok.mechanism]);
595
+ }
596
+ // Insert class usage
597
+ for (const cu of facts.classUsage) {
598
+ run('INSERT INTO style_class_usage (class_name, file_path, line, mechanism, unresolvable) VALUES (?, ?, ?, ?, ?)', [cu.className, cu.filePath, cu.line, cu.mechanism, cu.unresolvable ? 1 : 0]);
599
+ }
600
+ }
601
+ }
602
+ // ── Run the analyzer (queries the now-populated style_* tables) ───────
603
+ const { UniversalStylesAnalyzer } = await import('./analyzers/universal/UniversalStylesAnalyzer.js');
604
+ const analyzer = new UniversalStylesAnalyzer();
605
+ const result = await analyzer.analyze([], { ...context.config, indexHandle: context.indexHandle });
606
+ const factsConsumed = context.indexHandle.count('style_declarations')
607
+ + context.indexHandle.count('style_tokens')
608
+ + context.indexHandle.count('style_class_usage');
609
+ return { violations: result.violations ?? [], facts: {}, factsConsumed };
610
+ }
611
+ catch {
612
+ return { violations: [], facts: {} };
613
+ }
614
+ },
615
+ defaultConfig: {},
616
+ description: 'Detects style fragmentation, value drift, token bypass, and z-index sprawl',
617
+ category: 'style',
618
+ };
619
+ }
620
+ // ── Conventions reducer (stage 3) ────────────────────────────────────────────
621
+ /**
622
+ * Conventions reducer — queries conventions table and flags deviations
623
+ * (usage-pair, import-form, error-handling, export-shape, naming).
624
+ * Uses analyzer.analyze([]) with rawDb injected.
625
+ */
626
+ export function createConventionsReducer() {
627
+ return {
628
+ name: 'conventions',
629
+ stage: 'reducer',
630
+ consumes: [],
631
+ getRuleIds: () => getRuleIdsFor('conventions'),
632
+ async reduce(_allFacts, context) {
633
+ if (!context.indexHandle)
634
+ return { violations: [], facts: {} };
635
+ try {
636
+ const { UniversalConventionsAnalyzer } = await import('./analyzers/universal/UniversalConventionsAnalyzer.js');
637
+ const analyzer = new UniversalConventionsAnalyzer();
638
+ // Extract per-file source map from the file-sources infrastructure visitor
639
+ const fileSources = _allFacts['file-sources'];
640
+ const sourceMap = fileSources
641
+ ? new Map(Object.entries(fileSources))
642
+ : undefined;
643
+ // B2: Build exports map from function-index visitor facts (AST-extracted)
644
+ const functionIndexFacts = _allFacts['function-index'];
645
+ const exportsMap = functionIndexFacts
646
+ ? new Map(Object.entries(functionIndexFacts)
647
+ .filter(([, data]) => data?.exports?.length)
648
+ .map(([filePath, data]) => [filePath, data.exports]))
649
+ : undefined;
650
+ const config = { ...context.config, indexHandle: context.indexHandle, projectRoot: context.projectRoot, sourceMap, exportsMap };
651
+ const result = await analyzer.analyze([], config);
652
+ const factsConsumed = context.indexHandle.count('conventions');
653
+ return { violations: result.violations ?? [], facts: {}, factsConsumed };
654
+ }
655
+ catch {
656
+ return { violations: [], facts: {} };
657
+ }
658
+ },
659
+ defaultConfig: {},
660
+ description: 'Mines codebase conventions and flags deviations',
661
+ category: 'style',
662
+ };
663
+ }
664
+ // ── Cross-Domain reducer (stage 4) ───────────────────────────────────────────
665
+ /**
666
+ * Cross-domain reducer — runs written-never-read, read-never-written,
667
+ * transaction-boundary risk, validation-bypass, and uncovered-risk detectors.
668
+ * Uses analyzer.analyze([]) with rawDb injected.
669
+ */
670
+ export function createCrossDomainReducer() {
671
+ return {
672
+ name: 'cross-domain',
673
+ stage: 'derivedReducer',
674
+ consumes: [],
675
+ getRuleIds: () => getRuleIdsFor('cross-domain'),
676
+ async reduce(_allFacts, context) {
677
+ if (!context.indexHandle)
678
+ return { violations: [], facts: {} };
679
+ try {
680
+ const { CrossDomainAnalyzer } = await import('./analyzers/crossDomain/CrossDomainAnalyzer.js');
681
+ const analyzer = new CrossDomainAnalyzer();
682
+ const config = { ...context.config, indexHandle: context.indexHandle, projectRoot: context.projectRoot };
683
+ const result = await analyzer.analyze([], config);
684
+ // Count DB rows consumed across primary cross-domain tables
685
+ let factsConsumed = 0;
686
+ try {
687
+ factsConsumed += context.indexHandle.count('schema_usage');
688
+ }
689
+ catch { /* table may not exist */ }
690
+ try {
691
+ factsConsumed += context.indexHandle.count('indexed_functions');
692
+ }
693
+ catch { /* table may not exist */ }
694
+ return { violations: result.violations ?? [], facts: {}, factsConsumed };
695
+ }
696
+ catch (e) {
697
+ console.error('[cross-domain reducer] error:', e.message);
698
+ return { violations: [], facts: {} };
699
+ }
700
+ },
701
+ defaultConfig: {},
702
+ description: 'Detects cross-domain issues (schema lifecycle, validation bypass, coverage gaps)',
703
+ category: 'architecture',
704
+ };
705
+ }
706
+ // ── Invariants reducer (stage 3) ──────────────────────────────────────────────
707
+ /**
708
+ * Invariants reducer — enforces user-defined invariant rules from .codeauditor.json.
709
+ * Receives the full file list via context.files and runs the rule engine across
710
+ * all files at once (call-constraint rules inherently need cross-file scope).
711
+ */
712
+ export function createInvariantsReducer() {
713
+ return {
714
+ name: 'invariants',
715
+ stage: 'reducer',
716
+ consumes: [],
717
+ getRuleIds: () => getRuleIdsFor('invariants'),
718
+ async reduce(_allFacts, context) {
719
+ try {
720
+ const { analyzeInvariants } = await import('./analyzers/invariantsAnalyzer.js');
721
+ // Full file list is in _infra.files (merged into context.config via pipeline)
722
+ const files = context.config.files ?? [];
723
+ // Extract per-file source map from the file-sources infrastructure visitor
724
+ const fileSources = _allFacts['file-sources'];
725
+ const sourceMap = fileSources
726
+ ? new Map(Object.entries(fileSources))
727
+ : undefined;
728
+ const knownFiles = sourceMap
729
+ ? new Set(sourceMap.keys())
730
+ : undefined;
731
+ // B1: Build fileData from the function-index visitor's AST-extracted
732
+ // imports/exports — replaces regex-based extractImports/extractExportedSymbols
733
+ const functionIndexFacts = _allFacts['function-index'];
734
+ let fileData;
735
+ if (functionIndexFacts) {
736
+ fileData = new Map();
737
+ const projectRoot = context.projectRoot;
738
+ for (const [filePath, data] of Object.entries(functionIndexFacts)) {
739
+ if (data && (data.imports || data.exports)) {
740
+ // Normalize absolute paths to project-relative (matching checkRules normalization)
741
+ const normalized = path.isAbsolute(filePath)
742
+ ? path.relative(projectRoot, filePath)
743
+ : filePath;
744
+ fileData.set(normalized, {
745
+ imports: (data.imports || []).map((imp) => ({
746
+ moduleSpecifier: imp.moduleSpecifier,
747
+ isStatic: imp.isStatic,
748
+ isDynamic: imp.isDynamic,
749
+ isRequire: imp.isRequire,
750
+ line: imp.line,
751
+ })),
752
+ exports: (data.exports || []).map((exp) => ({
753
+ name: exp.name,
754
+ line: exp.location?.start?.line ?? 0,
755
+ })),
756
+ });
757
+ }
758
+ }
759
+ }
760
+ const result = await analyzeInvariants(files, { ...context.config, sourceMap, knownFiles, fileData }, { projectRoot: context.projectRoot, indexHandle: context.indexHandle });
761
+ return {
762
+ violations: result.violations ?? [],
763
+ facts: {},
764
+ factsConsumed: files.length,
765
+ };
766
+ }
767
+ catch {
768
+ return { violations: [], facts: {} };
769
+ }
770
+ },
771
+ defaultConfig: {},
772
+ description: 'Enforces custom invariant rules (import bans, call constraints, module boundaries, naming)',
773
+ category: 'architecture',
774
+ };
775
+ }
776
+ // ── Schema visitors and reducer (Task #41 Part A) ──────────────────────────
777
+ // Need provenance for per-file gate + reference extraction
778
+ let _provenanceModule = null;
779
+ async function _getProvenanceModule() {
780
+ if (!_provenanceModule) {
781
+ _provenanceModule = await import('./analyzers/provenance.js');
782
+ }
783
+ return _provenanceModule;
784
+ }
785
+ /**
786
+ * Schema SQL visitor (.sql files) — extracts raw DDL source for the Stage 3
787
+ * reducer to replay into the known-tables catalog.
788
+ */
789
+ export function createSchemaSqlVisitor() {
790
+ return {
791
+ name: 'schema-sql',
792
+ stage: 'visitor',
793
+ extensions: ['.sql'],
794
+ getRuleIds: () => [],
795
+ async visit(_ast, _adapter, context, sourceCode) {
796
+ return {
797
+ violations: [],
798
+ facts: { [context.filePath]: { ddlSource: sourceCode } },
799
+ };
800
+ },
801
+ defaultConfig: {},
802
+ description: 'Captures SQL DDL source for known-table catalog',
803
+ category: 'database',
804
+ };
805
+ }
806
+ /**
807
+ * Schema code visitor (.ts/.tsx/.js/.jsx) — per-file analysis for naming conventions,
808
+ * query patterns, SQL injection, table references, and ORM table extraction.
809
+ */
810
+ export function createSchemaCodeVisitor() {
811
+ const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalSchemaAnalyzer.js').then((m) => ({
812
+ analyzer: new m.UniversalSchemaAnalyzer(),
813
+ defaults: m.DEFAULT_SCHEMA_CONFIG,
814
+ })));
815
+ return {
816
+ name: 'schema-code',
817
+ stage: 'visitor',
818
+ extensions: ['.ts', '.tsx', '.js', '.jsx'],
819
+ getRuleIds: () => getRuleIdsFor('schema'),
820
+ async visit(ast, adapter, context, sourceCode) {
821
+ const { analyzer: a, defaults } = await getAnalyzer();
822
+ const pm = await _getProvenanceModule();
823
+ const violations = [];
824
+ const indexFacts = [];
825
+ // Extract ORM table names (Drizzle) from source
826
+ const ormTables = [];
827
+ const drizzleRe = /(?:pgTable|mysqlTable|sqliteTable)\s*\(\s*['"]([^'"]+)['"]/g;
828
+ let dm;
829
+ while ((dm = drizzleRe.exec(sourceCode)) !== null) {
830
+ ormTables.push(dm[1]);
831
+ }
832
+ // Extract DDL from sql.exec(...) string literals inside Durable Object classes.
833
+ // These are CREATE TABLE / DROP TABLE statements at runtime that migration
834
+ // discovery never sees. Extracting them completes the authoritative catalog
835
+ // so unknown-table doesn't false-positive on DO-local tables.
836
+ const doDDL = [];
837
+ const doTemplateDDL = /`([^`]*(?:CREATE|DROP|ALTER)\s+(?:TABLE|VIRTUAL\s+TABLE)\s+[^`]+)`/gis;
838
+ const doStringDDL = /(["'])((?:\s*(?:CREATE|DROP|ALTER)\s+(?:TABLE|VIRTUAL\s+TABLE)\s+[^"']+))\1/gis;
839
+ let ddlMatch;
840
+ while ((ddlMatch = doTemplateDDL.exec(sourceCode)) !== null) {
841
+ const sql = ddlMatch[1].trim();
842
+ if (sql)
843
+ doDDL.push(sql);
844
+ }
845
+ while ((ddlMatch = doStringDDL.exec(sourceCode)) !== null) {
846
+ const sql = ddlMatch[2].trim();
847
+ if (sql)
848
+ doDDL.push(sql);
849
+ }
850
+ // Build provenance context for this file — defaults from DEFAULT_SCHEMA_CONFIG
851
+ const schemaConfig = (context.config ?? {});
852
+ const detectionMode = schemaConfig.detection?.mode ?? 'hybrid';
853
+ const provenanceContext = pm.buildProvenanceContext(ast, adapter, sourceCode, {
854
+ mode: detectionMode,
855
+ dbReceiverNames: schemaConfig.dbReceiverNames ?? defaults.dbReceiverNames,
856
+ dbBindingNames: schemaConfig.dbBindingNames ?? defaults.dbBindingNames,
857
+ dbCallMethods: schemaConfig.dbCallMethods ?? defaults.dbCallMethods,
858
+ });
859
+ // File gate — skip files without DB usage
860
+ if (!a.passesFileGate(context.filePath, sourceCode, schemaConfig, provenanceContext)) {
861
+ const facts = { [context.filePath]: { tableRefs: [], ormTables } };
862
+ if (doDDL.length > 0)
863
+ facts[context.filePath].ddlSource = doDDL.join(';\n');
864
+ return { violations: [], facts };
865
+ }
866
+ // Build known-tables set from schemas config (pre-pipeline + DB-loaded schemas)
867
+ const schemas = schemaConfig.schemas ?? [];
868
+ const knownTablesArr = schemaConfig.knownTables ?? [];
869
+ const allTables = new Set();
870
+ for (const t of knownTablesArr)
871
+ allTables.add(t);
872
+ for (const schema of schemas) {
873
+ for (const table of (schema.tables ?? [])) {
874
+ allTables.add(table.name);
875
+ }
876
+ }
877
+ // Find table references (per-file, uses allTables for short-id false-positive filtering)
878
+ const tableRefs = a.findTableReferences(ast, adapter, sourceCode, schemaConfig, provenanceContext, allTables);
879
+ // Record schema usage → emit as indexFacts via the shared instance
880
+ a.recordTableUsage(ast, adapter, context.filePath, tableRefs);
881
+ const pending = a.getPendingSchemaRecords();
882
+ // Emit clear-by-file + per-usage index facts
883
+ if (pending.clearFiles.length > 0) {
884
+ for (const filePath of pending.clearFiles) {
885
+ indexFacts.push({ table: 'schema_usage', data: { _action: 'clear-by-file', file_path: filePath } });
886
+ }
887
+ }
888
+ for (const usage of pending.usages) {
889
+ indexFacts.push({
890
+ table: 'schema_usage',
891
+ data: {
892
+ file_path: usage.filePath,
893
+ table_name: usage.tableName,
894
+ function_name: usage.functionName,
895
+ usage_type: usage.usageType,
896
+ line: usage.line,
897
+ column: usage.column,
898
+ raw_query: usage.rawQuery,
899
+ },
900
+ });
901
+ }
902
+ // Check naming conventions
903
+ if (schemaConfig.checkNamingConventions !== false) {
904
+ violations.push(...a.checkNamingConventions(tableRefs, context.filePath));
905
+ }
906
+ // Check query patterns
907
+ if (schemaConfig.validateQueryPatterns !== false) {
908
+ violations.push(...a.checkQueryPatterns(ast, adapter, sourceCode, schemaConfig));
909
+ }
910
+ // Check SQL injection
911
+ violations.push(...a.checkSQLInjection(ast, adapter, sourceCode));
912
+ // Emit facts for the Stage 3 reducer
913
+ const fileFacts = {
914
+ tableRefs: tableRefs.map((r) => ({
915
+ table: r.table,
916
+ type: r.type,
917
+ line: r.location.line,
918
+ column: r.location.column,
919
+ context: r.context,
920
+ })),
921
+ ormTables,
922
+ };
923
+ if (doDDL.length > 0)
924
+ fileFacts.ddlSource = doDDL.join(';\n');
925
+ return {
926
+ violations,
927
+ facts: { [context.filePath]: fileFacts },
928
+ indexFacts: indexFacts.length > 0 ? indexFacts : undefined,
929
+ };
930
+ },
931
+ defaultConfig: {},
932
+ description: 'Per-file schema analysis: naming conventions, query patterns, SQL injection, table references',
933
+ category: 'database',
934
+ };
935
+ }
936
+ /**
937
+ * Schema Prisma visitor (.prisma files) — extracts model names for known-table catalog.
938
+ */
939
+ export function createSchemaPrismaVisitor() {
940
+ return {
941
+ name: 'schema-prisma',
942
+ stage: 'visitor',
943
+ extensions: ['.prisma'],
944
+ getRuleIds: () => [],
945
+ async visit(_ast, _adapter, context, sourceCode) {
946
+ const models = [];
947
+ const modelRe = /model\s+(\w+)\s*\{/g;
948
+ let m;
949
+ while ((m = modelRe.exec(sourceCode)) !== null) {
950
+ models.push(m[1]);
951
+ }
952
+ return {
953
+ violations: [],
954
+ facts: { [context.filePath]: { prismaModels: models } },
955
+ };
956
+ },
957
+ defaultConfig: {},
958
+ description: 'Extracts Prisma model names for known-table catalog',
959
+ category: 'database',
960
+ };
961
+ }
962
+ /**
963
+ * Schema JSON visitor (.json files) — parses JSON for the Stage 3 reducer to validate.
964
+ */
965
+ export function createSchemaJsonVisitor() {
966
+ return {
967
+ name: 'schema-json',
968
+ stage: 'visitor',
969
+ extensions: ['.json'],
970
+ getRuleIds: () => [],
971
+ async visit(_ast, _adapter, context, sourceCode) {
972
+ let parsed = null;
973
+ try {
974
+ parsed = JSON.parse(sourceCode);
975
+ }
976
+ catch {
977
+ // Invalid JSON — skip
978
+ }
979
+ return {
980
+ violations: [],
981
+ facts: { [context.filePath]: { jsonParsed: parsed, jsonRaw: sourceCode } },
982
+ };
983
+ },
984
+ defaultConfig: {},
985
+ description: 'Parses JSON files for schema validation',
986
+ category: 'database',
987
+ };
988
+ }
989
+ /**
990
+ * Schema Stage 3 reducer — cross-file unknown-table detection and JSON schema validation.
991
+ *
992
+ * Consumes facts from schema-sql, schema-code, schema-prisma, and schema-json visitors.
993
+ * Builds the complete known-tables catalog and checks all table references against it.
994
+ */
995
+ export function createSchemaReducer() {
996
+ const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalSchemaAnalyzer.js').then((m) => ({
997
+ analyzer: new m.UniversalSchemaAnalyzer(),
998
+ defaults: m.DEFAULT_SCHEMA_CONFIG,
999
+ })));
1000
+ return {
1001
+ name: 'schema',
1002
+ stage: 'reducer',
1003
+ consumes: [],
1004
+ getRuleIds: () => getRuleIdsFor('schema'),
1005
+ async reduce(allFacts, context) {
1006
+ const { analyzer: a } = await getAnalyzer();
1007
+ const violations = [];
1008
+ const schemaConfig = (context.config ?? {});
1009
+ // allFacts is keyed by visitor name, each value is { [filePath]: { ...per-file facts } }
1010
+ // Flatten to per-file iteration helpers.
1011
+ const perFile = function* () {
1012
+ for (const [, visitorFacts] of Object.entries(allFacts)) {
1013
+ if (typeof visitorFacts === 'object' && visitorFacts !== null) {
1014
+ for (const [filePath, fact] of Object.entries(visitorFacts)) {
1015
+ if (typeof fact === 'object' && fact !== null) {
1016
+ yield [filePath, fact];
1017
+ }
1018
+ }
1019
+ }
1020
+ }
1021
+ };
1022
+ // ── 1. Build known-tables catalog ──────────────────────────────────────
1023
+ const knownTables = new Set();
1024
+ // 1a. SQL DDL replay — collect all .sql file facts, sort by numeric prefix, replay
1025
+ const sqlFiles = [];
1026
+ for (const [filePath, fact] of perFile()) {
1027
+ if ('ddlSource' in fact) {
1028
+ sqlFiles.push({ filePath, source: fact.ddlSource });
1029
+ }
1030
+ }
1031
+ // Sort by numeric prefix in basename: "009_something" < "0010_rename"
1032
+ // Falls back to localeCompare for non-numeric-prefixed names.
1033
+ const numericPrefix = (p) => {
1034
+ const base = p.split('/').pop() ?? p;
1035
+ const m = base.match(/^(\d+)/);
1036
+ return m ? parseInt(m[1], 10) : 0;
1037
+ };
1038
+ sqlFiles.sort((a, b) => {
1039
+ const na = numericPrefix(a.filePath);
1040
+ const nb = numericPrefix(b.filePath);
1041
+ if (na !== nb)
1042
+ return na - nb;
1043
+ return a.filePath.localeCompare(b.filePath);
1044
+ });
1045
+ for (const sqlFile of sqlFiles) {
1046
+ a.processMigrationSource(sqlFile.source, knownTables);
1047
+ }
1048
+ // 1b. ORM tables from code files
1049
+ for (const [, fact] of perFile()) {
1050
+ const ormTables = fact.ormTables ?? [];
1051
+ for (const t of ormTables)
1052
+ knownTables.add(t);
1053
+ }
1054
+ // 1c. Prisma models
1055
+ for (const [, fact] of perFile()) {
1056
+ const prismaModels = fact.prismaModels ?? [];
1057
+ for (const m of prismaModels)
1058
+ knownTables.add(m);
1059
+ }
1060
+ // ── 2. Unknown-table detection ────────────────────────────────────────
1061
+ //
1062
+ // Fail-open guardrail: we can only accuse when the table catalog is
1063
+ // built from authoritative sources. Empty catalog → cannot accuse.
1064
+ //
1065
+ // Authoritative sources (independent of query sites, not circular):
1066
+ // • SQL migration files via schema-sql (step 1a)
1067
+ // • ORM model definitions via schema-code (step 1b)
1068
+ // • Prisma schemas via schema-prisma (step 1c)
1069
+ //
1070
+ // External config tables (schemas from CodeIndexDB, wrangler.toml
1071
+ // pre-discovered tables) are added as a bonus but are not required.
1072
+ //
1073
+ // NOT authoritative: tables inferred from query-text alone. Those
1074
+ // never enter the catalog (table refs are for checking, not building).
1075
+ // Merge external tables from config (bonus, not required).
1076
+ const externalKnownTables = schemaConfig.knownTables ?? [];
1077
+ for (const t of externalKnownTables)
1078
+ knownTables.add(t);
1079
+ if (knownTables.size > 0) {
1080
+ // Collect all table references across all files
1081
+ const allTableRefs = [];
1082
+ for (const [filePath, fact] of perFile()) {
1083
+ const refs = fact.tableRefs ?? [];
1084
+ for (const ref of refs) {
1085
+ allTableRefs.push({ file: filePath, ...ref });
1086
+ }
1087
+ }
1088
+ // 10:1 fail-open ratio guard
1089
+ const unknownRefs = allTableRefs.filter(ref => !knownTables.has(ref.table));
1090
+ if (unknownRefs.length / Math.max(knownTables.size, 1) <= 10) {
1091
+ for (const ref of unknownRefs) {
1092
+ const suggestions = a.getNearestTableSuggestions(ref.table, knownTables, 2);
1093
+ const msg = suggestions.length > 0
1094
+ ? `Reference to unknown table '${ref.table}' (${ref.type}). Did you mean: ${suggestions.join(', ')}?`
1095
+ : `Reference to unknown table '${ref.table}' (${ref.type})`;
1096
+ violations.push({
1097
+ file: ref.file,
1098
+ line: ref.line,
1099
+ column: ref.column,
1100
+ severity: 'suggestion',
1101
+ message: msg,
1102
+ rule: 'unknown-table',
1103
+ analyzer: 'schema',
1104
+ symbol: ref.table,
1105
+ });
1106
+ }
1107
+ }
1108
+ }
1109
+ // ── 3. JSON schema validation ──────────────────────────────────────────
1110
+ if (schemaConfig.validateJsonSchemas !== false) {
1111
+ try {
1112
+ // Build content Map from json facts for the refactored analyzeJsonSchemas
1113
+ const jsonContents = new Map();
1114
+ let jsonFactCount = 0;
1115
+ for (const [filePath, fact] of perFile()) {
1116
+ if ('jsonParsed' in fact) {
1117
+ jsonFactCount++;
1118
+ const raw = (fact.jsonParsed ?? null);
1119
+ // parsed is the result of JSON.parse: null on failure, or the
1120
+ // deserialized value. Only accept objects as usable parsed content.
1121
+ const parsed = raw !== null && typeof raw === 'object' ? raw : null;
1122
+ jsonContents.set(filePath, {
1123
+ parsed,
1124
+ raw: (fact.jsonRaw ?? fact.sourceCode ?? ''),
1125
+ });
1126
+ }
1127
+ }
1128
+ const jsonResult = a.analyzeJsonSchemas(jsonContents, schemaConfig);
1129
+ violations.push(...(jsonResult?.violations ?? []));
1130
+ }
1131
+ catch (e) {
1132
+ // JSON schema validation is best-effort (non-fatal)
1133
+ }
1134
+ }
1135
+ return {
1136
+ violations,
1137
+ facts: {},
1138
+ factsConsumed: [...perFile()].length,
1139
+ };
1140
+ },
1141
+ defaultConfig: {},
1142
+ description: 'Cross-file schema analysis: unknown-table detection and JSON schema validation',
1143
+ category: 'database',
1144
+ };
1145
+ }
1146
+ //# sourceMappingURL=pipelineAdapters.js.map