driftdetect 0.9.30 → 0.9.31

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 (37) hide show
  1. package/dist/bin/drift.js +3 -1
  2. package/dist/bin/drift.js.map +1 -1
  3. package/dist/commands/approve.d.ts +6 -0
  4. package/dist/commands/approve.d.ts.map +1 -1
  5. package/dist/commands/approve.js +91 -2
  6. package/dist/commands/approve.js.map +1 -1
  7. package/dist/commands/audit.d.ts +29 -0
  8. package/dist/commands/audit.d.ts.map +1 -0
  9. package/dist/commands/audit.js +495 -0
  10. package/dist/commands/audit.js.map +1 -0
  11. package/dist/commands/callgraph.d.ts.map +1 -1
  12. package/dist/commands/callgraph.js +174 -39
  13. package/dist/commands/callgraph.js.map +1 -1
  14. package/dist/commands/coupling.d.ts.map +1 -1
  15. package/dist/commands/coupling.js +131 -1
  16. package/dist/commands/coupling.js.map +1 -1
  17. package/dist/commands/env.d.ts.map +1 -1
  18. package/dist/commands/env.js +80 -1
  19. package/dist/commands/env.js.map +1 -1
  20. package/dist/commands/error-handling.d.ts.map +1 -1
  21. package/dist/commands/error-handling.js +126 -1
  22. package/dist/commands/error-handling.js.map +1 -1
  23. package/dist/commands/index.d.ts +1 -0
  24. package/dist/commands/index.d.ts.map +1 -1
  25. package/dist/commands/index.js +2 -0
  26. package/dist/commands/index.js.map +1 -1
  27. package/dist/commands/scan.d.ts +4 -0
  28. package/dist/commands/scan.d.ts.map +1 -1
  29. package/dist/commands/scan.js +490 -181
  30. package/dist/commands/scan.js.map +1 -1
  31. package/dist/commands/watch.d.ts.map +1 -1
  32. package/dist/commands/watch.js +3 -2
  33. package/dist/commands/watch.js.map +1 -1
  34. package/dist/commands/wrappers.d.ts.map +1 -1
  35. package/dist/commands/wrappers.js +184 -0
  36. package/dist/commands/wrappers.js.map +1 -1
  37. package/package.json +13 -13
@@ -11,7 +11,9 @@ import * as fs from 'node:fs/promises';
11
11
  import * as path from 'node:path';
12
12
  import * as crypto from 'node:crypto';
13
13
  import chalk from 'chalk';
14
- import { PatternStore, HistoryStore, FileWalker, createDataLake, loadProjectConfig, getProjectRegistry, createTelemetryClient, createTestTopologyAnalyzer, createCallGraphAnalyzer, } from 'driftdetect-core';
14
+ import { PatternStore, HistoryStore, FileWalker, createDataLake, loadProjectConfig, getProjectRegistry, createTelemetryClient, createTestTopologyAnalyzer, createCallGraphAnalyzer, getDefaultIgnorePatterns, mergeIgnorePatterns,
15
+ // Native adapters with TypeScript fallback
16
+ isNativeAvailable, analyzeTestTopologyWithFallback, scanBoundariesWithFallback, analyzeConstantsWithFallback, buildCallGraph, ConstantStore, } from 'driftdetect-core';
15
17
  import { createSpinner, status } from '../ui/spinner.js';
16
18
  import { createPatternsTable } from '../ui/table.js';
17
19
  import { createScannerService } from '../services/scanner-service.js';
@@ -157,87 +159,20 @@ async function isDriftInitialized(rootDir) {
157
159
  }
158
160
  /**
159
161
  * Load ignore patterns from .driftignore
160
- * Includes ecosystem-aware defaults for enterprise codebases
162
+ * Uses enterprise-grade defaults from @driftdetect/core
161
163
  */
162
164
  async function loadIgnorePatterns(rootDir) {
163
- const defaultIgnores = [
164
- // Universal
165
- 'node_modules/**',
166
- '.git/**',
167
- 'dist/**',
168
- 'build/**',
169
- 'coverage/**',
170
- '.drift/**',
171
- // Python
172
- '__pycache__/**',
173
- '.venv/**',
174
- 'venv/**',
175
- '.eggs/**',
176
- '*.egg-info/**',
177
- '.tox/**',
178
- '.mypy_cache/**',
179
- '.pytest_cache/**',
180
- // .NET / C# / MAUI / Blazor
181
- 'bin/**',
182
- 'obj/**',
183
- 'packages/**',
184
- '.vs/**',
185
- '*.dll',
186
- '*.exe',
187
- '*.pdb',
188
- '*.nupkg',
189
- 'wwwroot/lib/**',
190
- // Java / Spring / Gradle / Maven
191
- 'target/**',
192
- '.gradle/**',
193
- '.m2/**',
194
- '*.class',
195
- '*.jar',
196
- '*.war',
197
- // Go
198
- 'vendor/**',
199
- // Rust (target already covered above)
200
- // C++ / CMake
201
- 'cmake-build-*/**',
202
- 'out/**',
203
- '*.o',
204
- '*.obj',
205
- '*.a',
206
- '*.lib',
207
- '*.so',
208
- '*.dylib',
209
- // Node extras
210
- '.npm/**',
211
- '.yarn/**',
212
- '.pnpm-store/**',
213
- '.next/**',
214
- '.nuxt/**',
215
- // IDE / Editor
216
- '.idea/**',
217
- '.vscode/**',
218
- '*.swp',
219
- '*.swo',
220
- // Archives / binaries (common in enterprise)
221
- '*.zip',
222
- '*.rar',
223
- '*.7z',
224
- '*.tar',
225
- '*.gz',
226
- // Logs
227
- '*.log',
228
- 'logs/**',
229
- ];
230
165
  try {
231
166
  const driftignorePath = path.join(rootDir, '.driftignore');
232
167
  const content = await fs.readFile(driftignorePath, 'utf-8');
233
- const patterns = content
168
+ const userPatterns = content
234
169
  .split('\n')
235
170
  .map((line) => line.trim())
236
171
  .filter((line) => line && !line.startsWith('#'));
237
- return [...defaultIgnores, ...patterns];
172
+ return mergeIgnorePatterns(userPatterns);
238
173
  }
239
174
  catch {
240
- return defaultIgnores;
175
+ return getDefaultIgnorePatterns();
241
176
  }
242
177
  }
