gitnexus 1.4.10 → 1.5.0
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/README.md +6 -5
- package/dist/cli/ai-context.d.ts +4 -1
- package/dist/cli/ai-context.js +19 -11
- package/dist/cli/analyze.d.ts +6 -0
- package/dist/cli/analyze.js +105 -251
- package/dist/cli/eval-server.js +20 -11
- package/dist/cli/index-repo.js +20 -22
- package/dist/cli/index.js +8 -7
- package/dist/cli/mcp.js +1 -1
- package/dist/cli/serve.js +29 -1
- package/dist/cli/setup.js +9 -9
- package/dist/cli/skill-gen.js +15 -9
- package/dist/cli/wiki.d.ts +2 -0
- package/dist/cli/wiki.js +141 -26
- package/dist/config/ignore-service.js +102 -22
- package/dist/config/supported-languages.d.ts +8 -42
- package/dist/config/supported-languages.js +8 -43
- package/dist/core/augmentation/engine.js +19 -7
- package/dist/core/embeddings/embedder.js +19 -15
- package/dist/core/embeddings/embedding-pipeline.js +6 -6
- package/dist/core/embeddings/http-client.js +3 -3
- package/dist/core/embeddings/text-generator.js +9 -24
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/embeddings/types.js +1 -7
- package/dist/core/graph/graph.js +6 -2
- package/dist/core/graph/types.d.ts +9 -59
- package/dist/core/ingestion/ast-cache.js +3 -3
- package/dist/core/ingestion/call-processor.d.ts +20 -2
- package/dist/core/ingestion/call-processor.js +347 -144
- package/dist/core/ingestion/call-routing.js +10 -4
- package/dist/core/ingestion/call-sites/extract-language-call-site.d.ts +10 -0
- package/dist/core/ingestion/call-sites/extract-language-call-site.js +22 -0
- package/dist/core/ingestion/call-sites/java.d.ts +9 -0
- package/dist/core/ingestion/call-sites/java.js +30 -0
- package/dist/core/ingestion/cluster-enricher.js +6 -8
- package/dist/core/ingestion/cobol/cobol-copy-expander.js +10 -3
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +287 -81
- package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
- package/dist/core/ingestion/cobol/jcl-processor.js +1 -1
- package/dist/core/ingestion/cobol-processor.js +102 -56
- package/dist/core/ingestion/community-processor.js +21 -15
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -1
- package/dist/core/ingestion/entry-point-scoring.js +5 -6
- package/dist/core/ingestion/export-detection.js +32 -9
- package/dist/core/ingestion/field-extractor.d.ts +1 -1
- package/dist/core/ingestion/field-extractors/configs/c-cpp.js +8 -12
- package/dist/core/ingestion/field-extractors/configs/csharp.js +45 -2
- package/dist/core/ingestion/field-extractors/configs/dart.js +5 -3
- package/dist/core/ingestion/field-extractors/configs/go.js +3 -7
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.js +14 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.js +7 -7
- package/dist/core/ingestion/field-extractors/configs/php.js +9 -11
- package/dist/core/ingestion/field-extractors/configs/python.js +1 -1
- package/dist/core/ingestion/field-extractors/configs/ruby.js +4 -3
- package/dist/core/ingestion/field-extractors/configs/rust.js +2 -5
- package/dist/core/ingestion/field-extractors/configs/swift.js +9 -7
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +2 -6
- package/dist/core/ingestion/field-extractors/generic.d.ts +5 -2
- package/dist/core/ingestion/field-extractors/generic.js +6 -0
- package/dist/core/ingestion/field-extractors/typescript.d.ts +1 -1
- package/dist/core/ingestion/field-extractors/typescript.js +1 -1
- package/dist/core/ingestion/field-types.d.ts +4 -2
- package/dist/core/ingestion/filesystem-walker.js +3 -3
- package/dist/core/ingestion/framework-detection.d.ts +1 -1
- package/dist/core/ingestion/framework-detection.js +355 -85
- package/dist/core/ingestion/heritage-processor.d.ts +24 -0
- package/dist/core/ingestion/heritage-processor.js +99 -8
- package/dist/core/ingestion/import-processor.js +44 -15
- package/dist/core/ingestion/import-resolvers/csharp.js +7 -3
- package/dist/core/ingestion/import-resolvers/dart.js +1 -1
- package/dist/core/ingestion/import-resolvers/go.js +4 -2
- package/dist/core/ingestion/import-resolvers/jvm.js +4 -4
- package/dist/core/ingestion/import-resolvers/php.js +4 -4
- package/dist/core/ingestion/import-resolvers/python.js +1 -1
- package/dist/core/ingestion/import-resolvers/rust.js +9 -3
- package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/standard.js +6 -5
- package/dist/core/ingestion/import-resolvers/swift.js +2 -1
- package/dist/core/ingestion/import-resolvers/utils.js +26 -7
- package/dist/core/ingestion/language-config.js +5 -4
- package/dist/core/ingestion/language-provider.d.ts +7 -2
- package/dist/core/ingestion/languages/c-cpp.js +106 -21
- package/dist/core/ingestion/languages/cobol.js +1 -1
- package/dist/core/ingestion/languages/csharp.js +96 -19
- package/dist/core/ingestion/languages/dart.js +23 -7
- package/dist/core/ingestion/languages/go.js +1 -1
- package/dist/core/ingestion/languages/index.d.ts +1 -1
- package/dist/core/ingestion/languages/index.js +2 -3
- package/dist/core/ingestion/languages/java.js +4 -1
- package/dist/core/ingestion/languages/kotlin.js +60 -13
- package/dist/core/ingestion/languages/php.js +102 -25
- package/dist/core/ingestion/languages/python.js +28 -5
- package/dist/core/ingestion/languages/ruby.js +56 -14
- package/dist/core/ingestion/languages/rust.js +55 -11
- package/dist/core/ingestion/languages/swift.js +112 -27
- package/dist/core/ingestion/languages/typescript.js +95 -19
- package/dist/core/ingestion/markdown-processor.js +5 -5
- package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +283 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +326 -0
- package/dist/core/ingestion/method-extractors/generic.d.ts +5 -0
- package/dist/core/ingestion/method-extractors/generic.js +137 -0
- package/dist/core/ingestion/method-types.d.ts +61 -0
- package/dist/core/ingestion/method-types.js +2 -0
- package/dist/core/ingestion/mro-processor.d.ts +1 -1
- package/dist/core/ingestion/mro-processor.js +12 -8
- package/dist/core/ingestion/named-binding-processor.js +2 -2
- package/dist/core/ingestion/named-bindings/rust.js +3 -1
- package/dist/core/ingestion/parsing-processor.js +74 -24
- package/dist/core/ingestion/pipeline.d.ts +2 -1
- package/dist/core/ingestion/pipeline.js +208 -102
- package/dist/core/ingestion/process-processor.js +12 -10
- package/dist/core/ingestion/resolution-context.js +3 -3
- package/dist/core/ingestion/route-extractors/middleware.js +31 -7
- package/dist/core/ingestion/route-extractors/php.js +2 -1
- package/dist/core/ingestion/route-extractors/response-shapes.js +8 -4
- package/dist/core/ingestion/structure-processor.d.ts +1 -1
- package/dist/core/ingestion/structure-processor.js +4 -4
- package/dist/core/ingestion/symbol-table.d.ts +1 -1
- package/dist/core/ingestion/symbol-table.js +22 -6
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +1 -1
- package/dist/core/ingestion/type-env.d.ts +2 -2
- package/dist/core/ingestion/type-env.js +75 -50
- package/dist/core/ingestion/type-extractors/c-cpp.js +33 -30
- package/dist/core/ingestion/type-extractors/csharp.js +24 -14
- package/dist/core/ingestion/type-extractors/dart.js +6 -8
- package/dist/core/ingestion/type-extractors/go.js +7 -6
- package/dist/core/ingestion/type-extractors/jvm.js +10 -21
- package/dist/core/ingestion/type-extractors/php.js +26 -13
- package/dist/core/ingestion/type-extractors/python.js +11 -15
- package/dist/core/ingestion/type-extractors/ruby.js +8 -3
- package/dist/core/ingestion/type-extractors/rust.js +6 -8
- package/dist/core/ingestion/type-extractors/shared.js +134 -50
- package/dist/core/ingestion/type-extractors/swift.js +16 -13
- package/dist/core/ingestion/type-extractors/typescript.js +23 -15
- package/dist/core/ingestion/utils/ast-helpers.d.ts +8 -8
- package/dist/core/ingestion/utils/ast-helpers.js +72 -35
- package/dist/core/ingestion/utils/call-analysis.d.ts +2 -0
- package/dist/core/ingestion/utils/call-analysis.js +96 -49
- package/dist/core/ingestion/utils/event-loop.js +1 -1
- package/dist/core/ingestion/workers/parse-worker.d.ts +7 -2
- package/dist/core/ingestion/workers/parse-worker.js +364 -84
- package/dist/core/ingestion/workers/worker-pool.js +5 -10
- package/dist/core/lbug/csv-generator.js +54 -15
- package/dist/core/lbug/lbug-adapter.d.ts +5 -0
- package/dist/core/lbug/lbug-adapter.js +86 -23
- package/dist/core/lbug/schema.d.ts +3 -6
- package/dist/core/lbug/schema.js +6 -30
- package/dist/core/run-analyze.d.ts +49 -0
- package/dist/core/run-analyze.js +257 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -1
- package/dist/core/tree-sitter/parser-loader.js +1 -1
- package/dist/core/wiki/cursor-client.js +2 -7
- package/dist/core/wiki/generator.js +38 -23
- package/dist/core/wiki/graph-queries.js +10 -10
- package/dist/core/wiki/html-viewer.js +7 -3
- package/dist/core/wiki/llm-client.d.ts +23 -2
- package/dist/core/wiki/llm-client.js +96 -26
- package/dist/core/wiki/prompts.js +7 -6
- package/dist/mcp/core/embedder.js +1 -1
- package/dist/mcp/core/lbug-adapter.d.ts +4 -1
- package/dist/mcp/core/lbug-adapter.js +17 -7
- package/dist/mcp/local/local-backend.js +247 -95
- package/dist/mcp/resources.js +14 -6
- package/dist/mcp/server.js +13 -5
- package/dist/mcp/staleness.js +5 -1
- package/dist/mcp/tools.js +100 -23
- package/dist/server/analyze-job.d.ts +53 -0
- package/dist/server/analyze-job.js +146 -0
- package/dist/server/analyze-worker.d.ts +13 -0
- package/dist/server/analyze-worker.js +59 -0
- package/dist/server/api.js +795 -44
- package/dist/server/git-clone.d.ts +25 -0
- package/dist/server/git-clone.js +91 -0
- package/dist/storage/git.js +1 -3
- package/dist/storage/repo-manager.d.ts +5 -2
- package/dist/storage/repo-manager.js +4 -4
- package/dist/types/pipeline.d.ts +1 -21
- package/dist/types/pipeline.js +1 -18
- package/hooks/claude/gitnexus-hook.cjs +52 -22
- package/package.json +3 -2
- package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
- package/dist/core/ingestion/utils/language-detection.js +0 -70
package/dist/mcp/resources.js
CHANGED
|
@@ -81,10 +81,18 @@ function parseUri(uri) {
|
|
|
81
81
|
const repoName = decodeURIComponent(repoMatch[1]);
|
|
82
82
|
const rest = repoMatch[2];
|
|
83
83
|
if (rest.startsWith('cluster/')) {
|
|
84
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
repoName,
|
|
86
|
+
resourceType: 'cluster',
|
|
87
|
+
param: decodeURIComponent(rest.replace('cluster/', '')),
|
|
88
|
+
};
|
|
85
89
|
}
|
|
86
90
|
if (rest.startsWith('process/')) {
|
|
87
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
repoName,
|
|
93
|
+
resourceType: 'process',
|
|
94
|
+
param: decodeURIComponent(rest.replace('process/', '')),
|
|
95
|
+
};
|
|
88
96
|
}
|
|
89
97
|
return { repoName, resourceType: rest };
|
|
90
98
|
}
|
|
@@ -163,10 +171,10 @@ async function getContextResource(backend, repoName) {
|
|
|
163
171
|
// Check staleness
|
|
164
172
|
const repoPath = repo.repoPath;
|
|
165
173
|
const lastCommit = repo.lastCommit || 'HEAD';
|
|
166
|
-
const staleness = repoPath
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
];
|
|
174
|
+
const staleness = repoPath
|
|
175
|
+
? checkStaleness(repoPath, lastCommit)
|
|
176
|
+
: { isStale: false, commitsBehind: 0 };
|
|
177
|
+
const lines = [`project: ${context.projectName}`];
|
|
170
178
|
if (staleness.isStale && staleness.hint) {
|
|
171
179
|
lines.push('');
|
|
172
180
|
lines.push(`staleness: "${staleness.hint}"`);
|
package/dist/mcp/server.js
CHANGED
|
@@ -77,7 +77,7 @@ export function createMCPServer(backend) {
|
|
|
77
77
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
78
78
|
const resources = getResourceDefinitions();
|
|
79
79
|
return {
|
|
80
|
-
resources: resources.map(r => ({
|
|
80
|
+
resources: resources.map((r) => ({
|
|
81
81
|
uri: r.uri,
|
|
82
82
|
name: r.name,
|
|
83
83
|
description: r.description,
|
|
@@ -89,7 +89,7 @@ export function createMCPServer(backend) {
|
|
|
89
89
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
90
90
|
const templates = getResourceTemplates();
|
|
91
91
|
return {
|
|
92
|
-
resourceTemplates: templates.map(t => ({
|
|
92
|
+
resourceTemplates: templates.map((t) => ({
|
|
93
93
|
uriTemplate: t.uriTemplate,
|
|
94
94
|
name: t.name,
|
|
95
95
|
description: t.description,
|
|
@@ -168,7 +168,11 @@ export function createMCPServer(backend) {
|
|
|
168
168
|
name: 'detect_impact',
|
|
169
169
|
description: 'Analyze the impact of your current changes before committing. Guides through scope selection, change detection, process analysis, and risk assessment.',
|
|
170
170
|
arguments: [
|
|
171
|
-
{
|
|
171
|
+
{
|
|
172
|
+
name: 'scope',
|
|
173
|
+
description: 'What to analyze: unstaged, staged, all, or compare',
|
|
174
|
+
required: false,
|
|
175
|
+
},
|
|
172
176
|
{ name: 'base_ref', description: 'Branch/commit for compare scope', required: false },
|
|
173
177
|
],
|
|
174
178
|
},
|
|
@@ -176,7 +180,11 @@ export function createMCPServer(backend) {
|
|
|
176
180
|
name: 'generate_map',
|
|
177
181
|
description: 'Generate architecture documentation from the knowledge graph. Creates a codebase overview with execution flows and mermaid diagrams.',
|
|
178
182
|
arguments: [
|
|
179
|
-
{
|
|
183
|
+
{
|
|
184
|
+
name: 'repo',
|
|
185
|
+
description: 'Repository name (omit if only one indexed)',
|
|
186
|
+
required: false,
|
|
187
|
+
},
|
|
180
188
|
],
|
|
181
189
|
},
|
|
182
190
|
],
|
|
@@ -247,7 +255,7 @@ export async function startMCPServer(backend) {
|
|
|
247
255
|
return realStdoutWrite;
|
|
248
256
|
const val = Reflect.get(target, prop, receiver);
|
|
249
257
|
return typeof val === 'function' ? val.bind(target) : val;
|
|
250
|
-
}
|
|
258
|
+
},
|
|
251
259
|
});
|
|
252
260
|
const transport = new CompatibleStdioServerTransport(process.stdin, _safeStdout);
|
|
253
261
|
await server.connect(transport);
|
package/dist/mcp/staleness.js
CHANGED
|
@@ -11,7 +11,11 @@ import { execFileSync } from 'child_process';
|
|
|
11
11
|
export function checkStaleness(repoPath, lastCommit) {
|
|
12
12
|
try {
|
|
13
13
|
// Get count of commits between lastCommit and HEAD
|
|
14
|
-
const result = execFileSync('git', ['rev-list', '--count', `${lastCommit}..HEAD`], {
|
|
14
|
+
const result = execFileSync('git', ['rev-list', '--count', `${lastCommit}..HEAD`], {
|
|
15
|
+
cwd: repoPath,
|
|
16
|
+
encoding: 'utf-8',
|
|
17
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
18
|
+
}).trim();
|
|
15
19
|
const commitsBehind = parseInt(result, 10) || 0;
|
|
16
20
|
if (commitsBehind > 0) {
|
|
17
21
|
return {
|
package/dist/mcp/tools.js
CHANGED
|
@@ -40,12 +40,29 @@ Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank
|
|
|
40
40
|
type: 'object',
|
|
41
41
|
properties: {
|
|
42
42
|
query: { type: 'string', description: 'Natural language or keyword search query' },
|
|
43
|
-
task_context: {
|
|
44
|
-
|
|
43
|
+
task_context: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'What you are working on (e.g., "adding OAuth support"). Helps ranking.',
|
|
46
|
+
},
|
|
47
|
+
goal: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'What you want to find (e.g., "existing auth validation logic"). Helps ranking.',
|
|
50
|
+
},
|
|
45
51
|
limit: { type: 'number', description: 'Max processes to return (default: 5)', default: 5 },
|
|
46
|
-
max_symbols: {
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
max_symbols: {
|
|
53
|
+
type: 'number',
|
|
54
|
+
description: 'Max symbols per process (default: 10)',
|
|
55
|
+
default: 10,
|
|
56
|
+
},
|
|
57
|
+
include_content: {
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
description: 'Include full symbol source code (default: false)',
|
|
60
|
+
default: false,
|
|
61
|
+
},
|
|
62
|
+
repo: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
65
|
+
},
|
|
49
66
|
},
|
|
50
67
|
required: ['query'],
|
|
51
68
|
},
|
|
@@ -100,7 +117,10 @@ TIPS:
|
|
|
100
117
|
type: 'object',
|
|
101
118
|
properties: {
|
|
102
119
|
query: { type: 'string', description: 'Cypher query to execute' },
|
|
103
|
-
repo: {
|
|
120
|
+
repo: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
123
|
+
},
|
|
104
124
|
},
|
|
105
125
|
required: ['query'],
|
|
106
126
|
},
|
|
@@ -120,10 +140,20 @@ NOTE: ACCESSES edges (field read/write tracking) are included in context results
|
|
|
120
140
|
type: 'object',
|
|
121
141
|
properties: {
|
|
122
142
|
name: { type: 'string', description: 'Symbol name (e.g., "validateUser", "AuthService")' },
|
|
123
|
-
uid: {
|
|
143
|
+
uid: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
description: 'Direct symbol UID from prior tool results (zero-ambiguity lookup)',
|
|
146
|
+
},
|
|
124
147
|
file_path: { type: 'string', description: 'File path to disambiguate common names' },
|
|
125
|
-
include_content: {
|
|
126
|
-
|
|
148
|
+
include_content: {
|
|
149
|
+
type: 'boolean',
|
|
150
|
+
description: 'Include full symbol source code (default: false)',
|
|
151
|
+
default: false,
|
|
152
|
+
},
|
|
153
|
+
repo: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
156
|
+
},
|
|
127
157
|
},
|
|
128
158
|
required: [],
|
|
129
159
|
},
|
|
@@ -140,9 +170,20 @@ Returns: changed symbols, affected processes, and a risk summary.`,
|
|
|
140
170
|
inputSchema: {
|
|
141
171
|
type: 'object',
|
|
142
172
|
properties: {
|
|
143
|
-
scope: {
|
|
144
|
-
|
|
145
|
-
|
|
173
|
+
scope: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'What to analyze: "unstaged" (default), "staged", "all", or "compare"',
|
|
176
|
+
enum: ['unstaged', 'staged', 'all', 'compare'],
|
|
177
|
+
default: 'unstaged',
|
|
178
|
+
},
|
|
179
|
+
base_ref: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
description: 'Branch/commit for "compare" scope (e.g., "main")',
|
|
182
|
+
},
|
|
183
|
+
repo: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
186
|
+
},
|
|
146
187
|
},
|
|
147
188
|
required: [],
|
|
148
189
|
},
|
|
@@ -162,11 +203,21 @@ Each edit is tagged with confidence:
|
|
|
162
203
|
type: 'object',
|
|
163
204
|
properties: {
|
|
164
205
|
symbol_name: { type: 'string', description: 'Current symbol name to rename' },
|
|
165
|
-
symbol_uid: {
|
|
206
|
+
symbol_uid: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
description: 'Direct symbol UID from prior tool results (zero-ambiguity)',
|
|
209
|
+
},
|
|
166
210
|
new_name: { type: 'string', description: 'The new name for the symbol' },
|
|
167
211
|
file_path: { type: 'string', description: 'File path to disambiguate common names' },
|
|
168
|
-
dry_run: {
|
|
169
|
-
|
|
212
|
+
dry_run: {
|
|
213
|
+
type: 'boolean',
|
|
214
|
+
description: 'Preview edits without modifying files (default: true)',
|
|
215
|
+
default: true,
|
|
216
|
+
},
|
|
217
|
+
repo: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
220
|
+
},
|
|
170
221
|
},
|
|
171
222
|
required: ['new_name'],
|
|
172
223
|
},
|
|
@@ -199,12 +250,26 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`,
|
|
|
199
250
|
type: 'object',
|
|
200
251
|
properties: {
|
|
201
252
|
target: { type: 'string', description: 'Name of function, class, or file to analyze' },
|
|
202
|
-
direction: {
|
|
203
|
-
|
|
204
|
-
|
|
253
|
+
direction: {
|
|
254
|
+
type: 'string',
|
|
255
|
+
description: 'upstream (what depends on this) or downstream (what this depends on)',
|
|
256
|
+
},
|
|
257
|
+
maxDepth: {
|
|
258
|
+
type: 'number',
|
|
259
|
+
description: 'Max relationship depth (default: 3)',
|
|
260
|
+
default: 3,
|
|
261
|
+
},
|
|
262
|
+
relationTypes: {
|
|
263
|
+
type: 'array',
|
|
264
|
+
items: { type: 'string' },
|
|
265
|
+
description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)',
|
|
266
|
+
},
|
|
205
267
|
includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
|
|
206
268
|
minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },
|
|
207
|
-
repo: {
|
|
269
|
+
repo: {
|
|
270
|
+
type: 'string',
|
|
271
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
272
|
+
},
|
|
208
273
|
},
|
|
209
274
|
required: ['target', 'direction'],
|
|
210
275
|
},
|
|
@@ -220,8 +285,14 @@ Returns: route nodes with their handlers, middleware wrapper chains (e.g., withA
|
|
|
220
285
|
inputSchema: {
|
|
221
286
|
type: 'object',
|
|
222
287
|
properties: {
|
|
223
|
-
route: {
|
|
224
|
-
|
|
288
|
+
route: {
|
|
289
|
+
type: 'string',
|
|
290
|
+
description: 'Filter by route path (e.g., "/api/grants"). Omit for all routes.',
|
|
291
|
+
},
|
|
292
|
+
repo: {
|
|
293
|
+
type: 'string',
|
|
294
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
295
|
+
},
|
|
225
296
|
},
|
|
226
297
|
required: [],
|
|
227
298
|
},
|
|
@@ -253,8 +324,14 @@ Returns routes that have both detected response keys AND consumers. Shows top-le
|
|
|
253
324
|
inputSchema: {
|
|
254
325
|
type: 'object',
|
|
255
326
|
properties: {
|
|
256
|
-
route: {
|
|
257
|
-
|
|
327
|
+
route: {
|
|
328
|
+
type: 'string',
|
|
329
|
+
description: 'Check a specific route (e.g., "/api/grants"). Omit to check all routes.',
|
|
330
|
+
},
|
|
331
|
+
repo: {
|
|
332
|
+
type: 'string',
|
|
333
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
334
|
+
},
|
|
258
335
|
},
|
|
259
336
|
required: [],
|
|
260
337
|
},
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Job Manager
|
|
3
|
+
*
|
|
4
|
+
* Tracks server-side analysis jobs with:
|
|
5
|
+
* - In-memory Map storage
|
|
6
|
+
* - Single-slot concurrency (one active job at a time)
|
|
7
|
+
* - Same-repo deduplication (returns existing job)
|
|
8
|
+
* - Progress event emission for SSE relay
|
|
9
|
+
* - 1-hour TTL cleanup for completed/failed jobs
|
|
10
|
+
*/
|
|
11
|
+
import type { ChildProcess } from 'child_process';
|
|
12
|
+
export interface AnalyzeJobProgress {
|
|
13
|
+
phase: string;
|
|
14
|
+
percent: number;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AnalyzeJob {
|
|
18
|
+
id: string;
|
|
19
|
+
status: 'queued' | 'cloning' | 'analyzing' | 'loading' | 'complete' | 'failed';
|
|
20
|
+
repoUrl?: string;
|
|
21
|
+
repoPath?: string;
|
|
22
|
+
repoName?: string;
|
|
23
|
+
progress: AnalyzeJobProgress;
|
|
24
|
+
error?: string;
|
|
25
|
+
startedAt: number;
|
|
26
|
+
completedAt?: number;
|
|
27
|
+
/** Number of times the worker has been retried after a crash. */
|
|
28
|
+
retryCount: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class JobManager {
|
|
31
|
+
private jobs;
|
|
32
|
+
private children;
|
|
33
|
+
private timeouts;
|
|
34
|
+
private emitter;
|
|
35
|
+
private cleanupTimer;
|
|
36
|
+
constructor();
|
|
37
|
+
/** Create a new job, or return existing active job for the same repo. */
|
|
38
|
+
createJob(params: {
|
|
39
|
+
repoUrl?: string;
|
|
40
|
+
repoPath?: string;
|
|
41
|
+
}): AnalyzeJob;
|
|
42
|
+
getJob(id: string): AnalyzeJob | undefined;
|
|
43
|
+
updateJob(id: string, update: Partial<Pick<AnalyzeJob, 'status' | 'progress' | 'error' | 'repoPath' | 'repoName' | 'completedAt'>>): void;
|
|
44
|
+
/** Register a child process for a job — enables cancellation and timeout. */
|
|
45
|
+
registerChild(jobId: string, child: ChildProcess): void;
|
|
46
|
+
/** Cancel a running job — sends SIGTERM to child process. */
|
|
47
|
+
cancelJob(jobId: string, reason?: string): boolean;
|
|
48
|
+
/** Subscribe to progress events for a job. Returns unsubscribe function. */
|
|
49
|
+
onProgress(jobId: string, listener: (progress: AnalyzeJobProgress) => void): () => void;
|
|
50
|
+
dispose(): void;
|
|
51
|
+
private isTerminal;
|
|
52
|
+
private cleanup;
|
|
53
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Job Manager
|
|
3
|
+
*
|
|
4
|
+
* Tracks server-side analysis jobs with:
|
|
5
|
+
* - In-memory Map storage
|
|
6
|
+
* - Single-slot concurrency (one active job at a time)
|
|
7
|
+
* - Same-repo deduplication (returns existing job)
|
|
8
|
+
* - Progress event emission for SSE relay
|
|
9
|
+
* - 1-hour TTL cleanup for completed/failed jobs
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
const JOB_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
14
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
15
|
+
const JOB_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
16
|
+
export class JobManager {
|
|
17
|
+
jobs = new Map();
|
|
18
|
+
children = new Map();
|
|
19
|
+
timeouts = new Map();
|
|
20
|
+
emitter = new EventEmitter();
|
|
21
|
+
cleanupTimer;
|
|
22
|
+
constructor() {
|
|
23
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
|
|
24
|
+
}
|
|
25
|
+
/** Create a new job, or return existing active job for the same repo. */
|
|
26
|
+
createJob(params) {
|
|
27
|
+
// Dedup: return existing active job for the same repo (by URL or path)
|
|
28
|
+
for (const job of this.jobs.values()) {
|
|
29
|
+
if (!this.isTerminal(job.status)) {
|
|
30
|
+
const isSameRepo = (params.repoUrl && job.repoUrl === params.repoUrl) ||
|
|
31
|
+
(params.repoPath && job.repoPath === params.repoPath);
|
|
32
|
+
if (isSameRepo) {
|
|
33
|
+
return job;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Single-slot: reject if another job is active (different repo)
|
|
38
|
+
for (const job of this.jobs.values()) {
|
|
39
|
+
if (!this.isTerminal(job.status)) {
|
|
40
|
+
throw new Error(`Analysis already in progress (job ${job.id})`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const job = {
|
|
44
|
+
id: randomUUID(),
|
|
45
|
+
status: 'queued',
|
|
46
|
+
repoUrl: params.repoUrl,
|
|
47
|
+
repoPath: params.repoPath,
|
|
48
|
+
progress: { phase: 'queued', percent: 0, message: 'Waiting to start...' },
|
|
49
|
+
startedAt: Date.now(),
|
|
50
|
+
retryCount: 0,
|
|
51
|
+
};
|
|
52
|
+
this.jobs.set(job.id, job);
|
|
53
|
+
return job;
|
|
54
|
+
}
|
|
55
|
+
getJob(id) {
|
|
56
|
+
return this.jobs.get(id);
|
|
57
|
+
}
|
|
58
|
+
updateJob(id, update) {
|
|
59
|
+
const job = this.jobs.get(id);
|
|
60
|
+
if (!job)
|
|
61
|
+
return;
|
|
62
|
+
Object.assign(job, update);
|
|
63
|
+
if (this.isTerminal(job.status)) {
|
|
64
|
+
job.completedAt = job.completedAt ?? Date.now();
|
|
65
|
+
}
|
|
66
|
+
// Emit exactly one event per updateJob call to prevent SSE double-write
|
|
67
|
+
if (update.status === 'complete' || update.status === 'failed') {
|
|
68
|
+
// Terminal event takes precedence — don't also emit the progress event
|
|
69
|
+
this.emitter.emit(`progress:${id}`, {
|
|
70
|
+
phase: update.status,
|
|
71
|
+
percent: update.status === 'complete' ? 100 : job.progress.percent,
|
|
72
|
+
message: update.status === 'complete' ? 'Complete' : update.error || 'Failed',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else if (update.progress) {
|
|
76
|
+
this.emitter.emit(`progress:${id}`, update.progress);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Register a child process for a job — enables cancellation and timeout. */
|
|
80
|
+
registerChild(jobId, child) {
|
|
81
|
+
this.children.set(jobId, child);
|
|
82
|
+
// 30-minute timeout
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
const job = this.jobs.get(jobId);
|
|
85
|
+
if (job && !this.isTerminal(job.status)) {
|
|
86
|
+
this.cancelJob(jobId, 'Analysis timed out (30 minute limit)');
|
|
87
|
+
}
|
|
88
|
+
}, JOB_TIMEOUT_MS);
|
|
89
|
+
this.timeouts.set(jobId, timer);
|
|
90
|
+
// Clean up tracking when child exits
|
|
91
|
+
child.on('exit', () => {
|
|
92
|
+
this.children.delete(jobId);
|
|
93
|
+
const t = this.timeouts.get(jobId);
|
|
94
|
+
if (t) {
|
|
95
|
+
clearTimeout(t);
|
|
96
|
+
this.timeouts.delete(jobId);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Cancel a running job — sends SIGTERM to child process. */
|
|
101
|
+
cancelJob(jobId, reason) {
|
|
102
|
+
const job = this.jobs.get(jobId);
|
|
103
|
+
if (!job || this.isTerminal(job.status))
|
|
104
|
+
return false;
|
|
105
|
+
const child = this.children.get(jobId);
|
|
106
|
+
if (child) {
|
|
107
|
+
child.kill('SIGTERM');
|
|
108
|
+
}
|
|
109
|
+
this.updateJob(jobId, {
|
|
110
|
+
status: 'failed',
|
|
111
|
+
error: reason || 'Analysis cancelled',
|
|
112
|
+
});
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/** Subscribe to progress events for a job. Returns unsubscribe function. */
|
|
116
|
+
onProgress(jobId, listener) {
|
|
117
|
+
const event = `progress:${jobId}`;
|
|
118
|
+
this.emitter.on(event, listener);
|
|
119
|
+
return () => this.emitter.off(event, listener);
|
|
120
|
+
}
|
|
121
|
+
dispose() {
|
|
122
|
+
// Kill all active child processes
|
|
123
|
+
for (const child of this.children.values()) {
|
|
124
|
+
child.kill('SIGTERM');
|
|
125
|
+
}
|
|
126
|
+
this.children.clear();
|
|
127
|
+
// Clear all timeouts
|
|
128
|
+
for (const timer of this.timeouts.values()) {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
}
|
|
131
|
+
this.timeouts.clear();
|
|
132
|
+
clearInterval(this.cleanupTimer);
|
|
133
|
+
this.emitter.removeAllListeners();
|
|
134
|
+
}
|
|
135
|
+
isTerminal(status) {
|
|
136
|
+
return status === 'complete' || status === 'failed';
|
|
137
|
+
}
|
|
138
|
+
cleanup() {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
for (const [id, job] of this.jobs) {
|
|
141
|
+
if (this.isTerminal(job.status) && job.completedAt && now - job.completedAt > JOB_TTL_MS) {
|
|
142
|
+
this.jobs.delete(id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Worker — Forked Child Process
|
|
3
|
+
*
|
|
4
|
+
* This file is the entry point for `child_process.fork()`.
|
|
5
|
+
* It runs runFullAnalysis in an isolated process with 8GB heap.
|
|
6
|
+
*
|
|
7
|
+
* IPC Protocol:
|
|
8
|
+
* Parent -> Child: { type: 'start', repoPath: string, options: AnalyzeOptions }
|
|
9
|
+
* Child -> Parent: { type: 'progress', phase: string, percent: number, message: string }
|
|
10
|
+
* Child -> Parent: { type: 'complete', result: AnalyzeResult }
|
|
11
|
+
* Child -> Parent: { type: 'error', message: string }
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Worker — Forked Child Process
|
|
3
|
+
*
|
|
4
|
+
* This file is the entry point for `child_process.fork()`.
|
|
5
|
+
* It runs runFullAnalysis in an isolated process with 8GB heap.
|
|
6
|
+
*
|
|
7
|
+
* IPC Protocol:
|
|
8
|
+
* Parent -> Child: { type: 'start', repoPath: string, options: AnalyzeOptions }
|
|
9
|
+
* Child -> Parent: { type: 'progress', phase: string, percent: number, message: string }
|
|
10
|
+
* Child -> Parent: { type: 'complete', result: AnalyzeResult }
|
|
11
|
+
* Child -> Parent: { type: 'error', message: string }
|
|
12
|
+
*/
|
|
13
|
+
import { runFullAnalysis } from '../core/run-analyze.js';
|
|
14
|
+
import { closeLbug } from '../core/lbug/lbug-adapter.js';
|
|
15
|
+
function send(msg) {
|
|
16
|
+
process.send?.(msg);
|
|
17
|
+
}
|
|
18
|
+
// Catch uncaught exceptions and unhandled rejections — report to parent
|
|
19
|
+
process.on('uncaughtException', (err) => {
|
|
20
|
+
send({ type: 'error', message: err?.message || 'Uncaught exception in worker' });
|
|
21
|
+
setTimeout(() => process.exit(1), 500);
|
|
22
|
+
});
|
|
23
|
+
process.on('unhandledRejection', (reason) => {
|
|
24
|
+
send({ type: 'error', message: reason?.message || 'Unhandled rejection in worker' });
|
|
25
|
+
setTimeout(() => process.exit(1), 500);
|
|
26
|
+
});
|
|
27
|
+
// Handle graceful shutdown — notify parent before exit
|
|
28
|
+
process.on('SIGTERM', async () => {
|
|
29
|
+
send({ type: 'error', message: 'Analysis cancelled (worker received SIGTERM)' });
|
|
30
|
+
try {
|
|
31
|
+
await closeLbug();
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
process.exit(0);
|
|
35
|
+
});
|
|
36
|
+
// Listen for start command from parent — guarded against re-entry
|
|
37
|
+
let started = false;
|
|
38
|
+
process.on('message', async (msg) => {
|
|
39
|
+
if (msg.type !== 'start' || started)
|
|
40
|
+
return;
|
|
41
|
+
started = true;
|
|
42
|
+
try {
|
|
43
|
+
const result = await runFullAnalysis(msg.repoPath, msg.options, {
|
|
44
|
+
onProgress: (phase, percent, message) => {
|
|
45
|
+
send({ type: 'progress', phase, percent, message });
|
|
46
|
+
},
|
|
47
|
+
onLog: (message) => {
|
|
48
|
+
send({ type: 'progress', phase: 'log', percent: -1, message });
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
send({ type: 'complete', result });
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
send({ type: 'error', message: err?.message || 'Analysis failed' });
|
|
55
|
+
}
|
|
56
|
+
// LadybugDB's native module prevents clean exit — force it
|
|
57
|
+
// (same reason the CLI uses process.exit(0))
|
|
58
|
+
setTimeout(() => process.exit(0), 500);
|
|
59
|
+
});
|