@codebakers/cli 3.9.31 → 3.9.33

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.
package/src/mcp/server.ts CHANGED
@@ -1464,6 +1464,29 @@ class CodeBakersServer {
1464
1464
  properties: {},
1465
1465
  },
1466
1466
  },
1467
+ {
1468
+ name: 'coherence_audit',
1469
+ description:
1470
+ 'Full codebase coherence audit. Checks all imports/exports, type flows, schema dependencies, API contracts, env vars, circular dependencies, and dead code. Use this for the /coherence command or when user asks to check wiring/dependencies.',
1471
+ inputSchema: {
1472
+ type: 'object' as const,
1473
+ properties: {
1474
+ focus: {
1475
+ type: 'string',
1476
+ enum: ['all', 'imports', 'types', 'schema', 'api', 'env', 'circular', 'dead-code'],
1477
+ description: 'Focus area for the audit (default: all)',
1478
+ },
1479
+ autoFix: {
1480
+ type: 'boolean',
1481
+ description: 'Automatically fix issues that can be auto-fixed (default: false)',
1482
+ },
1483
+ includeNodeModules: {
1484
+ type: 'boolean',
1485
+ description: 'Include node_modules in analysis (default: false, very slow)',
1486
+ },
1487
+ },
1488
+ },
1489
+ },
1467
1490
  // ============================================
1468
1491
  // PROJECT TRACKING - Server-Side Dashboard
1469
1492
  // ============================================