243
178
  /**
@@ -1054,59 +989,186 @@ async function scanSingleProject(rootDir, options, quiet = false) {
1054
989
  const boundarySpinner = createSpinner('Scanning for data boundaries...');
1055
990
  boundarySpinner.start();
1056
991
  try {
1057
- const boundaryScanner = createBoundaryScanner({ rootDir, verbose });
1058
- await boundaryScanner.initialize();
1059
- const boundaryResult = await boundaryScanner.scanFiles(files);
1060
- // Store result for materializer
1061
- scanBoundaryResult = boundaryResult;
1062
- boundarySpinner.succeed(`Found ${boundaryResult.stats.tablesFound} tables, ` +
1063
- `${boundaryResult.stats.accessPointsFound} access points`);
1064
- // Show sensitive field access warnings
1065
- if (boundaryResult.stats.sensitiveFieldsFound > 0) {
1066
- console.log();
1067
- console.log(chalk.bold.yellow(`⚠️ ${boundaryResult.stats.sensitiveFieldsFound} Sensitive Field Access Detected:`));
1068
- const sensitiveFields = boundaryResult.accessMap.sensitiveFields.slice(0, 5);
1069
- for (const field of sensitiveFields) {
1070
- const fieldName = field.table ? `${field.table}.${field.field}` : field.field;
1071
- console.log(chalk.yellow(` ${fieldName} (${field.sensitivityType}) - ${field.file}:${field.line}`));
992
+ // Try native analyzer first (much faster)
993
+ if (isNativeAvailable()) {
994
+ try {
995
+ const nativeResult = await scanBoundariesWithFallback(rootDir, files);
996
+ // Convert native result to BoundaryScanResult format
997
+ // Note: Native types may differ slightly, so we convert carefully
998
+ const accessPointsMap = {};
999
+ for (const ap of nativeResult.accessPoints) {
1000
+ const id = `${ap.file}:${ap.line}:0:${ap.table}`;
1001
+ accessPointsMap[id] = {
1002
+ id,
1003
+ table: ap.table,
1004
+ fields: ap.fields,
1005
+ operation: ap.operation,
1006
+ file: ap.file,
1007
+ line: ap.line,
1008
+ column: 0,
1009
+ context: '',
1010
+ isRawSql: false,
1011
+ confidence: ap.confidence,
1012
+ };
1013
+ }
1014
+ const sensitiveFields = nativeResult.sensitiveFields.map(sf => ({
1015
+ field: sf.field,
1016
+ table: sf.table ?? null,
1017
+ sensitivityType: sf.sensitivityType,
1018
+ file: sf.file,
1019
+ line: sf.line,
1020
+ confidence: sf.confidence,
1021
+ }));
1022
+ const models = nativeResult.models.map(m => ({
1023
+ name: m.name,
1024
+ tableName: m.tableName,
1025
+ fields: m.fields,
1026
+ file: m.file,
1027
+ line: m.line,
1028
+ framework: m.framework,
1029
+ confidence: m.confidence,
1030
+ }));
1031
+ // Build tables map from access points
1032
+ const tablesMap = {};
1033
+ for (const ap of nativeResult.accessPoints) {
1034
+ let tableInfo = tablesMap[ap.table];
1035
+ if (!tableInfo) {
1036
+ tableInfo = {
1037
+ name: ap.table,
1038
+ model: null,
1039
+ fields: [],
1040
+ sensitiveFields: [],
1041
+ accessedBy: [],
1042
+ };
1043
+ tablesMap[ap.table] = tableInfo;
1044
+ }
1045
+ const apId = `${ap.file}:${ap.line}:0:${ap.table}`;
1046
+ const fullAp = accessPointsMap[apId];
1047
+ if (fullAp) {
1048
+ tableInfo.accessedBy.push(fullAp);
1049
+ }
1050
+ }
1051
+ scanBoundaryResult = {
1052
+ accessMap: {
1053
+ version: '1.0',
1054
+ generatedAt: new Date().toISOString(),
1055
+ projectRoot: rootDir,
1056
+ tables: tablesMap,
1057
+ accessPoints: accessPointsMap,
1058
+ sensitiveFields,
1059
+ models,
1060
+ stats: {
1061
+ totalTables: Object.keys(tablesMap).length,
1062
+ totalAccessPoints: nativeResult.accessPoints.length,
1063
+ totalSensitiveFields: nativeResult.sensitiveFields.length,
1064
+ totalModels: nativeResult.models.length,
1065
+ },
1066
+ },
1067
+ violations: [],
1068
+ stats: {
1069
+ filesScanned: nativeResult.filesScanned,
1070
+ tablesFound: Object.keys(tablesMap).length,
1071
+ accessPointsFound: nativeResult.accessPoints.length,
1072
+ sensitiveFieldsFound: nativeResult.sensitiveFields.length,
1073
+ violationsFound: 0,
1074
+ scanDurationMs: nativeResult.durationMs,
1075
+ },
1076
+ };
1077
+ boundarySpinner.succeed(`Found ${scanBoundaryResult.stats.tablesFound} tables, ` +
1078
+ `${scanBoundaryResult.stats.accessPointsFound} access points (native)`);
1079
+ // Show sensitive field access warnings
1080
+ if (scanBoundaryResult.stats.sensitiveFieldsFound > 0) {
1081
+ console.log();
1082
+ console.log(chalk.bold.yellow(`⚠️ ${scanBoundaryResult.stats.sensitiveFieldsFound} Sensitive Field Access Detected:`));
1083
+ const sensFields = scanBoundaryResult.accessMap.sensitiveFields.slice(0, 5);
1084
+ for (const field of sensFields) {
1085
+ const fieldName = field.table ? `${field.table}.${field.field}` : field.field;
1086
+ console.log(chalk.yellow(` ${fieldName} (${field.sensitivityType}) - ${field.file}:${field.line}`));
1087
+ }
1088
+ if (scanBoundaryResult.accessMap.sensitiveFields.length > 5) {
1089
+ console.log(chalk.gray(` ... and ${scanBoundaryResult.accessMap.sensitiveFields.length - 5} more`));
1090
+ }
1091
+ }
1092
+ console.log();
1093
+ console.log(chalk.gray('View data boundaries:'));
1094
+ console.log(chalk.cyan(' drift boundaries'));
1095
+ console.log(chalk.cyan(' drift boundaries table <name>'));
1072
1096
  }
1073
- if (boundaryResult.accessMap.sensitiveFields.length > 5) {
1074
- console.log(chalk.gray(` ... and ${boundaryResult.accessMap.sensitiveFields.length - 5} more`));
1097
+ catch (nativeError) {
1098
+ if (verbose) {
1099
+ boundarySpinner.text(chalk.gray(`Native boundary scanner failed, using TypeScript fallback`));
1100
+ }
1101
+ // Fall through to TypeScript implementation
1102
+ throw nativeError; // Re-throw to trigger fallback
1075
1103
  }
1076
1104
  }
1077
- // Check violations if rules exist
1078
- if (boundaryResult.stats.violationsFound > 0) {
1079
- console.log();
1080
- console.log(chalk.bold.red(`🚫 ${boundaryResult.stats.violationsFound} Boundary Violations:`));
1081
- for (const violation of boundaryResult.violations.slice(0, 5)) {
1082
- const icon = violation.severity === 'error' ? '🔴' : violation.severity === 'warning' ? '🟡' : '🔵';
1083
- console.log(chalk.red(` ${icon} ${violation.file}:${violation.line} - ${violation.message}`));
1105
+ else {
1106
+ // TypeScript fallback
1107
+ const boundaryScanner = createBoundaryScanner({ rootDir, verbose });
1108
+ await boundaryScanner.initialize();
1109
+ const boundaryResult = await boundaryScanner.scanFiles(files);
1110
+ // Store result for materializer
1111
+ scanBoundaryResult = boundaryResult;
1112
+ boundarySpinner.succeed(`Found ${boundaryResult.stats.tablesFound} tables, ` +
1113
+ `${boundaryResult.stats.accessPointsFound} access points`);
1114
+ // Show sensitive field access warnings
1115
+ if (boundaryResult.stats.sensitiveFieldsFound > 0) {
1116
+ console.log();
1117
+ console.log(chalk.bold.yellow(`⚠️ ${boundaryResult.stats.sensitiveFieldsFound} Sensitive Field Access Detected:`));
1118
+ const sensitiveFields = boundaryResult.accessMap.sensitiveFields.slice(0, 5);
1119
+ for (const field of sensitiveFields) {
1120
+ const fieldName = field.table ? `${field.table}.${field.field}` : field.field;
1121
+ console.log(chalk.yellow(` ${fieldName} (${field.sensitivityType}) - ${field.file}:${field.line}`));
1122
+ }
1123
+ if (boundaryResult.accessMap.sensitiveFields.length > 5) {
1124
+ console.log(chalk.gray(` ... and ${boundaryResult.accessMap.sensitiveFields.length - 5} more`));
1125
+ }
1084
1126
  }
1085
- if (boundaryResult.violations.length > 5) {
1086
- console.log(chalk.gray(` ... and ${boundaryResult.violations.length - 5} more`));
1127
+ // Check violations if rules exist
1128
+ if (boundaryResult.stats.violationsFound > 0) {
1129
+ console.log();
1130
+ console.log(chalk.bold.red(`🚫 ${boundaryResult.stats.violationsFound} Boundary Violations:`));
1131
+ for (const violation of boundaryResult.violations.slice(0, 5)) {
1132
+ const icon = violation.severity === 'error' ? '🔴' : violation.severity === 'warning' ? '🟡' : '🔵';
1133
+ console.log(chalk.red(` ${icon} ${violation.file}:${violation.line} - ${violation.message}`));
1134
+ }
1135
+ if (boundaryResult.violations.length > 5) {
1136
+ console.log(chalk.gray(` ... and ${boundaryResult.violations.length - 5} more`));
1137
+ }
1087
1138
  }
1088
- }
1089
- // Show top accessed tables in verbose mode
1090
- if (verbose && boundaryResult.stats.tablesFound > 0) {
1091
- console.log();
1092
- console.log(chalk.gray(' Top accessed tables:'));
1093
- const tableEntries = Object.entries(boundaryResult.accessMap.tables)
1094
- .map(([name, info]) => ({ name, count: info.accessedBy.length }))
1095
- .sort((a, b) => b.count - a.count)
1096
- .slice(0, 5);
1097
- for (const table of tableEntries) {
1098
- console.log(chalk.gray(` ${table.name}: ${table.count} access points`));
1139
+ // Show top accessed tables in verbose mode
1140
+ if (verbose && boundaryResult.stats.tablesFound > 0) {
1141
+ console.log();
1142
+ console.log(chalk.gray(' Top accessed tables:'));
1143
+ const tableEntries = Object.entries(boundaryResult.accessMap.tables)
1144
+ .map(([name, info]) => ({ name, count: info.accessedBy.length }))
1145
+ .sort((a, b) => b.count - a.count)
1146
+ .slice(0, 5);
1147
+ for (const table of tableEntries) {
1148
+ console.log(chalk.gray(` ${table.name}: ${table.count} access points`));
1149
+ }
1099
1150
  }
1151
+ console.log();
1152
+ console.log(chalk.gray('View data boundaries:'));
1153
+ console.log(chalk.cyan(' drift boundaries'));
1154
+ console.log(chalk.cyan(' drift boundaries table <name>'));
1100
1155
  }
1101
- console.log();
1102
- console.log(chalk.gray('View data boundaries:'));
1103
- console.log(chalk.cyan(' drift boundaries'));
1104
- console.log(chalk.cyan(' drift boundaries table <name>'));
1105
1156
  }
1106
1157
  catch (error) {
1107
- boundarySpinner.fail('Boundary scanning failed');
1108
- if (verbose) {
1109
- console.error(chalk.red(error.message));
1158
+ // If native failed, try TypeScript fallback
1159
+ try {
1160
+ const boundaryScanner = createBoundaryScanner({ rootDir, verbose });
1161
+ await boundaryScanner.initialize();
1162
+ const boundaryResult = await boundaryScanner.scanFiles(files);
1163
+ scanBoundaryResult = boundaryResult;
1164
+ boundarySpinner.succeed(`Found ${boundaryResult.stats.tablesFound} tables, ` +
1165
+ `${boundaryResult.stats.accessPointsFound} access points`);
1166
+ }
1167
+ catch (fallbackError) {
1168
+ boundarySpinner.fail('Boundary scanning failed');
1169
+ if (verbose) {
1170
+ console.error(chalk.red(fallbackError.message));
1171
+ }
1110
1172
  }
1111
1173
  }
1112
1174
  }
@@ -1116,82 +1178,230 @@ async function scanSingleProject(rootDir, options, quiet = false) {
1116
1178
  const testTopologySpinner = createSpinner('Building test topology...');
1117
1179
  testTopologySpinner.start();
1118
1180
  try {
1119
- // Initialize test topology analyzer
1120
- const testAnalyzer = createTestTopologyAnalyzer({});
1121
- // Try to load call graph for transitive analysis
1122
- try {
1123
- const callGraphAnalyzer = createCallGraphAnalyzer({ rootDir });
1124
- await callGraphAnalyzer.initialize();
1125
- const graph = callGraphAnalyzer.getGraph();
1126
- if (graph) {
1127
- testAnalyzer.setCallGraph(graph);
1181
+ // Try native analyzer first (much faster)
1182
+ if (isNativeAvailable()) {
1183
+ try {
1184
+ const nativeResult = await analyzeTestTopologyWithFallback(rootDir, files);
1185
+ // Save results
1186
+ const testTopologyDir = path.join(rootDir, DRIFT_DIR, 'test-topology');
1187
+ await fs.mkdir(testTopologyDir, { recursive: true });
1188
+ // Convert native result to summary format
1189
+ const summary = {
1190
+ testFiles: nativeResult.testFiles.length,
1191
+ testCases: nativeResult.totalTests,
1192
+ coveredFunctions: nativeResult.coverage.length,
1193
+ totalFunctions: nativeResult.coverage.length + nativeResult.uncoveredFiles.length,
1194
+ functionCoveragePercent: nativeResult.coverage.length > 0
1195
+ ? Math.round((nativeResult.coverage.length / (nativeResult.coverage.length + nativeResult.uncoveredFiles.length)) * 100)
1196
+ : 0,
1197
+ coveragePercent: 0,
1198
+ avgQualityScore: 0,
1199
+ byFramework: {},
1200
+ };
1201
+ // Count by framework
1202
+ for (const tf of nativeResult.testFiles) {
1203
+ summary.byFramework[tf.framework] = (summary.byFramework[tf.framework] ?? 0) + tf.testCount;
1204
+ }
1205
+ const mockAnalysis = {
1206
+ totalMocks: nativeResult.testFiles.reduce((sum, tf) => sum + tf.mockCount, 0),
1207
+ externalMocks: 0,
1208
+ internalMocks: 0,
1209
+ externalPercent: 0,
1210
+ internalPercent: 0,
1211
+ avgMockRatio: 0,
1212
+ highMockRatioTests: [],
1213
+ topMockedModules: [],
1214
+ };
1215
+ await fs.writeFile(path.join(testTopologyDir, 'summary.json'), JSON.stringify({ summary, mockAnalysis, generatedAt: new Date().toISOString() }, null, 2));
1216
+ testTopologySpinner.succeed(`Built test topology (native): ${summary.testFiles} test files, ${summary.testCases} tests`);
1217
+ if (verbose) {
1218
+ console.log(chalk.gray(` Native analyzer used for faster analysis`));
1219
+ console.log(chalk.gray(` Files analyzed: ${nativeResult.filesAnalyzed}`));
1220
+ }
1221
+ }
1222
+ catch (nativeError) {
1223
+ // Native failed, fall through to TypeScript implementation
1224
+ if (verbose) {
1225
+ console.log(chalk.gray(` Native analyzer failed, using TypeScript fallback`));
1226
+ }
1227
+ await runTypeScriptTestTopology(rootDir, files, verbose, testTopologySpinner);
1128
1228
  }
1129
1229
  }
1130
- catch {
1131
- // No call graph available, continue with direct analysis
1230
+ else {
1231
+ // Native not available, use TypeScript
1232
+ await runTypeScriptTestTopology(rootDir, files, verbose, testTopologySpinner);
1132
1233
  }
1133
- // Find test files from the already-discovered files
1134
- const testFilePatterns = [
1135
- /\.test\.[jt]sx?$/,
1136
- /\.spec\.[jt]sx?$/,
1137
- /_test\.py$/,
1138
- /test_.*\.py$/,
1139
- /Test\.java$/,
1140
- /Tests\.java$/,
1141
- /Test\.cs$/,
1142
- /Tests\.cs$/,
1143
- /Test\.php$/,
1144
- ];
1145
- const testFiles = files.filter(f => testFilePatterns.some(p => p.test(f)));
1146
- if (testFiles.length === 0) {
1147
- testTopologySpinner.succeed('No test files found');
1234
+ }
1235
+ catch (error) {
1236
+ testTopologySpinner.fail('Test topology build failed');
1237
+ if (verbose) {
1238
+ console.error(chalk.red(error.message));
1148
1239
  }
1149
- else {
1150
- // Extract tests from each file
1151
- let extractedCount = 0;
1152
- for (const testFile of testFiles) {
1153
- try {
1154
- const content = await fs.readFile(path.join(rootDir, testFile), 'utf-8');
1155
- const extraction = testAnalyzer.extractFromFile(content, testFile);
1156
- if (extraction)
1157
- extractedCount++;
1240
+ }
1241
+ }
1242
+ // Constants extraction (opt-in)
1243
+ if (options.constants) {
1244
+ console.log();
1245
+ const constantsSpinner = createSpinner('Extracting constants...');
1246
+ constantsSpinner.start();
1247
+ try {
1248
+ const constantsResult = await analyzeConstantsWithFallback(rootDir, files);
1249
+ // Save to ConstantStore
1250
+ const constantStore = new ConstantStore({ rootDir });
1251
+ await constantStore.initialize();
1252
+ // Group constants by file and save
1253
+ const constantsByFile = new Map();
1254
+ for (const constant of constantsResult.constants) {
1255
+ const existing = constantsByFile.get(constant.file) ?? [];
1256
+ existing.push(constant);
1257
+ constantsByFile.set(constant.file, existing);
1258
+ }
1259
+ // Map native language to ConstantLanguage
1260
+ const mapLanguage = (lang) => {
1261
+ const validLangs = ['typescript', 'javascript', 'python', 'java', 'csharp', 'php', 'go', 'rust', 'cpp'];
1262
+ return validLangs.includes(lang) ? lang : 'typescript';
1263
+ };
1264
+ // Map native kind to ConstantKind
1265
+ const mapKind = (kind) => {
1266
+ const kindMap = {
1267
+ 'const': 'primitive',
1268
+ 'let': 'primitive',
1269
+ 'var': 'primitive',
1270
+ 'readonly': 'primitive',
1271
+ 'static': 'class_constant',
1272
+ 'final': 'class_constant',
1273
+ 'define': 'primitive',
1274
+ 'enum_member': 'enum_member',
1275
+ 'primitive': 'primitive',
1276
+ 'enum': 'enum',
1277
+ 'object': 'object',
1278
+ 'array': 'array',
1279
+ 'computed': 'computed',
1280
+ 'class_constant': 'class_constant',
1281
+ 'interface_constant': 'interface_constant',
1282
+ };
1283
+ return kindMap[kind] ?? 'primitive';
1284
+ };
1285
+ // Map native category to ConstantCategory
1286
+ const mapCategory = (cat) => {
1287
+ const validCats = ['config', 'api', 'status', 'error', 'feature_flag', 'limit', 'regex', 'path', 'env', 'security', 'uncategorized'];
1288
+ return validCats.includes(cat) ? cat : 'uncategorized';
1289
+ };
1290
+ // Save each file's constants
1291
+ for (const [file, fileConstants] of constantsByFile) {
1292
+ const firstLang = fileConstants[0]?.language;
1293
+ await constantStore.saveFileResult({
1294
+ file,
1295
+ language: mapLanguage(firstLang ?? 'typescript'),
1296
+ constants: fileConstants.map(c => ({
1297
+ id: `${file}:${c.line}:${c.name}`,
1298
+ name: c.name,
1299
+ qualifiedName: c.name,
1300
+ file,
1301
+ line: c.line,
1302
+ column: 0,
1303
+ endLine: c.line,
1304
+ language: mapLanguage(c.language),
1305
+ kind: mapKind(c.declarationType ?? 'const'),
1306
+ category: mapCategory(c.category),
1307
+ value: c.value,
1308
+ isExported: c.isExported,
1309
+ decorators: [],
1310
+ modifiers: [],
1311
+ confidence: 0.9,
1312
+ })),
1313
+ enums: [],
1314
+ references: [],
1315
+ errors: [],
1316
+ quality: {
1317
+ method: 'regex',
1318
+ confidence: 0.9,
1319
+ coveragePercent: 100,
1320
+ itemsExtracted: fileConstants.length,
1321
+ parseErrors: 0,
1322
+ warnings: [],
1323
+ usedFallback: !isNativeAvailable(),
1324
+ extractionTimeMs: constantsResult.stats.durationMs,
1325
+ },
1326
+ });
1327
+ }
1328
+ // Rebuild index
1329
+ await constantStore.rebuildIndex();
1330
+ const nativeIndicator = isNativeAvailable() ? ' (native)' : '';
1331
+ constantsSpinner.succeed(`Extracted ${constantsResult.stats.totalConstants} constants from ${constantsResult.stats.filesAnalyzed} files${nativeIndicator}`);
1332
+ // Show secrets warning if any found
1333
+ if (constantsResult.secrets.length > 0) {
1334
+ console.log();
1335
+ console.log(chalk.bold.red(`🔐 ${constantsResult.secrets.length} Potential Hardcoded Secrets Detected!`));
1336
+ const critical = constantsResult.secrets.filter(s => s.severity === 'critical' || s.severity === 'high');
1337
+ if (critical.length > 0) {
1338
+ for (const secret of critical.slice(0, 3)) {
1339
+ console.log(chalk.red(` ${secret.name} (${secret.secretType}) - ${secret.file}:${secret.line}`));
1158
1340
  }
1159
- catch {
1160
- // Skip files that can't be read
1341
+ if (critical.length > 3) {
1342
+ console.log(chalk.gray(` ... and ${critical.length - 3} more`));
1161
1343
  }
1162
1344
  }
1163
- // Build mappings
1164
- testAnalyzer.buildMappings();
1165
- // Get results
1166
- const summary = testAnalyzer.getSummary();
1167
- const mockAnalysis = testAnalyzer.analyzeMocks();
1168
- // Save results
1169
- const testTopologyDir = path.join(rootDir, DRIFT_DIR, 'test-topology');
1170
- await fs.mkdir(testTopologyDir, { recursive: true });
1171
- await fs.writeFile(path.join(testTopologyDir, 'summary.json'), JSON.stringify({ summary, mockAnalysis, generatedAt: new Date().toISOString() }, null, 2));
1172
- testTopologySpinner.succeed(`Built test topology: ${summary.testFiles} test files, ${summary.testCases} tests, ` +
1173
- `${summary.coveredFunctions}/${summary.totalFunctions} functions covered`);
1345
+ console.log();
1346
+ console.log(chalk.yellow("Run 'drift constants secrets' to review all potential secrets"));
1347
+ }
1348
+ if (verbose) {
1349
+ console.log(chalk.gray(` Duration: ${constantsResult.stats.durationMs}ms`));
1350
+ console.log(chalk.gray(` Exported: ${constantsResult.stats.exportedCount}`));
1351
+ }
1352
+ console.log();
1353
+ console.log(chalk.gray('View constants:'));
1354
+ console.log(chalk.cyan(' drift constants'));
1355
+ console.log(chalk.cyan(' drift constants list'));
1356
+ }
1357
+ catch (error) {
1358
+ constantsSpinner.fail('Constants extraction failed');
1359
+ if (verbose) {
1360
+ console.error(chalk.red(error.message));
1361
+ }
1362
+ }
1363
+ }
1364
+ // Call graph building (opt-in) - uses native Rust for memory safety
1365
+ if (options.callgraph) {
1366
+ console.log();
1367
+ const callgraphSpinner = createSpinner('Building call graph...');
1368
+ callgraphSpinner.start();
1369
+ try {
1370
+ if (isNativeAvailable()) {
1371
+ const callgraphConfig = {
1372
+ root: rootDir,
1373
+ patterns: [
1374
+ '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx',
1375
+ '**/*.py', '**/*.cs', '**/*.java', '**/*.php',
1376
+ ],
1377
+ resolutionBatchSize: 50,
1378
+ };
1379
+ const callgraphResult = await buildCallGraph(callgraphConfig);
1380
+ callgraphSpinner.succeed(`Built call graph (native): ${callgraphResult.filesProcessed} files, ` +
1381
+ `${callgraphResult.totalFunctions.toLocaleString()} functions, ` +
1382
+ `${callgraphResult.resolvedCalls.toLocaleString()}/${callgraphResult.totalCalls.toLocaleString()} calls resolved`);
1174
1383
  if (verbose) {
1175
- console.log(chalk.gray(` Test files extracted: ${extractedCount}/${testFiles.length}`));
1176
- console.log(chalk.gray(` Coverage: ${summary.coveragePercent}%`));
1177
- if (mockAnalysis.totalMocks > 0) {
1178
- console.log(chalk.gray(` Mocks: ${mockAnalysis.totalMocks} (${mockAnalysis.externalPercent}% external)`));
1179
- }
1384
+ console.log(chalk.gray(` Entry points: ${callgraphResult.entryPoints}`));
1385
+ console.log(chalk.gray(` Data accessors: ${callgraphResult.dataAccessors}`));
1386
+ console.log(chalk.gray(` Resolution rate: ${Math.round(callgraphResult.resolutionRate * 100)}%`));
1387
+ console.log(chalk.gray(` Duration: ${(callgraphResult.durationMs / 1000).toFixed(2)}s`));
1180
1388
  }
1181
- // Show uncovered functions warning
1182
- if (summary.totalFunctions > 0 && summary.functionCoveragePercent < 50) {
1183
- console.log();
1184
- console.log(chalk.yellow(`⚠️ Low test coverage: ${summary.functionCoveragePercent}% of functions covered`));
1185
- console.log(chalk.gray(' Run `drift test-topology uncovered` to find untested code'));
1389
+ if (callgraphResult.errors.length > 0 && verbose) {
1390
+ console.log(chalk.yellow(` ⚠️ ${callgraphResult.errors.length} files had parse errors`));
1186
1391
  }
1187
1392
  console.log();
1188
- console.log(chalk.gray('View test topology:'));
1189
- console.log(chalk.cyan(' drift test-topology status'));
1190
- console.log(chalk.cyan(' drift test-topology uncovered'));
1393
+ console.log(chalk.gray('Query the call graph:'));
1394
+ console.log(chalk.cyan(' drift callgraph status'));
1395
+ console.log(chalk.cyan(' drift callgraph reach <function>'));
1396
+ console.log(chalk.cyan(' drift callgraph inverse <table>'));
1397
+ }
1398
+ else {
1399
+ callgraphSpinner.fail('Call graph requires native module (not available)');
1400
+ console.log(chalk.gray(' Run `drift callgraph build` for TypeScript fallback (may OOM on large codebases)'));
1191
1401
  }
1192
1402
  }
1193
1403
  catch (error) {
1194
- testTopologySpinner.fail('Test topology build failed');
1404
+ callgraphSpinner.fail('Call graph build failed');
1195
1405
  if (verbose) {
1196
1406
  console.error(chalk.red(error.message));
1197
1407
  }
@@ -1254,6 +1464,8 @@ async function scanSingleProject(rootDir, options, quiet = false) {
1254
1464
  if (stats.byStatus.discovered > 0) {
1255
1465
  const discovered = store.getDiscovered();
1256
1466
  const highConfidence = discovered.filter((p) => p.confidence.level === 'high');
1467
+ // Count auto-approve eligible (≥90% confidence)
1468
+ const autoApproveEligible = discovered.filter((p) => p.confidence.score >= 0.90).length;
1257
1469
  if (highConfidence.length > 0) {
1258
1470
  console.log(chalk.bold('High Confidence Patterns (ready for approval):'));
1259
1471
  console.log();
@@ -1271,12 +1483,107 @@ async function scanSingleProject(rootDir, options, quiet = false) {
1271
1483
  }
1272
1484
  console.log();
1273
1485
  }
1274
- console.log(chalk.gray('To review and approve patterns:'));
1275
- console.log(chalk.cyan(' drift status'));
1276
- console.log(chalk.cyan(' drift approve <pattern-id>'));
1486
+ // Post-scan summary with agent assistance prompt
1487
+ console.log(chalk.bold('📊 Pattern Review'));
1488
+ console.log(chalk.gray(''.repeat(50)));
1489
+ console.log(` Patterns discovered: ${chalk.cyan(stats.byStatus.discovered)}`);
1490
+ console.log(` Auto-approve eligible: ${chalk.green(autoApproveEligible)} (≥90% confidence)`);
1491
+ console.log(` Needs review: ${chalk.yellow(stats.byStatus.discovered - autoApproveEligible)}`);
1492
+ console.log();
1493
+ console.log(chalk.gray('Quick actions:'));
1494
+ if (autoApproveEligible > 0) {
1495
+ console.log(chalk.cyan(` drift approve --auto`) + chalk.gray(` - Auto-approve ${autoApproveEligible} high-confidence patterns`));
1496
+ }
1497
+ console.log(chalk.cyan(` drift audit --review`) + chalk.gray(` - Generate detailed review report`));
1498
+ console.log(chalk.cyan(` drift approve <id>`) + chalk.gray(` - Approve a specific pattern`));
1499
+ console.log();
1500
+ console.log(chalk.gray('For agent assistance, copy this to your AI assistant:'));
1501
+ console.log(chalk.cyan('┌─────────────────────────────────────────────────────────────┐'));
1502
+ console.log(chalk.cyan('│') + ' Run `drift audit --review` and approve high-confidence ' + chalk.cyan('│'));
1503
+ console.log(chalk.cyan('│') + ' patterns that match codebase conventions. Flag any that ' + chalk.cyan('│'));
1504
+ console.log(chalk.cyan('│') + ' look like false positives or duplicates. ' + chalk.cyan('│'));
1505
+ console.log(chalk.cyan('└─────────────────────────────────────────────────────────────┘'));
1277
1506
  }
1278
1507
  console.log();
1279
1508
  }
1509
+ /**
1510
+ * Run TypeScript test topology analyzer (fallback when native is unavailable)
1511
+ */
1512
+ async function runTypeScriptTestTopology(rootDir, files, verbose, spinner) {
1513
+ // Initialize test topology analyzer
1514
+ const testAnalyzer = createTestTopologyAnalyzer({});
1515
+ // Try to load call graph for transitive analysis
1516
+ try {
1517
+ const callGraphAnalyzer = createCallGraphAnalyzer({ rootDir });
1518
+ await callGraphAnalyzer.initialize();
1519
+ const graph = callGraphAnalyzer.getGraph();
1520
+ if (graph) {
1521
+ testAnalyzer.setCallGraph(graph);
1522
+ }
1523
+ }
1524
+ catch {
1525
+ // No call graph available, continue with direct analysis
1526
+ }
1527
+ // Find test files from the already-discovered files
1528
+ const testFilePatterns = [
1529
+ /\.test\.[jt]sx?$/,
1530
+ /\.spec\.[jt]sx?$/,
1531
+ /_test\.py$/,
1532
+ /test_.*\.py$/,
1533
+ /Test\.java$/,
1534
+ /Tests\.java$/,
1535
+ /Test\.cs$/,
1536
+ /Tests\.cs$/,
1537
+ /Test\.php$/,
1538
+ ];
1539
+ const testFiles = files.filter(f => testFilePatterns.some(p => p.test(f)));
1540
+ if (testFiles.length === 0) {
1541
+ spinner.succeed('No test files found');
1542
+ }
1543
+ else {
1544
+ // Extract tests from each file
1545
+ let extractedCount = 0;
1546
+ for (const testFile of testFiles) {
1547
+ try {
1548
+ const content = await fs.readFile(path.join(rootDir, testFile), 'utf-8');
1549
+ const extraction = testAnalyzer.extractFromFile(content, testFile);
1550
+ if (extraction)
1551
+ extractedCount++;
1552
+ }
1553
+ catch {
1554
+ // Skip files that can't be read
1555
+ }
1556
+ }
1557
+ // Build mappings
1558
+ testAnalyzer.buildMappings();
1559
+ // Get results
1560
+ const summary = testAnalyzer.getSummary();
1561
+ const mockAnalysis = testAnalyzer.analyzeMocks();
1562
+ // Save results
1563
+ const testTopologyDir = path.join(rootDir, DRIFT_DIR, 'test-topology');
1564
+ await fs.mkdir(testTopologyDir, { recursive: true });
1565
+ await fs.writeFile(path.join(testTopologyDir, 'summary.json'), JSON.stringify({ summary, mockAnalysis, generatedAt: new Date().toISOString() }, null, 2));
1566
+ spinner.succeed(`Built test topology: ${summary.testFiles} test files, ${summary.testCases} tests, ` +
1567
+ `${summary.coveredFunctions}/${summary.totalFunctions} functions covered`);
1568
+ if (verbose) {
1569
+ console.log(chalk.gray(` Test files extracted: ${extractedCount}/${testFiles.length}`));
1570
+ console.log(chalk.gray(` Coverage: ${summary.coveragePercent}%`));
1571
+ if (mockAnalysis.totalMocks > 0) {
1572
+ console.log(chalk.gray(` Mocks: ${mockAnalysis.totalMocks} (${mockAnalysis.externalPercent}% external)`));
1573
+ }
1574
+ }
1575
+ // Show uncovered functions warning
1576
+ if (summary.totalFunctions > 0 && summary.functionCoveragePercent < 50) {
1577
+ console.log();
1578
+ console.log(chalk.yellow(`⚠️ Low test coverage: ${summary.functionCoveragePercent}% of functions covered`));
1579
+ console.log(chalk.gray(' Run `drift test-topology uncovered` to find untested code'));
1580
+ }
1581
+ console.log();
1582
+ console.log(chalk.gray('View test topology:'));
1583
+ console.log(chalk.cyan(' drift test-topology status'));
1584
+ console.log(chalk.cyan(' drift test-topology uncovered'));
1585
+ }
1586
+ }
1280
1587
  export const scanCommand = new Command('scan')
1281
1588
  .description('Scan codebase for patterns using enterprise detectors')
1282
1589
  .argument('[paths...]', 'Paths to scan (defaults to current directory)')
@@ -1289,6 +1596,8 @@ export const scanCommand = new Command('scan')
1289
1596
  .option('--no-contracts', 'Skip BE↔FE contract scanning')
1290
1597
  .option('--no-boundaries', 'Skip data boundary scanning')
1291
1598
  .option('--test-topology', 'Build test topology (test-to-code mappings)')
1599
+ .option('--constants', 'Extract constants, enums, and detect hardcoded secrets')
1600
+ .option('--callgraph', 'Build call graph for reachability analysis (native Rust)')
1292
1601
  .option('-p, --project <name>', 'Scan a specific registered project by name')
1293
1602
  .option('--all-projects', 'Scan all registered projects')
1294
1603
  .option('-t, --timeout <seconds>', 'Scan timeout in seconds (default: 300)', '300')