gitnexus 1.3.6 → 1.3.7

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 (47) hide show
  1. package/dist/cli/ai-context.js +77 -23
  2. package/dist/cli/analyze.js +0 -5
  3. package/dist/cli/eval-server.d.ts +7 -0
  4. package/dist/cli/eval-server.js +16 -7
  5. package/dist/cli/index.js +2 -20
  6. package/dist/cli/mcp.js +2 -0
  7. package/dist/cli/setup.js +6 -1
  8. package/dist/config/supported-languages.d.ts +1 -0
  9. package/dist/config/supported-languages.js +1 -0
  10. package/dist/core/ingestion/call-processor.d.ts +5 -1
  11. package/dist/core/ingestion/call-processor.js +78 -0
  12. package/dist/core/ingestion/framework-detection.d.ts +1 -0
  13. package/dist/core/ingestion/framework-detection.js +49 -2
  14. package/dist/core/ingestion/import-processor.js +90 -39
  15. package/dist/core/ingestion/parsing-processor.d.ts +12 -1
  16. package/dist/core/ingestion/parsing-processor.js +92 -51
  17. package/dist/core/ingestion/pipeline.js +21 -2
  18. package/dist/core/ingestion/process-processor.js +0 -1
  19. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -0
  20. package/dist/core/ingestion/tree-sitter-queries.js +80 -0
  21. package/dist/core/ingestion/utils.d.ts +5 -0
  22. package/dist/core/ingestion/utils.js +20 -0
  23. package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
  24. package/dist/core/ingestion/workers/parse-worker.js +473 -51
  25. package/dist/core/kuzu/csv-generator.d.ts +4 -0
  26. package/dist/core/kuzu/csv-generator.js +23 -9
  27. package/dist/core/kuzu/kuzu-adapter.js +9 -3
  28. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  29. package/dist/core/tree-sitter/parser-loader.js +3 -0
  30. package/dist/mcp/core/kuzu-adapter.d.ts +4 -3
  31. package/dist/mcp/core/kuzu-adapter.js +79 -16
  32. package/dist/mcp/local/local-backend.d.ts +13 -0
  33. package/dist/mcp/local/local-backend.js +148 -105
  34. package/dist/mcp/server.js +26 -11
  35. package/dist/storage/git.js +4 -1
  36. package/dist/storage/repo-manager.js +16 -2
  37. package/hooks/claude/gitnexus-hook.cjs +28 -8
  38. package/hooks/claude/pre-tool-use.sh +2 -1
  39. package/package.json +11 -3
  40. package/dist/cli/claude-hooks.d.ts +0 -22
  41. package/dist/cli/claude-hooks.js +0 -97
  42. package/dist/cli/view.d.ts +0 -13
  43. package/dist/cli/view.js +0 -59
  44. package/dist/core/graph/html-graph-viewer.d.ts +0 -15
  45. package/dist/core/graph/html-graph-viewer.js +0 -542
  46. package/dist/core/graph/html-graph-viewer.test.d.ts +0 -1
  47. package/dist/core/graph/html-graph-viewer.test.js +0 -67
@@ -16,37 +16,91 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
16
16
  /**
17
17
  * Generate the full GitNexus context content.
18
18
  *
19
- * Design principles (learned from real agent behavior):
20
- * - AGENTS.md is the ROUTER it tells the agent WHICH skill to read
21
- * - Skills contain the actual workflowsAGENTS.md does NOT duplicate them
22
- * - Bold **IMPORTANT** block + "Skills Read First" heading agents skip soft suggestions
23
- * - One-line quick start (read context resource) gives agents an entry point
24
- * - Tools/Resources sections are labeled "Reference"agents treat them as lookup, not workflow
19
+ * Design principles (learned from real agent behavior and industry research):
20
+ * - Inline critical workflowsskills are skipped 56% of the time (Vercel eval data)
21
+ * - Use RFC 2119 language (MUST, NEVER, ALWAYS) models follow imperative rules
22
+ * - Three-tier boundaries (Always/When/Never)proven to change model behavior
23
+ * - Keep under 120 lines adherence degrades past 150 lines
24
+ * - Exact tool commands with parametersvague directives get ignored
25
+ * - Self-review checklist — forces model to verify its own work
25
26
  */