@@ -1808,6 +1831,9 @@ class CodeBakersServer {
1808
1831
  case 'guardian_status':
1809
1832
  return this.handleGuardianStatus();
1810
1833
 
1834
+ case 'coherence_audit':
1835
+ return this.handleCoherenceAudit(args as { focus?: string; autoFix?: boolean; includeNodeModules?: boolean });
1836
+
1811
1837
  // Project Tracking - Server-Side Dashboard
1812
1838
  case 'project_sync':
1813
1839
  return this.handleProjectSync(args as {
@@ -2808,10 +2834,10 @@ Or if user declines, call without fullDeploy:
2808
2834
  // Use default
2809
2835
  }
2810
2836
 
2811
- results.push(`# 🎨 Adding CodeBakers v6.16 to: ${projectName}\n`);
2837
+ results.push(`# 🎨 Adding CodeBakers v6.17 to: ${projectName}\n`);
2812
2838
 
2813
- // v6.16 bootstrap content - magic phrase + rules at START and END
2814
- const V6_CLAUDE_MD = `# CodeBakers v6.16
2839
+ // v6.17 bootstrap content - magic phrase + rules at START and END + coherence
2840
+ const V6_CLAUDE_MD = `# CodeBakers v6.17
2815
2841
 
2816
2842
  ## 🪄 MAGIC PHRASE: "codebakers go"
2817
2843
  When user says "codebakers go" in chat, start the onboarding conversation:
@@ -2838,8 +2864,11 @@ When user says "codebakers go" in chat, start the onboarding conversation:
2838
2864
  project_status() → Verify connection FIRST
2839
2865
  discover_patterns({ task: "what you're building" }) → Get patterns BEFORE code
2840
2866
  validate_complete({ feature: "name", files: [...] }) → Validate BEFORE done
2867
+ coherence_audit() → Check wiring & dependencies
2841
2868
  \`\`\`
2842
2869
 
2870
+ Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
2871
+
2843
2872
  Header (after project_status succeeds): 🍪 CodeBakers is working on this...
2844
2873
  Header (if project_status fails): ⚠️ CodeBakers not connected
2845
2874
  Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
@@ -2851,7 +2880,7 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
2851
2880
  4. Show footer after code responses
2852
2881
  `;
2853
2882
 
2854
- const V6_CURSORRULES = `# CodeBakers v6.16
2883
+ const V6_CURSORRULES = `# CodeBakers v6.17
2855
2884
 
2856
2885
  ## 🪄 "codebakers go" = Start onboarding conversation
2857
2886
  Ask existing/new → Ask what to build → Call init_project() → Help them build
@@ -2868,6 +2897,9 @@ Ask existing/new → Ask what to build → Call init_project() → Help them bui
2868
2897
  3. Show header without project_status succeeding
2869
2898
  4. Skip writing tests for new features
2870
2899
 
2900
+ Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
2901
+ Use coherence_audit() to check wiring & dependencies
2902
+
2871
2903
  ## 🚨 ALWAYS (Repeated at End)
2872
2904
  1. project_status() FIRST
2873
2905
  2. discover_patterns() before code
@@ -2878,8 +2910,8 @@ Ask existing/new → Ask what to build → Call init_project() → Help them bui
2878
2910
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');
2879
2911
  if (fs.existsSync(claudeMdPath)) {
2880
2912
  const content = fs.readFileSync(claudeMdPath, 'utf-8');
2881
- if (content.includes('v6.16') && content.includes('discover_patterns')) {
2882
- results.push('✓ CodeBakers v6.16 already installed\n');
2913
+ if ((content.includes('v6.16') || content.includes('v6.17')) && content.includes('discover_patterns')) {
2914
+ results.push('✓ CodeBakers v6.17 already installed\n');
2883
2915
  results.push('Patterns are server-enforced. Just call `discover_patterns` before coding!');
2884
2916
  return {
2885
2917
  content: [{ type: 'text' as const, text: results.join('\n') }],
@@ -2941,7 +2973,7 @@ Ask existing/new → Ask what to build → Call init_project() → Help them bui
2941
2973
  fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
2942
2974
 
2943
2975
  results.push('\n---\n');
2944
- results.push('## ✅ CodeBakers v6.16 Installed!\n');
2976
+ results.push('## ✅ CodeBakers v6.17 Installed!\n');
2945
2977
  results.push('**How it works now:**');
2946
2978
  results.push('1. Call `discover_patterns` before writing code');
2947
2979
  results.push('2. Server returns all patterns and rules');
@@ -7486,7 +7518,7 @@ ${handlers.join('\n')}
7486
7518
  }
7487
7519
 
7488
7520
  /**
7489
- * Update to CodeBakers v6.16 - server-enforced patterns with magic phrase
7521
+ * Update to CodeBakers v6.17 - server-enforced patterns with magic phrase + coherence
7490
7522
  * This is the MCP equivalent of the `codebakers upgrade` CLI command
7491
7523
  */
7492
7524
  private async handleUpdatePatterns(args: { force?: boolean }) {
@@ -7496,10 +7528,10 @@ ${handlers.join('\n')}
7496
7528
  const claudeDir = path.join(cwd, '.claude');
7497
7529
  const codebakersJson = path.join(cwd, '.codebakers.json');
7498
7530
 
7499
- let response = `# 🔄 CodeBakers v6.16 Update\n\n`;
7531
+ let response = `# 🔄 CodeBakers v6.17 Update\n\n`;
7500
7532
 
7501
- // v6.16 bootstrap content - magic phrase + rules at START and END
7502
- const V6_CLAUDE_MD = `# CodeBakers v6.16
7533
+ // v6.17 bootstrap content - magic phrase + rules at START and END + coherence
7534
+ const V6_CLAUDE_MD = `# CodeBakers v6.17
7503
7535
 
7504
7536
  ## 🪄 MAGIC PHRASE: "codebakers go"
7505
7537
  When user says "codebakers go" in chat, start the onboarding conversation:
@@ -7526,8 +7558,11 @@ When user says "codebakers go" in chat, start the onboarding conversation:
7526
7558
  project_status() → Verify connection FIRST
7527
7559
  discover_patterns({ task: "what you're building" }) → Get patterns BEFORE code
7528
7560
  validate_complete({ feature: "name", files: [...] }) → Validate BEFORE done
7561
+ coherence_audit() → Check wiring & dependencies
7529
7562
  \`\`\`
7530
7563
 
7564
+ Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
7565
+
7531
7566
  Header (after project_status succeeds): 🍪 CodeBakers is working on this...
7532
7567
  Header (if project_status fails): ⚠️ CodeBakers not connected
7533
7568
  Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
@@ -7539,7 +7574,7 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
7539
7574
  4. Show footer after code responses
7540
7575
  `;
7541
7576
 
7542
- const V6_CURSORRULES = `# CodeBakers v6.16
7577
+ const V6_CURSORRULES = `# CodeBakers v6.17
7543
7578
 
7544
7579
  ## 🪄 "codebakers go" = Start onboarding conversation
7545
7580
  Ask existing/new → Ask what to build → Call init_project() → Help them build
@@ -7556,6 +7591,9 @@ Ask existing/new → Ask what to build → Call init_project() → Help them bui
7556
7591
  3. Show header without project_status succeeding
7557
7592
  4. Skip writing tests for new features
7558
7593
 
7594
+ Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
7595
+ Use coherence_audit() to check wiring & dependencies
7596
+
7559
7597
  ## 🚨 ALWAYS (Repeated at End)
7560
7598
  1. project_status() FIRST
7561
7599
  2. discover_patterns() before code
@@ -8935,6 +8973,724 @@ ${events.includes('call-started') ? ` case 'call-started':
8935
8973
  return { content: [{ type: 'text' as const, text: response }] };
8936
8974
  }
8937
8975
 
8976
+ // ============================================================================
8977
+ // COHERENCE AUDIT - Full Codebase Wiring Check
8978
+ // ============================================================================
8979
+
8980
+ /**
8981
+ * Full coherence audit - checks all wiring, dependencies, and connections
8982
+ */
8983
+ private handleCoherenceAudit(args: { focus?: string; autoFix?: boolean; includeNodeModules?: boolean }) {
8984
+ const { focus = 'all', autoFix = false } = args;
8985
+ const cwd = process.cwd();
8986
+
8987
+ interface CoherenceIssue {
8988
+ category: 'import' | 'export' | 'type' | 'schema' | 'api' | 'env' | 'circular' | 'dead-code';
8989
+ severity: 'error' | 'warning' | 'info';
8990
+ file: string;
8991
+ line?: number;
8992
+ message: string;
8993
+ fix?: string;
8994
+ autoFixable: boolean;
8995
+ }
8996
+
8997
+ const issues: CoherenceIssue[] = [];
8998
+ const stats = {
8999
+ filesScanned: 0,
9000
+ importsChecked: 0,
9001
+ exportsFound: 0,
9002
+ typesAnalyzed: 0,
9003
+ envVarsFound: 0,
9004
+ };
9005
+
9006
+ // Helper to extract imports from a file
9007
+ const extractImports = (content: string): Array<{ path: string; names: string[]; line: number; isTypeOnly: boolean }> => {
9008
+ const imports: Array<{ path: string; names: string[]; line: number; isTypeOnly: boolean }> = [];
9009
+ const lines = content.split('\n');
9010
+
9011
+ lines.forEach((line, i) => {
9012
+ // Match: import { X, Y } from 'path'
9013
+ const namedMatch = line.match(/import\s+(type\s+)?{([^}]+)}\s+from\s+['"]([^'"]+)['"]/);
9014
+ if (namedMatch) {
9015
+ const isTypeOnly = !!namedMatch[1];
9016
+ const names = namedMatch[2].split(',').map(n => n.trim().split(' as ')[0].trim()).filter(Boolean);
9017
+ imports.push({ path: namedMatch[3], names, line: i + 1, isTypeOnly });
9018
+ }
9019
+
9020
+ // Match: import X from 'path'
9021
+ const defaultMatch = line.match(/import\s+(type\s+)?(\w+)\s+from\s+['"]([^'"]+)['"]/);
9022
+ if (defaultMatch && !namedMatch) {
9023
+ imports.push({ path: defaultMatch[3], names: ['default'], line: i + 1, isTypeOnly: !!defaultMatch[1] });
9024
+ }
9025
+
9026
+ // Match: import * as X from 'path'
9027
+ const namespaceMatch = line.match(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/);
9028
+ if (namespaceMatch) {
9029
+ imports.push({ path: namespaceMatch[2], names: ['*'], line: i + 1, isTypeOnly: false });
9030
+ }
9031
+ });
9032
+
9033
+ return imports;
9034
+ };
9035
+
9036
+ // Helper to extract exports from a file
9037
+ const extractExports = (content: string): string[] => {
9038
+ const exports: string[] = [];
9039
+
9040
+ // Named exports: export { X, Y }
9041
+ const namedExportMatches = content.matchAll(/export\s+{([^}]+)}/g);
9042
+ for (const match of namedExportMatches) {
9043
+ const names = match[1].split(',').map(n => n.trim().split(' as ').pop()?.trim() || '').filter(Boolean);
9044
+ exports.push(...names);
9045
+ }
9046
+
9047
+ // Direct exports: export const/function/class/type/interface X
9048
+ const directExportMatches = content.matchAll(/export\s+(const|let|var|function|class|type|interface|enum)\s+(\w+)/g);
9049
+ for (const match of directExportMatches) {
9050
+ exports.push(match[2]);
9051
+ }
9052
+
9053
+ // Default export
9054
+ if (content.includes('export default')) {
9055
+ exports.push('default');
9056
+ }
9057
+
9058
+ // Re-exports: export * from, export { X } from
9059
+ const reExportMatches = content.matchAll(/export\s+(?:\*|{[^}]+})\s+from\s+['"]([^'"]+)['"]/g);
9060
+ for (const match of reExportMatches) {
9061
+ exports.push(`__reexport:${match[1]}`);
9062
+ }
9063
+
9064
+ return exports;
9065
+ };
9066
+
9067
+ // Helper to resolve import path to file
9068
+ const resolveImportPath = (importPath: string, fromFile: string): string | null => {
9069
+ // Skip external packages
9070
+ if (!importPath.startsWith('.') && !importPath.startsWith('@/') && !importPath.startsWith('~/')) {
9071
+ return null;
9072
+ }
9073
+
9074
+ // Handle @ alias (common Next.js pattern)
9075
+ let resolvedPath = importPath;
9076
+ if (importPath.startsWith('@/')) {
9077
+ resolvedPath = path.join(cwd, 'src', importPath.slice(2));
9078
+ } else if (importPath.startsWith('~/')) {
9079
+ resolvedPath = path.join(cwd, importPath.slice(2));
9080
+ } else {
9081
+ resolvedPath = path.resolve(path.dirname(fromFile), importPath);
9082
+ }
9083
+
9084
+ // Try different extensions
9085
+ const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];
9086
+ for (const ext of extensions) {
9087
+ const fullPath = resolvedPath + ext;
9088
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
9089
+ return fullPath;
9090
+ }
9091
+ }
9092
+
9093
+ return null;
9094
+ };
9095
+
9096
+ // Build export map for all files
9097
+ const exportMap: Map<string, string[]> = new Map();
9098
+ const importGraph: Map<string, string[]> = new Map(); // file -> files it imports
9099
+ const usedExports: Set<string> = new Set(); // track which exports are actually used
9100
+
9101
+ const searchDirs = ['src', 'app', 'lib', 'components', 'services', 'types', 'utils', 'hooks', 'pages'];
9102
+ const extensions = ['.ts', '.tsx', '.js', '.jsx'];
9103
+
9104
+ // First pass: collect all exports
9105
+ const collectExports = (dir: string) => {
9106
+ if (!fs.existsSync(dir)) return;
9107
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
9108
+
9109
+ for (const entry of entries) {
9110
+ const fullPath = path.join(dir, entry.name);
9111
+
9112
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
9113
+ collectExports(fullPath);
9114
+ } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
9115
+ try {
9116
+ const content = fs.readFileSync(fullPath, 'utf-8');
9117
+ const exports = extractExports(content);
9118
+ exportMap.set(fullPath, exports);
9119
+ stats.exportsFound += exports.length;
9120
+ stats.filesScanned++;
9121
+ } catch {
9122
+ // Skip unreadable files
9123
+ }
9124
+ }
9125
+ }
9126
+ };
9127
+
9128
+ // Collect exports from all directories
9129
+ for (const dir of searchDirs) {
9130
+ collectExports(path.join(cwd, dir));
9131
+ }
9132
+
9133
+ // Also check root-level files
9134
+ try {
9135
+ const rootEntries = fs.readdirSync(cwd, { withFileTypes: true });
9136
+ for (const entry of rootEntries) {
9137
+ if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
9138
+ const fullPath = path.join(cwd, entry.name);
9139
+ try {
9140
+ const content = fs.readFileSync(fullPath, 'utf-8');
9141
+ const exports = extractExports(content);
9142
+ exportMap.set(fullPath, exports);
9143
+ } catch {
9144
+ // Skip
9145
+ }
9146
+ }
9147
+ }
9148
+ } catch {
9149
+ // Skip if can't read root
9150
+ }
9151
+
9152
+ // Second pass: check imports and build graph
9153
+ const checkImports = (dir: string) => {
9154
+ if (!fs.existsSync(dir)) return;
9155
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
9156
+
9157
+ for (const entry of entries) {
9158
+ const fullPath = path.join(dir, entry.name);
9159
+
9160
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
9161
+ checkImports(fullPath);
9162
+ } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
9163
+ try {
9164
+ const content = fs.readFileSync(fullPath, 'utf-8');
9165
+ const imports = extractImports(content);
9166
+ const relativePath = path.relative(cwd, fullPath);
9167
+ const importedFiles: string[] = [];
9168
+
9169
+ for (const imp of imports) {
9170
+ stats.importsChecked++;
9171
+ const resolvedPath = resolveImportPath(imp.path, fullPath);
9172
+
9173
+ if (resolvedPath === null) {
9174
+ // External package or unresolvable - skip
9175
+ continue;
9176
+ }
9177
+
9178
+ importedFiles.push(resolvedPath);
9179
+
9180
+ // Check if file exists
9181
+ if (!fs.existsSync(resolvedPath)) {
9182
+ if (focus === 'all' || focus === 'imports') {
9183
+ issues.push({
9184
+ category: 'import',
9185
+ severity: 'error',
9186
+ file: relativePath,
9187
+ line: imp.line,
9188
+ message: `Import target not found: '${imp.path}'`,
9189
+ fix: `Create the file or update the import path`,
9190
+ autoFixable: false,
9191
+ });
9192
+ }
9193
+ continue;
9194
+ }
9195
+
9196
+ // Check if named imports exist in the target
9197
+ const targetExports = exportMap.get(resolvedPath) || [];
9198
+
9199
+ for (const name of imp.names) {
9200
+ if (name === '*' || name === 'default') {
9201
+ if (name === 'default' && !targetExports.includes('default')) {
9202
+ if (focus === 'all' || focus === 'imports') {
9203
+ issues.push({
9204
+ category: 'import',
9205
+ severity: 'error',
9206
+ file: relativePath,
9207
+ line: imp.line,
9208
+ message: `No default export in '${imp.path}'`,
9209
+ fix: `Add 'export default' or change to named import`,
9210
+ autoFixable: false,
9211
+ });
9212
+ }
9213
+ }
9214
+ continue;
9215
+ }
9216
+
9217
+ // Track that this export is used
9218
+ usedExports.add(`${resolvedPath}:${name}`);
9219
+
9220
+ if (!targetExports.includes(name)) {
9221
+ if (focus === 'all' || focus === 'imports') {
9222
+ issues.push({
9223
+ category: 'export',
9224
+ severity: 'error',
9225
+ file: relativePath,
9226
+ line: imp.line,
9227
+ message: `'${name}' is not exported from '${imp.path}'`,
9228
+ fix: `Add 'export { ${name} }' to ${path.basename(resolvedPath)} or update import`,
9229
+ autoFixable: false,
9230
+ });
9231
+ }
9232
+ }
9233
+ }
9234
+ }
9235
+
9236
+ importGraph.set(fullPath, importedFiles);
9237
+ } catch {
9238
+ // Skip unreadable files
9239
+ }
9240
+ }
9241
+ }
9242
+ };
9243
+
9244
+ // Run import checks
9245
+ for (const dir of searchDirs) {
9246
+ checkImports(path.join(cwd, dir));
9247
+ }
9248
+
9249
+ // Check for circular dependencies
9250
+ if (focus === 'all' || focus === 'circular') {
9251
+ const visited = new Set<string>();
9252
+ const recursionStack = new Set<string>();
9253
+ const circularPaths: string[][] = [];
9254
+
9255
+ const detectCircular = (file: string, pathStack: string[]): boolean => {
9256
+ if (recursionStack.has(file)) {
9257
+ // Found circular dependency
9258
+ const cycleStart = pathStack.indexOf(file);
9259
+ if (cycleStart !== -1) {
9260
+ circularPaths.push(pathStack.slice(cycleStart).concat(file));
9261
+ }
9262
+ return true;
9263
+ }
9264
+
9265
+ if (visited.has(file)) return false;
9266
+
9267
+ visited.add(file);
9268
+ recursionStack.add(file);
9269
+
9270
+ const imports = importGraph.get(file) || [];
9271
+ for (const imported of imports) {
9272
+ detectCircular(imported, [...pathStack, file]);
9273
+ }
9274
+
9275
+ recursionStack.delete(file);
9276
+ return false;
9277
+ };
9278
+
9279
+ for (const file of importGraph.keys()) {
9280
+ detectCircular(file, []);
9281
+ }
9282
+
9283
+ // Add unique circular dependencies as issues
9284
+ const seenCycles = new Set<string>();
9285
+ for (const cycle of circularPaths) {
9286
+ const cycleKey = cycle.map(f => path.relative(cwd, f)).sort().join(' -> ');
9287
+ if (!seenCycles.has(cycleKey)) {
9288
+ seenCycles.add(cycleKey);
9289
+ issues.push({
9290
+ category: 'circular',
9291
+ severity: 'warning',
9292
+ file: path.relative(cwd, cycle[0]),
9293
+ message: `Circular dependency: ${cycle.map(f => path.basename(f)).join(' → ')}`,
9294
+ fix: 'Break the cycle by extracting shared code to a separate module',
9295
+ autoFixable: false,
9296
+ });
9297
+ }
9298
+ }
9299
+ }
9300
+
9301
+ // Check for dead code (unused exports)
9302
+ if (focus === 'all' || focus === 'dead-code') {
9303
+ for (const [file, exports] of exportMap.entries()) {
9304
+ const relativePath = path.relative(cwd, file);
9305
+
9306
+ // Skip entry points and config files
9307
+ if (
9308
+ relativePath.includes('page.tsx') ||
9309
+ relativePath.includes('layout.tsx') ||
9310
+ relativePath.includes('route.ts') ||
9311
+ relativePath.includes('middleware.ts') ||
9312
+ relativePath.endsWith('.config.ts') ||
9313
+ relativePath.endsWith('.config.js')
9314
+ ) {
9315
+ continue;
9316
+ }
9317
+
9318
+ for (const exp of exports) {
9319
+ if (exp.startsWith('__reexport:') || exp === 'default') continue;
9320
+
9321
+ const exportKey = `${file}:${exp}`;
9322
+ if (!usedExports.has(exportKey)) {
9323
+ issues.push({
9324
+ category: 'dead-code',
9325
+ severity: 'info',
9326
+ file: relativePath,
9327
+ message: `Export '${exp}' is never imported`,
9328
+ fix: `Remove if unused, or verify it's used externally`,
9329
+ autoFixable: true,
9330
+ });
9331
+ }
9332
+ }
9333
+ }
9334
+ }
9335
+
9336
+ // Check environment variables
9337
+ if (focus === 'all' || focus === 'env') {
9338
+ const envVarsUsed = new Set<string>();
9339
+ const envVarsDefined = new Set<string>();
9340
+
9341
+ // Scan for process.env usage
9342
+ const scanEnvUsage = (dir: string) => {
9343
+ if (!fs.existsSync(dir)) return;
9344
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
9345
+
9346
+ for (const entry of entries) {
9347
+ const fullPath = path.join(dir, entry.name);
9348
+
9349
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
9350
+ scanEnvUsage(fullPath);
9351
+ } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
9352
+ try {
9353
+ const content = fs.readFileSync(fullPath, 'utf-8');
9354
+ const envMatches = content.matchAll(/process\.env\.(\w+)|process\.env\[['"](\w+)['"]\]/g);
9355
+ for (const match of envMatches) {
9356
+ const varName = match[1] || match[2];
9357
+ envVarsUsed.add(varName);
9358
+ stats.envVarsFound++;
9359
+ }
9360
+ } catch {
9361
+ // Skip
9362
+ }
9363
+ }
9364
+ }
9365
+ };
9366
+
9367
+ for (const dir of searchDirs) {
9368
+ scanEnvUsage(path.join(cwd, dir));
9369
+ }
9370
+
9371
+ // Check .env.example and .env.local
9372
+ const envFiles = ['.env', '.env.local', '.env.example', '.env.development'];
9373
+ for (const envFile of envFiles) {
9374
+ const envPath = path.join(cwd, envFile);
9375
+ if (fs.existsSync(envPath)) {
9376
+ try {
9377
+ const content = fs.readFileSync(envPath, 'utf-8');
9378
+ const lines = content.split('\n');
9379
+ for (const line of lines) {
9380
+ const match = line.match(/^([A-Z][A-Z0-9_]*)=/);
9381
+ if (match) {
9382
+ envVarsDefined.add(match[1]);
9383
+ }
9384
+ }
9385
+ } catch {
9386
+ // Skip
9387
+ }
9388
+ }
9389
+ }
9390
+
9391
+ // Find env vars used but not defined
9392
+ for (const varName of envVarsUsed) {
9393
+ // Skip common Next.js vars
9394
+ if (varName.startsWith('NEXT_') || varName === 'NODE_ENV') continue;
9395
+
9396
+ if (!envVarsDefined.has(varName)) {
9397
+ issues.push({
9398
+ category: 'env',
9399
+ severity: 'warning',
9400
+ file: '.env.example',
9401
+ message: `Environment variable '${varName}' is used but not defined in .env files`,
9402
+ fix: `Add ${varName}= to .env.example`,
9403
+ autoFixable: true,
9404
+ });
9405
+ }
9406
+ }
9407
+
9408
+ // Find env vars defined but not used
9409
+ for (const varName of envVarsDefined) {
9410
+ if (!envVarsUsed.has(varName) && !varName.startsWith('NEXT_')) {
9411
+ issues.push({
9412
+ category: 'env',
9413
+ severity: 'info',
9414
+ file: '.env',
9415
+ message: `Environment variable '${varName}' is defined but never used`,
9416
+ fix: 'Remove if no longer needed',
9417
+ autoFixable: false,
9418
+ });
9419
+ }
9420
+ }
9421
+ }
9422
+
9423
+ // Check for Drizzle schema issues
9424
+ if (focus === 'all' || focus === 'schema') {
9425
+ const schemaPath = path.join(cwd, 'src', 'db', 'schema.ts');
9426
+ const altSchemaPath = path.join(cwd, 'drizzle', 'schema.ts');
9427
+ const schemaFile = fs.existsSync(schemaPath) ? schemaPath : fs.existsSync(altSchemaPath) ? altSchemaPath : null;
9428
+
9429
+ if (schemaFile) {
9430
+ try {
9431
+ const schemaContent = fs.readFileSync(schemaFile, 'utf-8');
9432
+
9433
+ // Extract table names
9434
+ const tableMatches = schemaContent.matchAll(/export\s+const\s+(\w+)\s*=\s*(?:pgTable|mysqlTable|sqliteTable)\s*\(/g);
9435
+ const tableNames = new Set<string>();
9436
+ for (const match of tableMatches) {
9437
+ tableNames.add(match[1]);
9438
+ }
9439
+
9440
+ // Scan for .insert(), .update(), .delete() calls on non-existent tables
9441
+ const scanSchemaUsage = (dir: string) => {
9442
+ if (!fs.existsSync(dir)) return;
9443
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
9444
+
9445
+ for (const entry of entries) {
9446
+ const fullPath = path.join(dir, entry.name);
9447
+
9448
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
9449
+ scanSchemaUsage(fullPath);
9450
+ } else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
9451
+ try {
9452
+ const content = fs.readFileSync(fullPath, 'utf-8');
9453
+ const relativePath = path.relative(cwd, fullPath);
9454
+
9455
+ // Check for db operations on tables
9456
+ const opMatches = content.matchAll(/db\.(insert|update|delete|select)\((\w+)\)/g);
9457
+ for (const match of opMatches) {
9458
+ const tableName = match[2];
9459
+ if (!tableNames.has(tableName) && !tableName.startsWith('sql')) {
9460
+ issues.push({
9461
+ category: 'schema',
9462
+ severity: 'error',
9463
+ file: relativePath,
9464
+ message: `Database operation on unknown table '${tableName}'`,
9465
+ fix: `Verify table exists in schema or fix the table name`,
9466
+ autoFixable: false,
9467
+ });
9468
+ }
9469
+ }
9470
+ } catch {
9471
+ // Skip
9472
+ }
9473
+ }
9474
+ }
9475
+ };
9476
+
9477
+ for (const dir of searchDirs) {
9478
+ scanSchemaUsage(path.join(cwd, dir));
9479
+ }
9480
+ } catch {
9481
+ // Skip if can't read schema
9482
+ }
9483
+ }
9484
+ }
9485
+
9486
+ // Check API contracts (Next.js API routes vs fetch calls)
9487
+ if (focus === 'all' || focus === 'api') {
9488
+ const apiRoutes = new Map<string, { methods: string[]; file: string }>();
9489
+
9490
+ // Find all API routes
9491
+ const apiDir = path.join(cwd, 'src', 'app', 'api');
9492
+ const altApiDir = path.join(cwd, 'app', 'api');
9493
+ const pagesApiDir = path.join(cwd, 'pages', 'api');
9494
+
9495
+ const scanApiRoutes = (dir: string, prefix: string = '/api') => {
9496
+ if (!fs.existsSync(dir)) return;
9497
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
9498
+
9499
+ for (const entry of entries) {
9500
+ const fullPath = path.join(dir, entry.name);
9501
+
9502
+ if (entry.isDirectory()) {
9503
+ scanApiRoutes(fullPath, `${prefix}/${entry.name}`);
9504
+ } else if (entry.name === 'route.ts' || entry.name === 'route.js') {
9505
+ try {
9506
+ const content = fs.readFileSync(fullPath, 'utf-8');
9507
+ const methods: string[] = [];
9508
+
9509
+ if (content.includes('export async function GET') || content.includes('export function GET')) methods.push('GET');
9510
+ if (content.includes('export async function POST') || content.includes('export function POST')) methods.push('POST');
9511
+ if (content.includes('export async function PUT') || content.includes('export function PUT')) methods.push('PUT');
9512
+ if (content.includes('export async function DELETE') || content.includes('export function DELETE')) methods.push('DELETE');
9513
+ if (content.includes('export async function PATCH') || content.includes('export function PATCH')) methods.push('PATCH');
9514
+
9515
+ apiRoutes.set(prefix, { methods, file: path.relative(cwd, fullPath) });
9516
+ } catch {
9517
+ // Skip
9518
+ }
9519
+ }
9520
+ }
9521
+ };
9522
+
9523
+ scanApiRoutes(apiDir);
9524
+ scanApiRoutes(altApiDir);
9525
+
9526
+ // Scan for fetch calls to API routes
9527
+ const scanFetchCalls = (dir: string) => {
9528
+ if (!fs.existsSync(dir)) return;
9529
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
9530
+
9531
+ for (const entry of entries) {
9532
+ const fullPath = path.join(dir, entry.name);
9533
+
9534
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'api') {
9535
+ scanFetchCalls(fullPath);
9536
+ } else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
9537
+ try {
9538
+ const content = fs.readFileSync(fullPath, 'utf-8');
9539
+ const relativePath = path.relative(cwd, fullPath);
9540
+
9541
+ // Match fetch('/api/...') or fetch(`/api/...`)
9542
+ const fetchMatches = content.matchAll(/fetch\s*\(\s*[`'"]([^`'"]+)[`'"]/g);
9543
+ for (const match of fetchMatches) {
9544
+ const url = match[1];
9545
+ if (url.startsWith('/api/')) {
9546
+ // Extract base path (before query params or dynamic segments)
9547
+ const basePath = url.split('?')[0].replace(/\$\{[^}]+\}/g, '[dynamic]');
9548
+
9549
+ // Check if this route exists
9550
+ let routeFound = false;
9551
+ for (const [route] of apiRoutes) {
9552
+ // Simple matching - could be improved
9553
+ if (basePath === route || basePath.startsWith(route + '/')) {
9554
+ routeFound = true;
9555
+ break;
9556
+ }
9557
+ }
9558
+
9559
+ if (!routeFound && !basePath.includes('[dynamic]')) {
9560
+ issues.push({
9561
+ category: 'api',
9562
+ severity: 'warning',
9563
+ file: relativePath,
9564
+ message: `Fetch to '${url}' but no matching API route found`,
9565
+ fix: `Create the API route or fix the URL`,
9566
+ autoFixable: false,
9567
+ });
9568
+ }
9569
+ }
9570
+ }
9571
+ } catch {
9572
+ // Skip
9573
+ }
9574
+ }
9575
+ }
9576
+ };
9577
+
9578
+ for (const dir of ['src', 'app', 'components', 'lib']) {
9579
+ scanFetchCalls(path.join(cwd, dir));
9580
+ }
9581
+ }
9582
+
9583
+ // Generate report
9584
+ let response = `# 🔗 Coherence Audit Report\n\n`;
9585
+
9586
+ // Summary
9587
+ const errorCount = issues.filter(i => i.severity === 'error').length;
9588
+ const warningCount = issues.filter(i => i.severity === 'warning').length;
9589
+ const infoCount = issues.filter(i => i.severity === 'info').length;
9590
+ const autoFixableCount = issues.filter(i => i.autoFixable).length;
9591
+
9592
+ response += `## 📊 Summary\n\n`;
9593
+ response += `| Metric | Value |\n`;
9594
+ response += `|--------|-------|\n`;
9595
+ response += `| Files scanned | ${stats.filesScanned} |\n`;
9596
+ response += `| Imports checked | ${stats.importsChecked} |\n`;
9597
+ response += `| Exports found | ${stats.exportsFound} |\n`;
9598
+ response += `| 🔴 Errors | ${errorCount} |\n`;
9599
+ response += `| 🟡 Warnings | ${warningCount} |\n`;
9600
+ response += `| 🔵 Info | ${infoCount} |\n`;
9601
+ response += `| 🔧 Auto-fixable | ${autoFixableCount} |\n\n`;
9602
+
9603
+ if (issues.length === 0) {
9604
+ response += `## ✅ All Clear!\n\n`;
9605
+ response += `No coherence issues found. Your codebase wiring is solid.\n`;
9606
+ } else {
9607
+ // Group issues by category
9608
+ const categories: Record<string, CoherenceIssue[]> = {
9609
+ import: [],
9610
+ export: [],
9611
+ type: [],
9612
+ schema: [],
9613
+ api: [],
9614
+ env: [],
9615
+ circular: [],
9616
+ 'dead-code': [],
9617
+ };
9618
+
9619
+ for (const issue of issues) {
9620
+ categories[issue.category].push(issue);
9621
+ }
9622
+
9623
+ const categoryNames: Record<string, string> = {
9624
+ import: '🔴 Broken Imports',
9625
+ export: '🔴 Missing Exports',
9626
+ type: '🟠 Type Mismatches',
9627
+ schema: '🟠 Schema Issues',
9628
+ api: '🟡 API Contract Issues',
9629
+ env: '🟡 Environment Variables',
9630
+ circular: '⚪ Circular Dependencies',
9631
+ 'dead-code': '🔵 Dead Code',
9632
+ };
9633
+
9634
+ for (const [category, catIssues] of Object.entries(categories)) {
9635
+ if (catIssues.length === 0) continue;
9636
+
9637
+ response += `## ${categoryNames[category]} (${catIssues.length})\n\n`;
9638
+
9639
+ // Show first 10 issues per category
9640
+ const displayIssues = catIssues.slice(0, 10);
9641
+ for (const issue of displayIssues) {
9642
+ response += `### \`${issue.file}${issue.line ? `:${issue.line}` : ''}\`\n`;
9643
+ response += `**Issue:** ${issue.message}\n`;
9644
+ if (issue.fix) {
9645
+ response += `**Fix:** ${issue.fix}${issue.autoFixable ? ' 🔧' : ''}\n`;
9646
+ }
9647
+ response += `\n`;
9648
+ }
9649
+
9650
+ if (catIssues.length > 10) {
9651
+ response += `*... and ${catIssues.length - 10} more ${category} issues*\n\n`;
9652
+ }
9653
+ }
9654
+ }
9655
+
9656
+ // Recommendations
9657
+ response += `## 💡 Recommendations\n\n`;
9658
+
9659
+ if (errorCount > 0) {
9660
+ response += `1. **Fix ${errorCount} errors first** - These will cause runtime failures\n`;
9661
+ }
9662
+ if (warningCount > 0) {
9663
+ response += `2. **Review ${warningCount} warnings** - These may cause issues\n`;
9664
+ }
9665
+ response += `3. Run \`npx tsc --noEmit\` to verify TypeScript compiles\n`;
9666
+ response += `4. Run your test suite to verify functionality\n`;
9667
+
9668
+ if (autoFixableCount > 0) {
9669
+ response += `\n---\n\n`;
9670
+ response += `**${autoFixableCount} issues can be auto-fixed.** Run \`guardian_heal\` to fix them.\n`;
9671
+ }
9672
+
9673
+ // Save state for guardian_heal
9674
+ const statePath = path.join(cwd, '.codebakers', 'coherence-state.json');
9675
+ try {
9676
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
9677
+ fs.writeFileSync(statePath, JSON.stringify({
9678
+ lastAudit: new Date().toISOString(),
9679
+ focus,
9680
+ stats,
9681
+ issues: issues.map(i => ({
9682
+ ...i,
9683
+ // Include only auto-fixable for healing
9684
+ })),
9685
+ summary: { errors: errorCount, warnings: warningCount, info: infoCount, autoFixable: autoFixableCount },
9686
+ }, null, 2));
9687
+ } catch {
9688
+ // Ignore write errors
9689
+ }
9690
+
9691
+ return { content: [{ type: 'text' as const, text: response }] };
9692
+ }
9693
+
8938
9694
  // ============================================================================
8939
9695
  // PROJECT TRACKING - Server-Side Dashboard
8940
9696
  // ============================================================================