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
@@ -8,10 +8,30 @@
8
8
  * R2.5: Legacy scan-all-strings path DELETED.
9
9
  * R7: schema/unknown-table severity is "suggestion".
10
10
  */
11
+ import fs from 'fs/promises';
12
+ import path from 'path';
11
13
  import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
12
14
  import picomatch from 'picomatch';
13
15
  import { buildProvenanceContext, isDBProvenanced, DB_CALL_METHODS, } from '../provenance.js';
14
16
  import { OrmAdapterRegistry } from '../orm/index.js';
17
+ import { makeVisitorStatus } from '../../pipeline.js';
18
+ /**
19
+ * Single-source constants for SQL context detection.
20
+ *
21
+ * These are the ground-truth defaults. DEFAULT_SCHEMA_CONFIG references them,
22
+ * and every inline fallback dereferences them directly — so ?? narrowing works
23
+ * (TypeScript infers `string[]`, not `string[] | undefined` from the optional
24
+ * SchemaAnalyzerConfig fields).
25
+ *
26
+ * Trimmed to D1/Workers DB patterns only (4 receivers, 6 methods).
27
+ * Broader entries like 'connection'/'client'/'query'/'get'/'each' matched
28
+ * non-DB code (WebSocket, Map, jQuery, vector stores), causing phantom
29
+ * cross-domain lifecycle violations. See CHANGELOG 3.4.9 accuracy fix.
30
+ */
31
+ export const DB_RECEIVER_NAMES = ['db', 'database', 'sql', 'stmt'];
32
+ export const DB_CALL_METHOD_NAMES = ['exec', 'prepare', 'batch', 'run', 'all', 'first'];
33
+ export const DB_BINDING_NAMES = ['env.DB'];
34
+ export const SQL_TAG_NAMES = ['sql', 'db'];
15
35
  export const DEFAULT_SCHEMA_CONFIG = {
16
36
  enableTableUsageTracking: true,
17
37
  checkMissingReferences: true,
@@ -29,53 +49,79 @@ export const DEFAULT_SCHEMA_CONFIG = {
29
49
  strictMode: false,
30
50
  allowAdditionalProperties: true,
31
51
  // Spec-17 R2 defaults
32
- sqlTagNames: ['sql', 'db'],
33
- dbReceiverNames: ['db', 'database', 'sql', 'stmt', 'connection', 'pool', 'client'],
34
- dbCallMethods: ['exec', 'prepare', 'batch', 'run', 'all', 'first', 'query', 'get', 'each'],
35
- dbBindingNames: ['env.DB'],
52
+ sqlTagNames: [...SQL_TAG_NAMES],
53
+ dbReceiverNames: [...DB_RECEIVER_NAMES],
54
+ dbCallMethods: [...DB_CALL_METHOD_NAMES],
55
+ dbBindingNames: [...DB_BINDING_NAMES],
36
56
  fileGateGlobs: ['**/*.sql', '**/migrations/**'],
37
57
  schemaFiles: [],
38
58
  };
39
- import { promises as fs } from 'fs';
40
- import * as path from 'path';
41
- import { CodeIndexDB } from '../../codeIndexDB.js';
42
59
  export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
43
60
  name = 'schema';
44
61
  description = 'Analyzes code against database schemas and validates JSON schemas';
45
62
  category = 'database';
46
63
  // Track references across files
47
64
  tableReferences = new Map();
48
- // Project root for DB scoping (Bug #4 / Item 1)
65
+ // Spec 25 B4 — Queue schema records for the pipeline to write after stage 2.
66
+ // Was: direct CodeIndexDB.getInstance() call in recordTableUsage.
67
+ _pendingSchemaRecords = { clearFiles: [], usages: [] };
49
68
  projectRoot;
69
+ /**
70
+ * Strip SQL identifier delimiters: backticks or double-quotes.
71
+ */
72
+ stripIdentifier(name) {
73
+ if ((name.startsWith('`') && name.endsWith('`')) ||
74
+ (name.startsWith('"') && name.endsWith('"'))) {
75
+ return name.slice(1, -1);
76
+ }
77
+ return name;
78
+ }
79
+ /**
80
+ * Parse a migration SQL source and apply stateful CREATE/DROP/RENAME
81
+ * operations to the given table set in migration order.
82
+ */
83
+ processMigrationSource(source, tables) {
84
+ // Sequential state machine: apply CREATE/DROP/RENAME in statement order
85
+ // within each migration file. Fixes the rename-replay bug where CREATE
86
+ // after RENAME in the same file was silently deleted by the old three-pass
87
+ // approach (all CREATE then all DROP then all RENAME).
88
+ const ddlRe = /(CREATE)\s+(?:VIRTUAL\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)|(DROP)\s+TABLE\s+(?:IF\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)|(ALTER)\s+TABLE\s+(`[^`]+`|"[^"]+"|\w+)\s+RENAME\s+TO\s+(`[^`]+`|"[^"]+"|\w+)/gi;
89
+ let match;
90
+ while ((match = ddlRe.exec(source)) !== null) {
91
+ const op = match[1] || match[3] || match[5];
92
+ if (op === 'CREATE') {
93
+ tables.add(this.stripIdentifier(match[2]));
94
+ }
95
+ else if (op === 'DROP') {
96
+ tables.delete(this.stripIdentifier(match[4]));
97
+ }
98
+ else if (op === 'ALTER') {
99
+ tables.delete(this.stripIdentifier(match[6]));
100
+ tables.add(this.stripIdentifier(match[7]));
101
+ }
102
+ }
103
+ }
104
+ /**
105
+ * Standalone analyze() override for backward compatibility with direct analyzer
106
+ * calls (e.g., tests and non-pipeline audit paths). All production analysis now
107
+ * flows through the pipeline visitors, but this method is preserved so tests
108
+ * that call analyzer.analyze([file], config) continue to work.
109
+ */
50
110
  async analyze(files, config) {
51
111
  const jsonFiles = files.filter(f => f.endsWith('.json'));
52
112
  const codeFiles = files.filter(f => !f.endsWith('.json'));
53
- // Spec 22 Item 2 — Auto-discover known tables from migration/SQL files
54
- // when no schemas are configured.
55
- //
56
- // Spec 24 Item 2 — wrangler.toml is the external authority for D1 projects:
57
- // parse [[d1_databases]] migration_dir, walk .sql files statefully with
58
- // CREATE/DROP/RENAME to derive the current table catalog.
59
- //
60
- // Spec 24 Item 3 — schemaFiles config lets users point at a full snapshot
61
- // (e.g., snapshots/schema.sql) for easy mode.
62
- //
63
- // Spec 24 Item 4 — Auto-discover from ORM schema definitions (Drizzle
64
- // pgTable/mysqlTable/sqliteTable calls and Prisma schema.prisma model blocks)
65
- // via import provenance.
66
- //
67
- // Discovery priority: wrangler.toml > schemaFiles > migration glob walk > ORM.
113
+ // Auto-discover known tables when no schemas are configured.
68
114
  const schemas = config.schemas;
69
115
  const projectRoot = config.projectRoot || process.cwd();
70
116
  this.projectRoot = projectRoot;
71
117
  if (!schemas || schemas.length === 0) {
72
- const fromWrangler = await this.discoverTablesFromWrangler(projectRoot);
118
+ const fromWrangler = await this._discoverTablesFromWrangler(projectRoot);
73
119
  const schemaFiles = config.schemaFiles;
74
120
  const fromSchemaFiles = schemaFiles && schemaFiles.length > 0
75
- ? await this.discoverTablesFromSchemaFiles(schemaFiles, projectRoot)
121
+ ? await this._discoverTablesFromSchemaFiles(schemaFiles, projectRoot)
76
122
  : new Set();
77
- const fromMigrations = await this.discoverTablesFromMigrations(projectRoot, config);
78
- const fromOrm = await this.discoverTablesFromOrmSchemas(codeFiles);
123
+ const fromMigrations = await this._discoverTablesFromMigrations(projectRoot, config);
124
+ const fromOrm = await this._discoverTablesFromOrmSchemas(codeFiles);
79
125
  const discovered = new Set([
80
126
  ...fromWrangler,
81
127
  ...fromSchemaFiles,
@@ -94,57 +140,127 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
94
140
  }
95
141
  const codeResult = codeFiles.length > 0 ? await super.analyze(codeFiles, config) : {
96
142
  violations: [],
143
+ executionTime: 0,
144
+ status: makeVisitorStatus(0),
145
+ analyzerName: this.name,
146
+ errors: [],
147
+ filesProcessed: 0,
148
+ };
149
+ // Adapt JSON handling to the pipeline-style analyzeJsonSchemas(Map) signature.
150
+ let jsonResult = {
151
+ violations: [],
152
+ executionTime: 0,
153
+ status: makeVisitorStatus(0),
154
+ analyzerName: this.name,
97
155
  errors: [],
98
156
  filesProcessed: 0,
99
- executionTime: 0
100
157
  };
101
- const jsonResult = await this.analyzeJsonSchemas(jsonFiles, config);
158
+ if (jsonFiles.length > 0) {
159
+ const jsonContents = new Map();
160
+ for (const file of jsonFiles) {
161
+ try {
162
+ const raw = await fs.readFile(file, 'utf8');
163
+ let parsed = null;
164
+ try {
165
+ parsed = JSON.parse(raw);
166
+ }
167
+ catch { /* not valid JSON */ }
168
+ jsonContents.set(file, { parsed, raw });
169
+ }
170
+ catch {
171
+ // Skip unreadable files
172
+ }
173
+ }
174
+ jsonResult = this.analyzeJsonSchemas(jsonContents, config);
175
+ }
102
176
  return {
103
177
  violations: [...codeResult.violations, ...jsonResult.violations],
178
+ executionTime: (codeResult.executionTime || 0) + (jsonResult.executionTime || 0),
179
+ status: makeVisitorStatus((codeResult.filesProcessed ?? 0) + (jsonResult.filesProcessed ?? 0)),
180
+ analyzerName: this.name,
104
181
  errors: [...(codeResult.errors || []), ...(jsonResult.errors || [])],
105
- filesProcessed: codeResult.filesProcessed + jsonResult.filesProcessed,
106
- executionTime: (codeResult.executionTime || 0) + (jsonResult.executionTime || 0)
182
+ filesProcessed: (codeResult.filesProcessed ?? 0) + (jsonResult.filesProcessed ?? 0),
107
183
  };
108
184
  }
109
- /**
110
- * Spec 22 Item 2 Auto-discover known tables from migration/SQL files.
111
- *
112
- * When config.schemas is empty, scans files matching fileGateGlobs for
113
- * CREATE TABLE statements and returns the set of discovered table names.
114
- * These are injected as a synthetic schema so the unknown-table detector
115
- * has a reference set to check against.
116
- *
117
- * Only extracts `create`-type references (CREATE TABLE ...), since those
118
- * define tables — SELECT/INSERT/UPDATE references to an unknown table are
119
- * the violations we're trying to avoid flagging.
120
- */
121
- async discoverTablesFromMigrations(projectRoot, config) {
122
- const tables = new Set();
123
- const gateGlobs = config.fileGateGlobs ?? ['**/*.sql', '**/migrations/**'];
124
- // Own glob walk does NOT depend on codeFiles from the caller pipeline.
125
- // Walks projectRoot recursively, matching files against fileGateGlobs via picomatch.
126
- const walkedFiles = await this.walkFiles(projectRoot, gateGlobs);
127
- // Sort for deterministic processing (migrations are often ordered by filename)
128
- walkedFiles.sort();
129
- // Sequential state-machine replay over the migration ledger:
130
- // CREATE adds, DROP removes, RENAME moves — each at its position
131
- // within the file (processMigrationSource applies them inline).
132
- for (const file of walkedFiles) {
133
- try {
134
- const source = await fs.readFile(file, 'utf8');
135
- this.processMigrationSource(source, tables);
185
+ // analyzeAST is required by the protected abstract in UniversalAnalyzer.
186
+ // In production, all schema analysis flows through the pipeline visitors.
187
+ // This method is preserved for backward compatibility with direct test calls.
188
+ async analyzeAST(ast, adapter, config, sourceCode) {
189
+ const violations = [];
190
+ const finalConfig = { ...DEFAULT_SCHEMA_CONFIG, ...config };
191
+ // Spec 21: Build provenance context for this file (R1 — provenance-primary detection)
192
+ const detectionMode = config.detection?.mode ?? 'hybrid';
193
+ const provenanceContext = buildProvenanceContext(ast, adapter, sourceCode, {
194
+ mode: detectionMode,
195
+ dbReceiverNames: finalConfig.dbReceiverNames ?? DEFAULT_SCHEMA_CONFIG.dbReceiverNames,
196
+ dbBindingNames: finalConfig.dbBindingNames ?? DEFAULT_SCHEMA_CONFIG.dbBindingNames,
197
+ dbCallMethods: finalConfig.dbCallMethods ?? DEFAULT_SCHEMA_CONFIG.dbCallMethods,
198
+ });
199
+ // R2.2 File gate: only analyze files with DB context (Spec 21: provenance-based)
200
+ if (!this.passesFileGate(ast.filePath, sourceCode, finalConfig, provenanceContext)) {
201
+ return violations;
202
+ }
203
+ // Get available schemas
204
+ const schemas = finalConfig.schemas || [];
205
+ const allTables = new Set();
206
+ for (const schema of schemas) {
207
+ for (const table of schema.tables) {
208
+ allTables.add(table.name);
136
209
  }
137
- catch {
138
- // Skip unreadable files discovery is best-effort
210
+ }
211
+ if (finalConfig.requiredSchemas && finalConfig.requiredSchemas.length > 0 && schemas.length === 0) {
212
+ violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'No database schemas loaded for analysis', 'warning', 'missing-schemas', undefined, 'top-level:missing-schemas'));
213
+ return violations;
214
+ }
215
+ // R2.1 — AST-based table reference extraction (replaces legacy regex scan-all-strings)
216
+ // Spec 21: Uses provenance context for DB-call pattern detection
217
+ const tableRefs = this.findTableReferences(ast, adapter, sourceCode, finalConfig, provenanceContext, allTables);
218
+ // Spec 15 R1 — Record schema usage for cross-domain lifecycle analysis.
219
+ // Idempotent per-file: clear stale entries before inserting fresh references.
220
+ if (finalConfig.enableTableUsageTracking) {
221
+ this.recordTableUsage(ast, adapter, ast.filePath, tableRefs);
222
+ }
223
+ // Check for missing table references — R2.4: Levenshtein suggestions
224
+ if (finalConfig.checkMissingReferences) {
225
+ // Spec 24 Item 4 Part B — 10:1 fail-open ratio.
226
+ const unknownRefs = tableRefs.filter(ref => !allTables.has(ref.table) && !this.isSystemTable(ref.table));
227
+ const knownCount = allTables.size;
228
+ const unknownCount = unknownRefs.length;
229
+ if (knownCount === 0 || unknownCount / Math.max(knownCount, 1) > 10) {
230
+ const displayRatio = knownCount === 0 ? '∞' : (unknownCount / Math.max(knownCount, 1)).toFixed(1);
231
+ // Silently skip — in direct test mode we don't emit console warnings
232
+ }
233
+ else {
234
+ for (const ref of unknownRefs) {
235
+ const suggestions = this.getNearestTableSuggestions(ref.table, allTables, 2);
236
+ const msg = suggestions.length > 0
237
+ ? `Reference to unknown table '${ref.table}' (${ref.type}). Did you mean: ${suggestions.join(', ')}?`
238
+ : `Reference to unknown table '${ref.table}' (${ref.type})`;
239
+ violations.push(this.createViolation(ast.filePath, ref.location, msg, 'suggestion', 'unknown-table', undefined, ref.table));
240
+ }
139
241
  }
140
242
  }
141
- return tables;
243
+ // Check naming conventions
244
+ if (finalConfig.checkNamingConventions) {
245
+ violations.push(...this.checkNamingConventions(tableRefs, ast.filePath));
246
+ }
247
+ // Check query patterns
248
+ if (finalConfig.validateQueryPatterns) {
249
+ violations.push(...this.checkQueryPatterns(ast, adapter, sourceCode, finalConfig));
250
+ }
251
+ // Check for SQL injection patterns
252
+ violations.push(...this.checkSQLInjection(ast, adapter, sourceCode));
253
+ return violations;
142
254
  }
255
+ // ---------------------------------------------------------------------------
256
+ // Auto-discovery helpers — used by the standalone analyze() override and
257
+ // the pre-pipeline discovery phase in auditRunner.
258
+ // ---------------------------------------------------------------------------
143
259
  /**
144
260
  * Walk project root recursively, returning files matching any of the given
145
261
  * picomatch globs. Skips node_modules and dot-directories.
146
262
  */
147
- async walkFiles(root, globs) {
263
+ async _walkFiles(root, globs) {
148
264
  const results = [];
149
265
  async function walk(dir) {
150
266
  let names;
@@ -170,7 +286,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
170
286
  await walk(fullPath);
171
287
  }
172
288
  else if (stat.isFile()) {
173
- // Check picomatch against the path relative to root
174
289
  const relative = path.relative(root, fullPath);
175
290
  const matched = globs.some(g => picomatch.isMatch(relative, g));
176
291
  if (matched) {
@@ -182,115 +297,32 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
182
297
  await walk(root);
183
298
  return results;
184
299
  }
185
- /**
186
- * Strip SQL identifier delimiters: backticks or double-quotes.
187
- */
188
- stripIdentifier(name) {
189
- if ((name.startsWith('`') && name.endsWith('`')) ||
190
- (name.startsWith('"') && name.endsWith('"'))) {
191
- return name.slice(1, -1);
192
- }
193
- return name;
194
- }
195
- /**
196
- * Parse a migration SQL source and apply stateful CREATE/DROP/RENAME
197
- * operations to the given table set in migration order.
198
- */
199
- processMigrationSource(source, tables) {
200
- // Sequential state machine: apply CREATE/DROP/RENAME in statement order
201
- // within each migration file. Fixes the rename-replay bug where CREATE
202
- // after RENAME in the same file was silently deleted by the old three-pass
203
- // approach (all CREATE then all DROP then all RENAME).
204
- const ddlRe = /(CREATE)\s+(?:VIRTUAL\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)|(DROP)\s+TABLE\s+(?:IF\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)|(ALTER)\s+TABLE\s+(`[^`]+`|"[^"]+"|\w+)\s+RENAME\s+TO\s+(`[^`]+`|"[^"]+"|\w+)/gi;
205
- let match;
206
- while ((match = ddlRe.exec(source)) !== null) {
207
- const op = match[1] || match[3] || match[5];
208
- if (op === 'CREATE') {
209
- tables.add(this.stripIdentifier(match[2]));
210
- }
211
- else if (op === 'DROP') {
212
- tables.delete(this.stripIdentifier(match[4]));
213
- }
214
- else if (op === 'ALTER') {
215
- tables.delete(this.stripIdentifier(match[6]));
216
- tables.add(this.stripIdentifier(match[7]));
217
- }
218
- }
219
- }
220
- /**
221
- * Spec 24 Item 4 — Auto-discover known tables from ORM schema definitions.
222
- *
223
- * Uses import provenance to identify schema files (files importing Drizzle
224
- * table constructors from drizzle-orm) and Prisma's canonical schema.prisma
225
- * filename. Extracts table/model names and feeds them into allTables so the
226
- * unknown-table detector works without explicit user config.
227
- *
228
- * Drizzle: scans .ts/.tsx/.js/.jsx files that import from drizzle-orm for
229
- * pgTable/mysqlTable/sqliteTable('tableName', ...) calls.
230
- * Prisma: scans schema.prisma files for model Name { ... } blocks.
231
- */
232
- async discoverTablesFromOrmSchemas(files) {
300
+ async _discoverTablesFromMigrations(projectRoot, config) {
233
301
  const tables = new Set();
234
- for (const file of files) {
235
- const lowerFile = file.toLowerCase();
236
- // ── Drizzle: import provenance — files importing drizzle-orm table builders ──
237
- if (/\.(ts|tsx|js|jsx)$/i.test(file)) {
238
- try {
239
- const source = await fs.readFile(file, 'utf8');
240
- // Import provenance: only scan files that import from drizzle-orm
241
- if (/from\s+['"]drizzle-orm/.test(source)) {
242
- const builderRegex = /(?:pgTable|mysqlTable|sqliteTable)\s*\(\s*['"]([^'"]+)['"]/g;
243
- let match;
244
- while ((match = builderRegex.exec(source)) !== null) {
245
- tables.add(match[1]);
246
- }
247
- }
248
- }
249
- catch {
250
- // Skip unreadable files — discovery is best-effort
251
- }
302
+ const gateGlobs = config.fileGateGlobs ?? ['**/*.sql', '**/migrations/**'];
303
+ const walkedFiles = await this._walkFiles(projectRoot, gateGlobs);
304
+ walkedFiles.sort();
305
+ for (const file of walkedFiles) {
306
+ try {
307
+ const source = await fs.readFile(file, 'utf8');
308
+ this.processMigrationSource(source, tables);
252
309
  }
253
- // ── Prisma: canonical schema.prisma filename ──
254
- if (file.endsWith('schema.prisma') || file.endsWith('\\schema.prisma')) {
255
- try {
256
- const source = await fs.readFile(file, 'utf8');
257
- const modelRegex = /model\s+(\w+)\s*\{/g;
258
- let match;
259
- while ((match = modelRegex.exec(source)) !== null) {
260
- tables.add(match[1]);
261
- }
262
- }
263
- catch {
264
- // Skip unreadable files — discovery is best-effort
265
- }
310
+ catch {
311
+ // Skip unreadable files
266
312
  }
267
313
  }
268
314
  return tables;
269
315
  }
270
- /**
271
- * Spec 24 Item 2 — Discover tables from wrangler.toml for D1/Cloudflare Workers projects.
272
- *
273
- * wrangler.toml is the external authority declaring where D1 migrations live.
274
- * Parses it for [[d1_databases]] blocks with migrations_dir, then walks the
275
- * migration directory reading .sql files in alphanumeric order (migration
276
- * order). Processes statefully in migration order: CREATE adds, DROP removes,
277
- * RENAME moves inline (delete old + add new) so recreated tables survive.
278
- *
279
- * Returns only the current table set — not historical/transient names.
280
- */
281
- async discoverTablesFromWrangler(projectRoot) {
316
+ async _discoverTablesFromWrangler(projectRoot) {
282
317
  const tables = new Set();
283
- // 1. Look for wrangler.toml in project root
284
318
  const wranglerPath = path.join(projectRoot, 'wrangler.toml');
285
319
  let wranglerContent;
286
320
  try {
287
321
  wranglerContent = await fs.readFile(wranglerPath, 'utf8');
288
322
  }
289
323
  catch {
290
- return tables; // No wrangler.toml — nothing to discover
324
+ return tables; // No wrangler.toml
291
325
  }
292
- // 2. Parse [[d1_databases]] blocks for migrations_dir
293
- // Simple TOML section parser — no dependency needed for this narrow use case
294
326
  const migrationDirs = [];
295
327
  let inD1Block = false;
296
328
  for (const line of wranglerContent.split('\n')) {
@@ -300,7 +332,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
300
332
  continue;
301
333
  }
302
334
  if (inD1Block && trimmed.startsWith('[')) {
303
- // Next TOML section — exit d1_databases block
304
335
  inD1Block = false;
305
336
  continue;
306
337
  }
@@ -311,7 +342,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
311
342
  }
312
343
  }
313
344
  }
314
- // 3. Walk each migration directory, read .sql files in alphanumeric order
315
345
  for (const migDir of migrationDirs) {
316
346
  const absDir = path.resolve(projectRoot, migDir);
317
347
  let entries;
@@ -320,10 +350,10 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
320
350
  entries = dirents
321
351
  .filter(e => e.isFile() && e.name.endsWith('.sql'))
322
352
  .map(e => e.name)
323
- .sort(); // Alphanumeric = chronological migration order
353
+ .sort();
324
354
  }
325
355
  catch {
326
- continue; // Non-existent directory — skip
356
+ continue;
327
357
  }
328
358
  for (const entry of entries) {
329
359
  const filePath = path.join(absDir, entry);
@@ -338,23 +368,12 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
338
368
  }
339
369
  return tables;
340
370
  }
341
- /**
342
- * Spec 24 Item 3 — Discover tables from explicit schema file paths.
343
- *
344
- * Reads each file path (resolved against projectRoot) and extracts
345
- * CREATE TABLE [IF NOT EXISTS] name statements. Supports the common
346
- * D1 pattern where users snapshot their full schema to a single .sql file
347
- * (e.g., snapshots/schema.sql).
348
- */
349
- async discoverTablesFromSchemaFiles(schemaFiles, projectRoot) {
371
+ async _discoverTablesFromSchemaFiles(schemaFiles, projectRoot) {
350
372
  const tables = new Set();
351
373
  for (const file of schemaFiles) {
352
374
  const absPath = path.resolve(projectRoot, file);
353
375
  try {
354
376
  const source = await fs.readFile(absPath, 'utf8');
355
- // Only extract CREATE TABLE — these are snapshots, not migration streams,
356
- // so no stateful DROP/RENAME processing needed.
357
- // v3.4.8: Added VIRTUAL TABLE support (FTS tables) and quoted/backtick identifiers
358
377
  const createRe = /CREATE\s+(?:VIRTUAL\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)/gi;
359
378
  let match;
360
379
  while ((match = createRe.exec(source)) !== null) {
@@ -362,94 +381,45 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
362
381
  }
363
382
  }
364
383
  catch {
365
- // Skip unreadable/missing files — best-effort
384
+ // Skip unreadable files
366
385
  }
367
386
  }
368
387
  return tables;
369
388
  }
370
- async analyzeAST(ast, adapter, config, sourceCode) {
371
- const violations = [];
372
- const finalConfig = { ...DEFAULT_SCHEMA_CONFIG, ...config };
373
- // Spec 21: Build provenance context for this file (R1 — provenance-primary detection)
374
- const detectionMode = config.detection?.mode ?? 'hybrid';
375
- const p0 = performance.now();
376
- const provenanceContext = buildProvenanceContext(ast, adapter, sourceCode, {
377
- mode: detectionMode,
378
- dbReceiverNames: finalConfig.dbReceiverNames ?? DEFAULT_SCHEMA_CONFIG.dbReceiverNames,
379
- dbBindingNames: finalConfig.dbBindingNames ?? DEFAULT_SCHEMA_CONFIG.dbBindingNames,
380
- dbCallMethods: finalConfig.dbCallMethods ?? DEFAULT_SCHEMA_CONFIG.dbCallMethods,
381
- });
382
- const timingAcc = config._provenanceTiming;
383
- if (timingAcc)
384
- timingAcc.totalMs += performance.now() - p0;
385
- // R2.2 — File gate: only analyze files with DB context (Spec 21: provenance-based)
386
- if (!this.passesFileGate(ast.filePath, sourceCode, finalConfig, provenanceContext)) {
387
- return violations;
388
- }
389
- // Get available schemas
390
- const schemas = finalConfig.schemas || [];
391
- const allTables = new Set();
392
- for (const schema of schemas) {
393
- for (const table of schema.tables) {
394
- allTables.add(table.name);
395
- }
396
- }
397
- if (finalConfig.requiredSchemas && finalConfig.requiredSchemas.length > 0 && schemas.length === 0) {
398
- violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'No database schemas loaded for analysis', 'warning', 'missing-schemas', undefined, 'top-level:missing-schemas'));
399
- return violations;
400
- }
401
- // R2.1 — AST-based table reference extraction (replaces legacy regex scan-all-strings)
402
- // Spec 21: Uses provenance context for DB-call pattern detection
403
- const tableRefs = this.findTableReferences(ast, adapter, sourceCode, finalConfig, provenanceContext, allTables);
404
- // Spec 15 R1 — Record schema usage for cross-domain lifecycle analysis.
405
- // Idempotent per-file: clear stale entries before inserting fresh references.
406
- if (finalConfig.enableTableUsageTracking) {
407
- this.recordTableUsage(ast, adapter, ast.filePath, tableRefs);
408
- }
409
- // Check for missing table references — R2.4: Levenshtein suggestions
410
- if (finalConfig.checkMissingReferences) {
411
- // Spec 24 Item 4 Part B — 10:1 fail-open ratio.
412
- // When unknown table references vastly outnumber known tables, the
413
- // schema catalog is likely incomplete (e.g. external/managed tables).
414
- // Disable the rule with a warning instead of flooding the output with
415
- // false positives.
416
- const unknownRefs = tableRefs.filter(ref => !allTables.has(ref.table) && !this.isSystemTable(ref.table));
417
- const knownCount = allTables.size;
418
- const unknownCount = unknownRefs.length;
419
- // Spec 24 Item 4 Part B — 10:1 fail-open ratio.
420
- // When zero known tables: a detector that knows zero tables may not
421
- // call anything unknown — every reference is "unknown" by construction.
422
- // When known tables exist: disable if unknown:known ratio exceeds 10:1.
423
- if (knownCount === 0 || unknownCount / Math.max(knownCount, 1) > 10) {
424
- const displayRatio = knownCount === 0 ? '∞' : (unknownCount / Math.max(knownCount, 1)).toFixed(1);
425
- console.error(`[code-auditor] unknown-table rule disabled: ` +
426
- `${unknownCount} unknown refs vs ${knownCount} known tables ` +
427
- `(ratio ${displayRatio}:1 exceeds 10:1). ` +
428
- `Add schemas to .codeauditor.json or ORM schema files.`);
429
- // Skip unknown-table findings — fall through to column refs below
389
+ async _discoverTablesFromOrmSchemas(files) {
390
+ const tables = new Set();
391
+ for (const file of files) {
392
+ const lowerFile = file.toLowerCase();
393
+ if (/\.(ts|tsx|js|jsx)$/i.test(file)) {
394
+ try {
395
+ const source = await fs.readFile(file, 'utf8');
396
+ if (/from\s+['"]drizzle-orm/.test(source)) {
397
+ const builderRegex = /(?:pgTable|mysqlTable|sqliteTable)\s*\(\s*['"]([^'"]+)['"]/g;
398
+ let match;
399
+ while ((match = builderRegex.exec(source)) !== null) {
400
+ tables.add(match[1]);
401
+ }
402
+ }
403
+ }
404
+ catch {
405
+ // Skip unreadable files
406
+ }
430
407
  }
431
- else {
432
- for (const ref of unknownRefs) {
433
- const suggestions = this.getNearestTableSuggestions(ref.table, allTables, 2);
434
- const msg = suggestions.length > 0
435
- ? `Reference to unknown table '${ref.table}' (${ref.type}). Did you mean: ${suggestions.join(', ')}?`
436
- : `Reference to unknown table '${ref.table}' (${ref.type})`;
437
- violations.push(this.createViolation(ast.filePath, ref.location, msg, 'suggestion', // R7
438
- 'unknown-table', undefined, ref.table));
408
+ if (file.endsWith('schema.prisma') || file.endsWith('\\schema.prisma')) {
409
+ try {
410
+ const source = await fs.readFile(file, 'utf8');
411
+ const modelRegex = /model\s+(\w+)\s*\{/g;
412
+ let match;
413
+ while ((match = modelRegex.exec(source)) !== null) {
414
+ tables.add(match[1]);
415
+ }
416
+ }
417
+ catch {
418
+ // Skip unreadable files
439
419
  }
440
420
  }
441
421
  }
442
- // Check naming conventions
443
- if (finalConfig.checkNamingConventions) {
444
- violations.push(...this.checkNamingConventions(tableRefs, ast.filePath));
445
- }
446
- // Check query patterns
447
- if (finalConfig.validateQueryPatterns) {
448
- violations.push(...this.checkQueryPatterns(ast, adapter, sourceCode, finalConfig));
449
- }
450
- // Check for SQL injection patterns — R7: no critical by default
451
- violations.push(...this.checkSQLInjection(ast, adapter, sourceCode));
452
- return violations;
422
+ return tables;
453
423
  }
454
424
  // ---------------------------------------------------------------------------
455
425
  // R2.2 — File gate
@@ -492,14 +462,14 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
492
462
  return true;
493
463
  }
494
464
  // Check for env-binding patterns (e.g., env.DB in Cloudflare Workers)
495
- const bindingNames = config.dbBindingNames ?? ['env.DB'];
465
+ const bindingNames = config.dbBindingNames ?? [...DB_BINDING_NAMES];
496
466
  for (const binding of bindingNames) {
497
467
  if (sourceCode.includes(binding))
498
468
  return true;
499
469
  }
500
470
  // Check for DB call patterns (receiver.method)
501
- const receivers = config.dbReceiverNames ?? ['db', 'database', 'sql', 'stmt'];
502
- const methods = config.dbCallMethods ?? ['exec', 'prepare', 'batch', 'run', 'all', 'first'];
471
+ const receivers = config.dbReceiverNames ?? [...DB_RECEIVER_NAMES];
472
+ const methods = config.dbCallMethods ?? [...DB_CALL_METHOD_NAMES];
503
473
  for (const receiver of receivers) {
504
474
  for (const method of methods) {
505
475
  const pattern = new RegExp(`\\b${escapeRegex(receiver)}\\.${escapeRegex(method)}\\s*\\(`);
@@ -509,7 +479,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
509
479
  }
510
480
  }
511
481
  // Check for SQL tagged template literals (syntax feature, not naming convention)
512
- const sqlTags = config.sqlTagNames ?? ['sql', 'db'];
482
+ const sqlTags = config.sqlTagNames ?? [...SQL_TAG_NAMES];
513
483
  for (const tag of sqlTags) {
514
484
  const pattern = new RegExp(`\\b${escapeRegex(tag)}\`\\s*SELECT|\\b${escapeRegex(tag)}\`\\s*INSERT|\\b${escapeRegex(tag)}\`\\s*UPDATE|\\b${escapeRegex(tag)}\`\\s*DELETE|\\b${escapeRegex(tag)}\`\\s*CREATE`, 'i');
515
485
  if (pattern.test(sourceCode))
@@ -527,7 +497,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
527
497
  */
528
498
  findTableReferences(ast, adapter, sourceCode, config, provenanceContext, allTables) {
529
499
  const references = [];
530
- const sqlTags = config.sqlTagNames ?? ['sql', 'db'];
500
+ const sqlTags = config.sqlTagNames ?? [...SQL_TAG_NAMES];
531
501
  // (1) Tagged template SQL — e.g. sql`SELECT * FROM heroes`
532
502
  // This is a syntax feature, not a naming convention — keep the sqlTagNames gate.
533
503
  const taggedTemplates = adapter.findNodes(ast, {
@@ -564,8 +534,8 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
564
534
  const callee = this.getCallee(node, adapter, sourceCode);
565
535
  if (!callee)
566
536
  return false;
567
- const dbMethods = config.dbCallMethods ?? ['exec', 'prepare', 'batch', 'run', 'all', 'first', 'query'];
568
- const dbReceivers = config.dbReceiverNames ?? ['db', 'database', 'sql', 'stmt'];
537
+ const dbMethods = config.dbCallMethods ?? [...DB_CALL_METHOD_NAMES];
538
+ const dbReceivers = config.dbReceiverNames ?? [...DB_RECEIVER_NAMES];
569
539
  return this.isDbMemberCall(node, callee, dbMethods, dbReceivers, adapter, sourceCode);
570
540
  },
571
541
  });
@@ -619,8 +589,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
619
589
  */
620
590
  recordTableUsage(ast, adapter, filePath, references) {
621
591
  try {
622
- const db = CodeIndexDB.getInstance(undefined, this.projectRoot);
623
- db.clearSchemaUsageForFile(filePath);
592
+ this._pendingSchemaRecords.clearFiles.push(filePath);
624
593
  for (const ref of references) {
625
594
  // Find enclosing function from the AST position
626
595
  const node = this.findClosestNodeAt(ast.root, ref.location, adapter);
@@ -629,7 +598,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
629
598
  : ast.filePath.endsWith('.sql') || ast.filePath.includes('/migrations/')
630
599
  ? 'schema-file'
631
600
  : 'top-level';
632
- const usage = {
601
+ this._pendingSchemaRecords.usages.push({
633
602
  tableName: ref.table,
634
603
  filePath,
635
604
  functionName,
@@ -637,14 +606,19 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
637
606
  line: ref.location.line,
638
607
  column: ref.location.column,
639
608
  rawQuery: ref.context,
640
- };
641
- db.recordSchemaUsage(usage);
609
+ });
642
610
  }
643
611
  }
644
612
  catch {
645
613
  // Schema recording is best-effort — failures don't block analysis.
646
614
  }
647
615
  }
616
+ /** Spec 25 B4 — Drain pending schema records for the pipeline to write. */
617
+ getPendingSchemaRecords() {
618
+ const records = this._pendingSchemaRecords;
619
+ this._pendingSchemaRecords = { clearFiles: [], usages: [] };
620
+ return records;
621
+ }
648
622
  /**
649
623
  * Parse SQL table names from a SQL text string.
650
624
  * R2.3: Template expressions (${...}) resolve portions to wildcards.
@@ -1164,23 +1138,68 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
1164
1138
  // ---------------------------------------------------------------------------
1165
1139
  // JSON Schema validation (unchanged from original)
1166
1140
  // ---------------------------------------------------------------------------
1167
- async analyzeJsonSchemas(files, config) {
1141
+ /**
1142
+ * Analyze JSON schemas and validate data files against them.
1143
+ *
1144
+ * Pipeline-refactored: accepts a Map of pre-loaded JSON contents instead of
1145
+ * reading from the filesystem, so this can run inside a Stage 3 reducer.
1146
+ *
1147
+ * @param jsonContents Map from filePath → { parsed: pre-parsed object or null, raw: string }
1148
+ * @param config Schema analyzer configuration
1149
+ */
1150
+ analyzeJsonSchemas(jsonContents, config) {
1168
1151
  const violations = [];
1169
1152
  const errors = [];
1170
1153
  let filesProcessed = 0;
1171
1154
  const startTime = Date.now();
1172
1155
  const finalConfig = { ...DEFAULT_SCHEMA_CONFIG, ...config };
1173
1156
  if (!finalConfig.validateJsonSchemas) {
1174
- return { violations, errors, filesProcessed, executionTime: 0 };
1157
+ return { violations, errors, status: makeVisitorStatus(filesProcessed), executionTime: 0, analyzerName: this.name };
1175
1158
  }
1159
+ const files = Array.from(jsonContents.keys());
1176
1160
  const schemaFiles = this.identifySchemaFiles(files, finalConfig);
1177
1161
  const dataFiles = this.identifyDataFiles(files, finalConfig);
1178
1162
  const unknownJsonFiles = files.filter(f => !schemaFiles.includes(f) && !dataFiles.includes(f));
1163
+ // Helper: parse from Map content (with fallback)
1164
+ const getParsed = (filePath) => {
1165
+ const entry = jsonContents.get(filePath);
1166
+ if (!entry)
1167
+ return null;
1168
+ if (entry.parsed !== null)
1169
+ return entry.parsed;
1170
+ try {
1171
+ return JSON.parse(entry.raw);
1172
+ }
1173
+ catch {
1174
+ return null;
1175
+ }
1176
+ };
1179
1177
  const schemas = new Map();
1180
1178
  for (const file of schemaFiles) {
1181
1179
  try {
1182
- const content = await fs.readFile(file, 'utf8');
1183
- const schema = JSON.parse(content);
1180
+ const entry = jsonContents.get(file);
1181
+ const content = entry?.raw;
1182
+ if (!content) {
1183
+ violations.push({
1184
+ file,
1185
+ line: 1,
1186
+ column: 1,
1187
+ severity: 'warning',
1188
+ message: `Content not available for schema file`,
1189
+ rule: 'file-error',
1190
+ analyzer: 'schema'
1191
+ });
1192
+ filesProcessed++;
1193
+ continue;
1194
+ }
1195
+ const schema = entry.parsed ?? (() => { try {
1196
+ return JSON.parse(content);
1197
+ }
1198
+ catch {
1199
+ return null;
1200
+ } })();
1201
+ if (schema === null)
1202
+ throw new SyntaxError('JSON parse failed');
1184
1203
  schemas.set(file, schema);
1185
1204
  const fileViolations = this.validateJsonSchema(schema, file, finalConfig);
1186
1205
  violations.push(...fileViolations);
@@ -1209,13 +1228,28 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
1209
1228
  }
1210
1229
  if (finalConfig.schemaDataPairs) {
1211
1230
  for (const pair of finalConfig.schemaDataPairs) {
1212
- const schema = schemas.get(pair.schema) || await this.loadSchema(pair.schema);
1231
+ const schema = schemas.get(pair.schema) ?? getParsed(pair.schema);
1213
1232
  if (schema) {
1214
1233
  const dataFiles = Array.isArray(pair.data) ? pair.data : [pair.data];
1215
1234
  for (const dataFile of dataFiles) {
1216
1235
  if (files.includes(dataFile)) {
1217
- const dataViolations = await this.validateDataAgainstSchema(dataFile, schema, finalConfig);
1218
- violations.push(...dataViolations);
1236
+ const parsed = getParsed(dataFile);
1237
+ if (parsed !== null) {
1238
+ const dataViolations = [];
1239
+ this.validateAgainstSchema(parsed, schema, dataFile, dataViolations, finalConfig);
1240
+ violations.push(...dataViolations);
1241
+ }
1242
+ else {
1243
+ violations.push({
1244
+ file: dataFile,
1245
+ line: 1,
1246
+ column: 1,
1247
+ severity: 'warning',
1248
+ message: 'Invalid JSON in data file',
1249
+ rule: 'invalid-json',
1250
+ analyzer: 'schema'
1251
+ });
1252
+ }
1219
1253
  filesProcessed++;
1220
1254
  }
1221
1255
  }
@@ -1226,21 +1260,22 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
1226
1260
  for (const dataFile of [...dataFiles, ...unknownJsonFiles]) {
1227
1261
  const matchedSchema = this.findMatchingSchema(dataFile, schemas, finalConfig);
1228
1262
  if (matchedSchema) {
1229
- const dataViolations = await this.validateDataAgainstSchema(dataFile, matchedSchema, finalConfig);
1230
- violations.push(...dataViolations);
1263
+ const parsed = getParsed(dataFile);
1264
+ if (parsed !== null) {
1265
+ const dataViolations = [];
1266
+ this.validateAgainstSchema(parsed, matchedSchema, dataFile, dataViolations, finalConfig);
1267
+ violations.push(...dataViolations);
1268
+ }
1231
1269
  }
1232
1270
  else if (unknownJsonFiles.includes(dataFile)) {
1233
- try {
1234
- const content = await fs.readFile(dataFile, 'utf8');
1235
- JSON.parse(content);
1236
- }
1237
- catch (error) {
1271
+ const parsed = getParsed(dataFile);
1272
+ if (parsed === null) {
1238
1273
  violations.push({
1239
1274
  file: dataFile,
1240
1275
  line: 1,
1241
1276
  column: 1,
1242
1277
  severity: 'warning',
1243
- message: `Invalid JSON: ${error instanceof Error ? error.message : 'Parse error'}`,
1278
+ message: `Invalid JSON: Parse error`,
1244
1279
  rule: 'invalid-json',
1245
1280
  analyzer: 'schema'
1246
1281
  });
@@ -1252,8 +1287,9 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
1252
1287
  return {
1253
1288
  violations,
1254
1289
  errors,
1255
- filesProcessed,
1256
- executionTime: Date.now() - startTime
1290
+ status: makeVisitorStatus(filesProcessed),
1291
+ executionTime: Date.now() - startTime,
1292
+ analyzerName: this.name,
1257
1293
  };
1258
1294
  }
1259
1295
  validateJsonSchema(schema, filePath, config) {
@@ -1357,15 +1393,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
1357
1393
  });
1358
1394
  });
1359
1395
  }
1360
- async loadSchema(schemaPath) {
1361
- try {
1362
- const content = await fs.readFile(schemaPath, 'utf8');
1363
- return JSON.parse(content);
1364
- }
1365
- catch {
1366
- return null;
1367
- }
1368
- }
1369
1396
  findMatchingSchema(dataFile, schemas, config) {
1370
1397
  const dataFileName = dataFile.split('/').pop() || '';
1371
1398
  const dataBaseName = dataFileName.replace(/\.(data|example|test)\.json$/, '');
@@ -1381,39 +1408,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
1381
1408
  }
1382
1409
  return null;
1383
1410
  }
1384
- async validateDataAgainstSchema(dataFile, schema, config) {
1385
- const violations = [];
1386
- try {
1387
- const content = await fs.readFile(dataFile, 'utf8');
1388
- const data = JSON.parse(content);
1389
- this.validateAgainstSchema(data, schema, dataFile, violations, config);
1390
- }
1391
- catch (error) {
1392
- if (error instanceof SyntaxError) {
1393
- violations.push({
1394
- file: dataFile,
1395
- line: 1,
1396
- column: 1,
1397
- severity: 'warning',
1398
- message: `Invalid JSON: ${error.message}`,
1399
- rule: 'invalid-json',
1400
- analyzer: 'schema'
1401
- });
1402
- }
1403
- else {
1404
- violations.push({
1405
- file: dataFile,
1406
- line: 1,
1407
- column: 1,
1408
- severity: 'warning',
1409
- message: `Error reading file: ${error instanceof Error ? error.message : 'Unknown error'}`,
1410
- rule: 'file-error',
1411
- analyzer: 'schema'
1412
- });
1413
- }
1414
- }
1415
- return violations;
1416
- }
1417
1411
  validateAgainstSchema(data, schema, filePath, violations, config, path = '') {
1418
1412
  if (schema.type) {
1419
1413
  const actualType = Array.isArray(data) ? 'array' :