26
27
  function generateGitNexusContent(projectName, stats) {
27
28
  return `${GITNEXUS_START_MARKER}
28
- # GitNexus MCP
29
+ # GitNexus — Code Intelligence
29
30
 
30
- This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows).
31
+ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
31
32
 
32
- ## Always Start Here
33
+ > If any GitNexus tool warns the index is stale, run \`npx gitnexus analyze\` in terminal first.
33
34
 
34
- 1. **Read \`gitnexus://repo/{name}/context\`** — codebase overview + check index freshness
35
- 2. **Match your task to a skill below** and **read that skill file**
36
- 3. **Follow the skill's workflow and checklist**
35
+ ## Always Do
37
36
 
38
- > If step 1 warns the index is stale, run \`npx gitnexus analyze\` in the terminal first.
37
+ - **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`gitnexus_impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.
38
+ - **MUST run \`gitnexus_detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows.
39
+ - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
40
+ - When exploring unfamiliar code, use \`gitnexus_query({query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
41
+ - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use \`gitnexus_context({name: "symbolName"})\`.
39
42
 
40
- ## Skills
43
+ ## When Debugging
41
44
 
42
- | Task | Read this skill file |
43
- |------|---------------------|
44
- | Understand architecture / "How does X work?" | \`.claude/skills/gitnexus/gitnexus-exploring/SKILL.md\` |
45
- | Blast radius / "What breaks if I change X?" | \`.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md\` |
46
- | Trace bugs / "Why is X failing?" | \`.claude/skills/gitnexus/gitnexus-debugging/SKILL.md\` |
47
- | Rename / extract / split / refactor | \`.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md\` |
48
- | Tools, resources, schema reference | \`.claude/skills/gitnexus/gitnexus-guide/SKILL.md\` |
49
- | Index, status, clean, wiki CLI commands | \`.claude/skills/gitnexus/gitnexus-cli/SKILL.md\` |
45
+ 1. \`gitnexus_query({query: "<error or symptom>"})\` find execution flows related to the issue
46
+ 2. \`gitnexus_context({name: "<suspect function>"})\` — see all callers, callees, and process participation
47
+ 3. \`READ gitnexus://repo/${projectName}/process/{processName}\` trace the full execution flow step by step
48
+ 4. For regressions: \`gitnexus_detect_changes({scope: "compare", base_ref: "main"})\` see what your branch changed
49
+
50
+ ## When Refactoring
51
+
52
+ - **Renaming**: MUST use \`gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})\` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with \`dry_run: false\`.
53
+ - **Extracting/Splitting**: MUST run \`gitnexus_context({name: "target"})\` to see all incoming/outgoing refs, then \`gitnexus_impact({target: "target", direction: "upstream"})\` to find all external callers before moving code.
54
+ - After any refactor: run \`gitnexus_detect_changes({scope: "all"})\` to verify only expected files changed.
55
+
56
+ ## Never Do
57
+
58
+ - NEVER edit a function, class, or method without first running \`gitnexus_impact\` on it.
59
+ - NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
60
+ - NEVER rename symbols with find-and-replace — use \`gitnexus_rename\` which understands the call graph.
61
+ - NEVER commit changes without running \`gitnexus_detect_changes()\` to check affected scope.
62
+
63
+ ## Tools Quick Reference
64
+
65
+ | Tool | When to use | Command |
66
+ |------|-------------|---------|
67
+ | \`query\` | Find code by concept | \`gitnexus_query({query: "auth validation"})\` |
68
+ | \`context\` | 360-degree view of one symbol | \`gitnexus_context({name: "validateUser"})\` |
69
+ | \`impact\` | Blast radius before editing | \`gitnexus_impact({target: "X", direction: "upstream"})\` |
70
+ | \`detect_changes\` | Pre-commit scope check | \`gitnexus_detect_changes({scope: "staged"})\` |
71
+ | \`rename\` | Safe multi-file rename | \`gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})\` |
72
+ | \`cypher\` | Custom graph queries | \`gitnexus_cypher({query: "MATCH ..."})\` |
73
+
74
+ ## Impact Risk Levels
75
+
76
+ | Depth | Meaning | Action |
77
+ |-------|---------|--------|
78
+ | d=1 | WILL BREAK — direct callers/importers | MUST update these |
79
+ | d=2 | LIKELY AFFECTED — indirect deps | Should test |
80
+ | d=3 | MAY NEED TESTING — transitive | Test if critical path |
81
+
82
+ ## Resources
83
+
84
+ | Resource | Use for |
85
+ |----------|---------|
86
+ | \`gitnexus://repo/${projectName}/context\` | Codebase overview, check index freshness |
87
+ | \`gitnexus://repo/${projectName}/clusters\` | All functional areas |
88
+ | \`gitnexus://repo/${projectName}/processes\` | All execution flows |
89
+ | \`gitnexus://repo/${projectName}/process/{name}\` | Step-by-step execution trace |
90
+
91
+ ## Self-Check Before Finishing
92
+
93
+ Before completing any code modification task, verify:
94
+ 1. \`gitnexus_impact\` was run for all modified symbols
95
+ 2. No HIGH/CRITICAL risk warnings were ignored
96
+ 3. \`gitnexus_detect_changes()\` confirms changes match expected scope
97
+ 4. All d=1 (WILL BREAK) dependents were updated
98
+
99
+ ## CLI
100
+
101
+ - Re-index: \`npx gitnexus analyze\`
102
+ - Check freshness: \`npx gitnexus status\`
103
+ - Generate docs: \`npx gitnexus wiki\`
50
104
 
51
105
  ${GITNEXUS_END_MARKER}`;
52
106
  }
