gitnexus 1.3.6 → 1.3.8
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/dist/cli/ai-context.js +77 -23
- package/dist/cli/analyze.js +4 -11
- package/dist/cli/eval-server.d.ts +7 -0
- package/dist/cli/eval-server.js +16 -7
- package/dist/cli/index.js +2 -20
- package/dist/cli/mcp.js +2 -0
- package/dist/cli/setup.js +6 -1
- package/dist/config/supported-languages.d.ts +1 -0
- package/dist/config/supported-languages.js +1 -0
- package/dist/core/ingestion/call-processor.d.ts +5 -1
- package/dist/core/ingestion/call-processor.js +78 -0
- package/dist/core/ingestion/framework-detection.d.ts +1 -0
- package/dist/core/ingestion/framework-detection.js +49 -2
- package/dist/core/ingestion/import-processor.js +90 -39
- package/dist/core/ingestion/parsing-processor.d.ts +12 -1
- package/dist/core/ingestion/parsing-processor.js +92 -51
- package/dist/core/ingestion/pipeline.js +21 -2
- package/dist/core/ingestion/process-processor.js +0 -1
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -0
- package/dist/core/ingestion/tree-sitter-queries.js +80 -0
- package/dist/core/ingestion/utils.d.ts +5 -0
- package/dist/core/ingestion/utils.js +20 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
- package/dist/core/ingestion/workers/parse-worker.js +473 -51
- package/dist/core/kuzu/csv-generator.d.ts +4 -0
- package/dist/core/kuzu/csv-generator.js +23 -9
- package/dist/core/kuzu/kuzu-adapter.js +9 -3
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
- package/dist/core/tree-sitter/parser-loader.js +3 -0
- package/dist/mcp/core/kuzu-adapter.d.ts +4 -3
- package/dist/mcp/core/kuzu-adapter.js +79 -16
- package/dist/mcp/local/local-backend.d.ts +13 -0
- package/dist/mcp/local/local-backend.js +148 -105
- package/dist/mcp/server.js +26 -11
- package/dist/storage/git.js +4 -1
- package/dist/storage/repo-manager.js +16 -2
- package/hooks/claude/gitnexus-hook.cjs +28 -8
- package/hooks/claude/pre-tool-use.sh +2 -1
- package/package.json +11 -3
- package/dist/cli/claude-hooks.d.ts +0 -22
- package/dist/cli/claude-hooks.js +0 -97
- package/dist/cli/view.d.ts +0 -13
- package/dist/cli/view.js +0 -59
- package/dist/core/graph/html-graph-viewer.d.ts +0 -15
- package/dist/core/graph/html-graph-viewer.js +0 -542
- package/dist/core/graph/html-graph-viewer.test.d.ts +0 -1
- package/dist/core/graph/html-graph-viewer.test.js +0 -67
package/dist/cli/ai-context.js
CHANGED
|
@@ -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
|
-
* -
|
|
21
|
-
* -
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
19
|
+
* Design principles (learned from real agent behavior and industry research):
|
|
20
|
+
* - Inline critical workflows — skills 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 parameters — vague 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
|
|
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
|
-
|
|
33
|
+
> If any GitNexus tool warns the index is stale, run \`npx gitnexus analyze\` in terminal first.
|
|
33
34
|
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
43
|
+
## When Debugging
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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);
|
package/dist/cli/analyze.js
CHANGED
|
@@ -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) => {
|
|
@@ -316,10 +311,8 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
316
311
|
console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
|
|
317
312
|
}
|
|
318
313
|
console.log('');
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
process.exit(0);
|
|
324
|
-
}
|
|
314
|
+
// KuzuDB's native module holds open handles that prevent Node from exiting.
|
|
315
|
+
// ONNX Runtime also registers native atexit hooks that segfault on some
|
|
316
|
+
// platforms (#38, #40). Force-exit to ensure clean termination.
|
|
317
|
+
process.exit(0);
|
|
325
318
|
};
|
|
@@ -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;
|
package/dist/cli/eval-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
3
|
-
//
|
|
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
|
-
|
|
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 {
|
|
@@ -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 =
|
|
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
|
|
361
|
+
if (normalized.includes(pattern)) {
|
|
315
362
|
return {
|
|
316
363
|
framework: cfg.framework,
|
|
317
364
|
entryPointMultiplier: cfg.entryPointMultiplier,
|