@@ -78,7 +132,7 @@ async function upsertGitNexusSection(filePath, content) {
78
132
  // Check if GitNexus section already exists
79
133
  const startIdx = existingContent.indexOf(GITNEXUS_START_MARKER);
80
134
  const endIdx = existingContent.indexOf(GITNEXUS_END_MARKER);
81
- if (startIdx !== -1 && endIdx !== -1) {
135
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
82
136
  // Replace existing section
83
137
  const before = existingContent.substring(0, startIdx);
84
138
  const after = existingContent.substring(endIdx + GITNEXUS_END_MARKER.length);
@@ -17,7 +17,6 @@ import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getG
17
17
  import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
18
18
  import { generateAIContextFiles } from './ai-context.js';
19
19
  import fs from 'fs/promises';
20
- import { registerClaudeHook } from './claude-hooks.js';
21
20
  const HEAP_MB = 8192;
22
21
  const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
23
22
  /** Re-exec the process with an 8GB heap if we're currently below that. */
@@ -258,7 +257,6 @@ export const analyzeCommand = async (inputPath, options) => {
258
257
  await saveMeta(storagePath, meta);
259
258
  await registerRepo(repoPath, meta);
260
259
  await addToGitignore(repoPath);
261
- const hookResult = await registerClaudeHook();
262
260
  const projectName = path.basename(repoPath);
263
261
  let aggregatedClusterCount = 0;
264
262
  if (pipelineResult.communityResult?.communities) {
@@ -298,9 +296,6 @@ export const analyzeCommand = async (inputPath, options) => {
298
296
  if (aiContext.files.length > 0) {
299
297
  console.log(` Context: ${aiContext.files.join(', ')}`);
300
298
  }
301
- if (hookResult.registered) {
302
- console.log(` Hooks: ${hookResult.message}`);
303
- }
304
299
  // Show a quiet summary if some edge types needed fallback insertion
305
300
  if (kuzuWarnings.length > 0) {
306
301
  const totalFallback = kuzuWarnings.reduce((sum, w) => {
@@ -27,4 +27,11 @@ export interface EvalServerOptions {
27
27
  port?: string;
28
28
  idleTimeout?: string;
29
29
  }
30
+ export declare function formatQueryResult(result: any): string;
31
+ export declare function formatContextResult(result: any): string;
32
+ export declare function formatImpactResult(result: any): string;
33
+ export declare function formatCypherResult(result: any): string;
34
+ export declare function formatDetectChangesResult(result: any): string;
35
+ export declare function formatListReposResult(result: any): string;
30
36
  export declare function evalServerCommand(options?: EvalServerOptions): Promise<void>;
37
+ export declare const MAX_BODY_SIZE: number;
@@ -28,7 +28,7 @@ import { LocalBackend } from '../mcp/local/local-backend.js';
28
28
  // ─── Text Formatters ──────────────────────────────────────────────────
29
29
  // Convert structured JSON results into compact, LLM-friendly text.
30
30
  // Design: minimize tokens, maximize actionability.
31
- function formatQueryResult(result) {
31
+ export function formatQueryResult(result) {
32
32
  if (result.error)
33
33
  return `Error: ${result.error}`;
34
34
  const lines = [];
@@ -63,7 +63,7 @@ function formatQueryResult(result) {
63
63
  }
64
64
  return lines.join('\n').trim();
65
65
  }
66
- function formatContextResult(result) {
66
+ export function formatContextResult(result) {
67
67
  if (result.error)
68
68
  return `Error: ${result.error}`;
69
69
  if (result.status === 'ambiguous') {
@@ -120,7 +120,7 @@ function formatContextResult(result) {
120
120
  }
121
121
  return lines.join('\n').trim();
122
122
  }
123
- function formatImpactResult(result) {
123
+ export function formatImpactResult(result) {
124
124
  if (result.error)
125
125
  return `Error: ${result.error}`;
126
126
  const target = result.target;
@@ -154,7 +154,7 @@ function formatImpactResult(result) {
154
154
  }
155
155
  return lines.join('\n').trim();
156
156
  }
157
- function formatCypherResult(result) {
157
+ export function formatCypherResult(result) {
158
158
  if (result.error)
159
159
  return `Error: ${result.error}`;
160
160
  if (Array.isArray(result)) {
@@ -174,7 +174,7 @@ function formatCypherResult(result) {
174
174
  }
175
175
  return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
176
176
  }
177
- function formatDetectChangesResult(result) {
177
+ export function formatDetectChangesResult(result) {
178
178
  if (result.error)
179
179
  return `Error: ${result.error}`;
180
180
  const summary = result.summary || {};
@@ -205,7 +205,7 @@ function formatDetectChangesResult(result) {
205
205
  }
206
206
  return lines.join('\n').trim();
207
207
  }
208
- function formatListReposResult(result) {
208
+ export function formatListReposResult(result) {
209
209
  if (!Array.isArray(result) || result.length === 0) {
210
210
  return 'No indexed repositories.';
211
211
  }
@@ -362,10 +362,19 @@ export async function evalServerCommand(options) {
362
362
  process.on('SIGINT', shutdown);
363
363
  process.on('SIGTERM', shutdown);
364
364
  }
365
+ export const MAX_BODY_SIZE = 1024 * 1024; // 1MB
365
366
  function readBody(req) {
366
367
  return new Promise((resolve, reject) => {
367
368
  const chunks = [];
368
- req.on('data', (chunk) => chunks.push(chunk));
369
+ let totalSize = 0;
370
+ req.on('data', (chunk) => {
371
+ totalSize += chunk.length;
372
+ if (totalSize > MAX_BODY_SIZE) {
373
+ req.destroy(new Error('Request body too large (max 1MB)'));
374
+ return;
375
+ }
376
+ chunks.push(chunk);
377
+ });
369
378
  req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
370
379
  req.on('error', reject);
371
380
  });
package/dist/cli/index.js CHANGED
@@ -1,24 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // Raise Node heap limit for large repos (e.g. Linux kernel).
3
- // Must run before any heavy allocation. If already set by the user, respect it.
4
- if (!process.env.NODE_OPTIONS?.includes('--max-old-space-size')) {
5
- const execArgv = process.execArgv.join(' ');
6
- if (!execArgv.includes('--max-old-space-size')) {
7
- // Re-spawn with a larger heap (8 GB)
8
- const { execFileSync } = await import('node:child_process');
9
- try {
10
- execFileSync(process.execPath, ['--max-old-space-size=8192', ...process.argv.slice(1)], {
11
- stdio: 'inherit',
12
- env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --max-old-space-size=8192`.trim() },
13
- });
14
- process.exit(0);
15
- }
16
- catch (e) {
17
- // If the child exited with an error code, propagate it
18
- process.exit(e.status ?? 1);
19
- }
20
- }
21
- }
2
+ // Heap re-spawn removed only analyze.ts needs the 8GB heap (via its own ensureHeap()).
3
+ // Removing it from here improves MCP server startup time significantly.
22
4
  import { Command } from 'commander';
23
5
  import { analyzeCommand } from './analyze.js';
24
6
  import { serveCommand } from './serve.js';
package/dist/cli/mcp.js CHANGED
@@ -12,6 +12,8 @@ export const mcpCommand = async () => {
12
12
  // KuzuDB lock conflicts and transient errors should degrade gracefully.
13
13
  process.on('uncaughtException', (err) => {
14
14
  console.error(`GitNexus MCP: uncaught exception — ${err.message}`);
15
+ // Process is in an undefined state after uncaughtException — exit after flushing
16
+ setTimeout(() => process.exit(1), 100);
15
17
  });
16
18
  process.on('unhandledRejection', (reason) => {
17
19
  const msg = reason instanceof Error ? reason.message : String(reason);
package/dist/cli/setup.js CHANGED
@@ -142,7 +142,12 @@ async function installClaudeCodeHooks(result) {
142
142
  const src = path.join(pluginHooksPath, 'gitnexus-hook.cjs');
143
143
  const dest = path.join(destHooksDir, 'gitnexus-hook.cjs');
144
144
  try {
145
- const content = await fs.readFile(src, 'utf-8');
145
+ let content = await fs.readFile(src, 'utf-8');
146
+ // Inject resolved CLI path so the copied hook can find the CLI
147
+ // even when it's no longer inside the npm package tree
148
+ const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
149
+ const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
150
+ content = content.replace("let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", `let cliPath = '${normalizedCli}';`);
146
151
  await fs.writeFile(dest, content, 'utf-8');
147
152
  }
148
153
  catch {
@@ -9,5 +9,6 @@ export declare enum SupportedLanguages {
9
9
  Go = "go",
10
10
  Rust = "rust",
11
11
  PHP = "php",
12
+ Kotlin = "kotlin",
12
13
  Swift = "swift"
13
14
  }
@@ -10,6 +10,7 @@ export var SupportedLanguages;
10
10
  SupportedLanguages["Go"] = "go";
11
11
  SupportedLanguages["Rust"] = "rust";
12
12
  SupportedLanguages["PHP"] = "php";
13
+ SupportedLanguages["Kotlin"] = "kotlin";
13
14
  // Ruby = 'ruby',
14
15
  SupportedLanguages["Swift"] = "swift";
15
16
  })(SupportedLanguages || (SupportedLanguages = {}));
@@ -2,7 +2,7 @@ import { KnowledgeGraph } from '../graph/types.js';
2
2
  import { ASTCache } from './ast-cache.js';
3
3
  import { SymbolTable } from './symbol-table.js';
4
4
  import { ImportMap } from './import-processor.js';
5
- import type { ExtractedCall } from './workers/parse-worker.js';
5
+ import type { ExtractedCall, ExtractedRoute } from './workers/parse-worker.js';
6
6
  export declare const processCalls: (graph: KnowledgeGraph, files: {
7
7
  path: string;
8
8
  content: string;
@@ -13,3 +13,7 @@ export declare const processCalls: (graph: KnowledgeGraph, files: {
13
13
  * This function only does symbol table lookups + graph mutations.
14
14
  */
15
15
  export declare const processCallsFromExtracted: (graph: KnowledgeGraph, extractedCalls: ExtractedCall[], symbolTable: SymbolTable, importMap: ImportMap, onProgress?: (current: number, total: number) => void) => Promise<void>;
16
+ /**
17
+ * Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
18
+ */
19
+ export declare const processRoutesFromExtracted: (graph: KnowledgeGraph, extractedRoutes: ExtractedRoute[], symbolTable: SymbolTable, importMap: ImportMap, onProgress?: (current: number, total: number) => void) => Promise<void>;
@@ -31,6 +31,10 @@ const FUNCTION_NODE_TYPES = new Set([
31
31
  // Rust
32
32
  'function_item',
33
33
  'impl_item', // Methods inside impl blocks
34
+ // Kotlin (function_declaration already included above via JS/TS)
35
+ 'anonymous_function',
36
+ 'lambda_literal',
37
+ // PHP — no additional node types needed
34
38
  // Swift
35
39
  'init_declaration',
36
40
  'deinit_declaration',
@@ -269,6 +273,22 @@ const BUILT_IN_NAMES = new Set([
269
273
  'open', 'read', 'write', 'close', 'append', 'extend', 'update',
270
274
  'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
271
275
  'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
276
+ // Kotlin stdlib (IMPORTANT: keep in sync with parse-worker.ts BUILT_IN_NAMES)
277
+ 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error',
278
+ 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf',
279
+ 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless',
280
+ 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet',
281
+ 'repeat', 'synchronized',
282
+ // Kotlin coroutine builders & scope functions
283
+ 'launch', 'async', 'runBlocking', 'withContext', 'coroutineScope',
284
+ 'supervisorScope', 'delay',
285
+ // Kotlin Flow operators
286
+ 'flow', 'flowOf', 'collect', 'emit', 'onEach', 'catch',
287
+ 'buffer', 'conflate', 'distinctUntilChanged',
288
+ 'flatMapLatest', 'flatMapMerge', 'combine',
289
+ 'stateIn', 'shareIn', 'launchIn',
290
+ // Kotlin infix stdlib functions
291
+ 'to', 'until', 'downTo', 'step',
272
292
  // C/C++ standard library and common kernel helpers
273
293
  'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
274
294
  'scanf', 'fscanf', 'sscanf',
@@ -364,3 +384,61 @@ export const processCallsFromExtracted = async (graph, extractedCalls, symbolTab
364
384
  }
365
385
  onProgress?.(totalFiles, totalFiles);
366
386
  };
387
+ /**
388
+ * Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
389
+ */
390
+ export const processRoutesFromExtracted = async (graph, extractedRoutes, symbolTable, importMap, onProgress) => {
391
+ for (let i = 0; i < extractedRoutes.length; i++) {
392
+ const route = extractedRoutes[i];
393
+ if (i % 50 === 0) {
394
+ onProgress?.(i, extractedRoutes.length);
395
+ await yieldToEventLoop();
396
+ }
397
+ if (!route.controllerName || !route.methodName)
398
+ continue;
399
+ // Resolve controller class in symbol table
400
+ const controllerDefs = symbolTable.lookupFuzzy(route.controllerName);
401
+ if (controllerDefs.length === 0)
402
+ continue;
403
+ // Prefer import-resolved match
404
+ const importedFiles = importMap.get(route.filePath);
405
+ let controllerDef = controllerDefs[0];
406
+ let confidence = controllerDefs.length === 1 ? 0.7 : 0.5;
407
+ if (importedFiles) {
408
+ for (const def of controllerDefs) {
409
+ if (importedFiles.has(def.filePath)) {
410
+ controllerDef = def;
411
+ confidence = 0.9;
412
+ break;
413
+ }
414
+ }
415
+ }
416
+ // Find the method on the controller
417
+ const methodId = symbolTable.lookupExact(controllerDef.filePath, route.methodName);
418
+ const sourceId = generateId('File', route.filePath);
419
+ if (!methodId) {
420
+ // Construct method ID manually
421
+ const guessedId = generateId('Method', `${controllerDef.filePath}:${route.methodName}`);
422
+ const relId = generateId('CALLS', `${sourceId}:route->${guessedId}`);
423
+ graph.addRelationship({
424
+ id: relId,
425
+ sourceId,
426
+ targetId: guessedId,
427
+ type: 'CALLS',
428
+ confidence: confidence * 0.8,
429
+ reason: 'laravel-route',
430
+ });
431
+ continue;
432
+ }
433
+ const relId = generateId('CALLS', `${sourceId}:route->${methodId}`);
434
+ graph.addRelationship({
435
+ id: relId,
436
+ sourceId,
437
+ targetId: methodId,
438
+ type: 'CALLS',
439
+ confidence,
440
+ reason: 'laravel-route',
441
+ });
442
+ }
443
+ onProgress?.(extractedRoutes.length, extractedRoutes.length);
444
+ };
@@ -45,5 +45,6 @@ export declare const FRAMEWORK_AST_PATTERNS: {
45
45
  /**
46
46
  * Detect framework entry points from AST definition text (decorators/annotations/attributes).
47
47
  * Returns null if no known pattern is found.
48
+ * Note: callers should slice definitionText to ~300 chars since annotations appear at the start.
48
49
  */
49
50
  export declare function detectFrameworkFromAST(language: string, definitionText: string): FrameworkHint | null;
@@ -96,6 +96,41 @@ export function detectFrameworkFromPath(filePath) {
96
96
  if ((p.includes('/service/') || p.includes('/services/')) && p.endsWith('.java')) {
97
97
  return { framework: 'java-service', entryPointMultiplier: 1.8, reason: 'java-service' };
98
98
  }
99
+ // ========== KOTLIN FRAMEWORKS ==========
100
+ // Spring Boot Kotlin controllers
101
+ if ((p.includes('/controller/') || p.includes('/controllers/')) && p.endsWith('.kt')) {
102
+ return { framework: 'spring-kotlin', entryPointMultiplier: 3.0, reason: 'spring-kotlin-controller' };
103
+ }
104
+ // Spring Boot - files ending in Controller.kt
105
+ if (p.endsWith('controller.kt')) {
106
+ return { framework: 'spring-kotlin', entryPointMultiplier: 3.0, reason: 'spring-kotlin-controller-file' };
107
+ }
108
+ // Ktor routes
109
+ if (p.includes('/routes/') && p.endsWith('.kt')) {
110
+ return { framework: 'ktor', entryPointMultiplier: 2.5, reason: 'ktor-routes' };
111
+ }
112
+ // Ktor plugins folder or Routing.kt files
113
+ if (p.includes('/plugins/') && p.endsWith('.kt')) {
114
+ return { framework: 'ktor', entryPointMultiplier: 2.0, reason: 'ktor-plugin' };
115
+ }
116
+ if (p.endsWith('routing.kt') || p.endsWith('routes.kt')) {
117
+ return { framework: 'ktor', entryPointMultiplier: 2.5, reason: 'ktor-routing-file' };
118
+ }
119
+ // Android Activities, Fragments
120
+ if ((p.includes('/activity/') || p.includes('/ui/')) && p.endsWith('.kt')) {
121
+ return { framework: 'android-kotlin', entryPointMultiplier: 2.5, reason: 'android-ui' };
122
+ }
123
+ if (p.endsWith('activity.kt') || p.endsWith('fragment.kt')) {
124
+ return { framework: 'android-kotlin', entryPointMultiplier: 2.5, reason: 'android-component' };
125
+ }
126
+ // Kotlin main entry point
127
+ if (p.endsWith('/main.kt')) {
128
+ return { framework: 'kotlin', entryPointMultiplier: 3.0, reason: 'kotlin-main' };
129
+ }
130
+ // Kotlin Application entry point (common naming)
131
+ if (p.endsWith('/application.kt')) {
132
+ return { framework: 'kotlin', entryPointMultiplier: 2.5, reason: 'kotlin-application' };
133
+ }
99
134
  // ========== C# / .NET FRAMEWORKS ==========
100
135
  // ASP.NET Controllers
101
136
  if (p.includes('/controllers/') && p.endsWith('.cs')) {
@@ -291,6 +326,12 @@ const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE = {
291
326
  { framework: 'spring', entryPointMultiplier: 3.2, reason: 'spring-annotation', patterns: FRAMEWORK_AST_PATTERNS.spring },
292
327
  { framework: 'jaxrs', entryPointMultiplier: 3.0, reason: 'jaxrs-annotation', patterns: FRAMEWORK_AST_PATTERNS.jaxrs },
293
328
  ],
329
+ kotlin: [
330
+ { framework: 'spring-kotlin', entryPointMultiplier: 3.2, reason: 'spring-kotlin-annotation', patterns: FRAMEWORK_AST_PATTERNS.spring },
331
+ { framework: 'jaxrs', entryPointMultiplier: 3.0, reason: 'jaxrs-annotation', patterns: FRAMEWORK_AST_PATTERNS.jaxrs },
332
+ { framework: 'ktor', entryPointMultiplier: 2.8, reason: 'ktor-routing', patterns: ['routing', 'embeddedServer', 'Application.module'] },
333
+ { framework: 'android-kotlin', entryPointMultiplier: 2.5, reason: 'android-annotation', patterns: ['@AndroidEntryPoint', 'AppCompatActivity', 'Fragment('] },
334
+ ],
294
335
  csharp: [
295
336
  { framework: 'aspnet', entryPointMultiplier: 3.2, reason: 'aspnet-attribute', patterns: FRAMEWORK_AST_PATTERNS.aspnet },
296
337
  ],
@@ -298,20 +339,26 @@ const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE = {
298
339
  { framework: 'laravel', entryPointMultiplier: 3.0, reason: 'php-route-attribute', patterns: FRAMEWORK_AST_PATTERNS.laravel },
299
340
  ],
300
341
  };
342
+ /** Pre-lowercased patterns for O(1) pattern matching at runtime */
343
+ const AST_PATTERNS_LOWERED = Object.fromEntries(Object.entries(AST_FRAMEWORK_PATTERNS_BY_LANGUAGE).map(([lang, cfgs]) => [
344
+ lang,
345
+ cfgs.map(cfg => ({ ...cfg, patterns: cfg.patterns.map(p => p.toLowerCase()) })),
346
+ ]));
301
347
  /**
302
348
  * Detect framework entry points from AST definition text (decorators/annotations/attributes).
303
349
  * Returns null if no known pattern is found.
350
+ * Note: callers should slice definitionText to ~300 chars since annotations appear at the start.
304
351
  */
305
352
  export function detectFrameworkFromAST(language, definitionText) {
306
353
  if (!language || !definitionText)
307
354
  return null;
308
- const configs = AST_FRAMEWORK_PATTERNS_BY_LANGUAGE[language.toLowerCase()];
355
+ const configs = AST_PATTERNS_LOWERED[language.toLowerCase()];
309
356
  if (!configs || configs.length === 0)
310
357
  return null;
311
358
  const normalized = definitionText.toLowerCase();
312
359
  for (const cfg of configs) {
313
360
  for (const pattern of cfg.patterns) {
314
- if (normalized.includes(pattern.toLowerCase())) {
361
+ if (normalized.includes(pattern)) {
315
362
  return {
316
363
  framework: cfg.framework,
317
364
  entryPointMultiplier: cfg.entryPointMultiplier,