@veewo/gitnexus 1.3.11 → 1.4.7-rc
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 +37 -80
- package/dist/benchmark/agent-context/tool-runner.js +2 -2
- package/dist/benchmark/neonspark-candidates.js +3 -3
- package/dist/benchmark/tool-runner.js +2 -2
- package/dist/cli/ai-context.d.ts +2 -1
- package/dist/cli/ai-context.js +16 -12
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +68 -48
- package/dist/cli/augment.js +1 -1
- package/dist/cli/eval-server.d.ts +8 -1
- package/dist/cli/eval-server.js +30 -13
- package/dist/cli/index.js +28 -82
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/mcp.js +3 -1
- package/dist/cli/setup.js +87 -48
- package/dist/cli/setup.test.js +18 -13
- package/dist/cli/skill-gen.d.ts +26 -0
- package/dist/cli/skill-gen.js +549 -0
- package/dist/cli/status.js +13 -4
- package/dist/cli/tool.d.ts +3 -2
- package/dist/cli/tool.js +50 -16
- package/dist/cli/wiki.js +8 -4
- package/dist/config/ignore-service.d.ts +25 -0
- package/dist/config/ignore-service.js +76 -0
- package/dist/config/supported-languages.d.ts +4 -1
- package/dist/config/supported-languages.js +3 -2
- package/dist/core/augmentation/engine.js +94 -67
- package/dist/core/embeddings/embedder.d.ts +1 -1
- package/dist/core/embeddings/embedder.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
- package/dist/core/embeddings/embedding-pipeline.js +52 -25
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +7 -2
- package/dist/core/ingestion/ast-cache.js +3 -2
- package/dist/core/ingestion/call-processor.d.ts +8 -6
- package/dist/core/ingestion/call-processor.js +468 -206
- package/dist/core/ingestion/call-routing.d.ts +53 -0
- package/dist/core/ingestion/call-routing.js +108 -0
- package/dist/core/ingestion/constants.d.ts +16 -0
- package/dist/core/ingestion/constants.js +16 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
- package/dist/core/ingestion/entry-point-scoring.js +116 -23
- package/dist/core/ingestion/export-detection.d.ts +18 -0
- package/dist/core/ingestion/export-detection.js +231 -0
- package/dist/core/ingestion/filesystem-walker.js +4 -3
- package/dist/core/ingestion/framework-detection.d.ts +19 -4
- package/dist/core/ingestion/framework-detection.js +182 -6
- package/dist/core/ingestion/heritage-processor.d.ts +13 -5
- package/dist/core/ingestion/heritage-processor.js +109 -55
- package/dist/core/ingestion/import-processor.d.ts +16 -20
- package/dist/core/ingestion/import-processor.js +199 -579
- package/dist/core/ingestion/language-config.d.ts +46 -0
- package/dist/core/ingestion/language-config.js +167 -0
- package/dist/core/ingestion/mro-processor.d.ts +45 -0
- package/dist/core/ingestion/mro-processor.js +369 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
- package/dist/core/ingestion/named-binding-extraction.js +363 -0
- package/dist/core/ingestion/parsing-processor.d.ts +4 -1
- package/dist/core/ingestion/parsing-processor.js +107 -109
- package/dist/core/ingestion/pipeline.d.ts +6 -3
- package/dist/core/ingestion/pipeline.js +208 -114
- package/dist/core/ingestion/process-processor.js +8 -2
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
- package/dist/core/ingestion/resolvers/csharp.js +109 -0
- package/dist/core/ingestion/resolvers/go.d.ts +19 -0
- package/dist/core/ingestion/resolvers/go.js +42 -0
- package/dist/core/ingestion/resolvers/index.d.ts +18 -0
- package/dist/core/ingestion/resolvers/index.js +13 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
- package/dist/core/ingestion/resolvers/jvm.js +87 -0
- package/dist/core/ingestion/resolvers/php.d.ts +15 -0
- package/dist/core/ingestion/resolvers/php.js +35 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
- package/dist/core/ingestion/resolvers/rust.js +73 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
- package/dist/core/ingestion/resolvers/standard.js +123 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
- package/dist/core/ingestion/resolvers/utils.js +122 -0
- package/dist/core/ingestion/symbol-table.d.ts +21 -1
- package/dist/core/ingestion/symbol-table.js +40 -12
- package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
- package/dist/core/ingestion/tree-sitter-queries.js +297 -7
- package/dist/core/ingestion/type-env.d.ts +49 -0
- package/dist/core/ingestion/type-env.js +611 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +383 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/go.js +467 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
- package/dist/core/ingestion/type-extractors/index.js +31 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
- package/dist/core/ingestion/type-extractors/jvm.js +681 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/php.js +549 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/python.js +406 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/rust.js +449 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
- package/dist/core/ingestion/type-extractors/shared.js +703 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/swift.js +137 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/typescript.js +494 -0
- package/dist/core/ingestion/utils.d.ts +103 -0
- package/dist/core/ingestion/utils.js +1085 -4
- package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
- package/dist/core/ingestion/workers/parse-worker.js +634 -222
- package/dist/core/ingestion/workers/worker-pool.js +8 -0
- package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
- package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
- package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
- package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
- package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
- package/dist/core/{kuzu → lbug}/schema.js +23 -22
- package/dist/core/lbug/schema.test.d.ts +1 -0
- package/dist/core/search/bm25-index.d.ts +4 -4
- package/dist/core/search/bm25-index.js +12 -11
- package/dist/core/search/hybrid-search.d.ts +2 -2
- package/dist/core/search/hybrid-search.js +6 -6
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
- package/dist/core/tree-sitter/parser-loader.js +19 -0
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +6 -6
- package/dist/core/wiki/graph-queries.d.ts +4 -4
- package/dist/core/wiki/graph-queries.js +7 -7
- package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
- package/dist/mcp/compatible-stdio-transport.js +200 -0
- package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
- package/dist/mcp/core/lbug-adapter.js +327 -0
- package/dist/mcp/local/local-backend.d.ts +21 -16
- package/dist/mcp/local/local-backend.js +306 -706
- package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
- package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
- package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
- package/dist/mcp/resources.js +2 -2
- package/dist/mcp/server.js +28 -13
- package/dist/mcp/staleness.js +2 -2
- package/dist/mcp/tools.js +12 -3
- package/dist/server/api.js +12 -12
- package/dist/server/mcp-http.d.ts +1 -1
- package/dist/server/mcp-http.js +1 -1
- package/dist/storage/git.js +4 -1
- package/dist/storage/repo-manager.d.ts +20 -2
- package/dist/storage/repo-manager.js +74 -4
- package/dist/types/pipeline.d.ts +1 -1
- package/hooks/claude/gitnexus-hook.cjs +149 -46
- package/hooks/claude/pre-tool-use.sh +2 -1
- package/hooks/claude/session-start.sh +0 -0
- package/package.json +20 -4
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/skills/gitnexus-cli.md +8 -8
- package/skills/gitnexus-debugging.md +1 -1
- package/skills/gitnexus-exploring.md +1 -1
- package/skills/gitnexus-guide.md +1 -1
- package/skills/gitnexus-impact-analysis.md +1 -1
- package/skills/gitnexus-pr-review.md +163 -0
- package/skills/gitnexus-refactoring.md +1 -1
- package/dist/cli/claude-hooks.d.ts +0 -22
- package/dist/cli/claude-hooks.js +0 -97
- package/dist/mcp/core/kuzu-adapter.js +0 -231
- /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
- /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
- /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
- /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
- /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
|
@@ -3,39 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides tool implementations using local .gitnexus/ indexes.
|
|
5
5
|
* Supports multiple indexed repositories via a global registry.
|
|
6
|
-
*
|
|
6
|
+
* LadybugDB connections are opened lazily per repo on first query.
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import {
|
|
11
|
-
import { parseUnityHydrationMode, parseUnityResourcesMode } from '../../core/unity/options.js';
|
|
12
|
-
import { buildUnityScanContext, buildUnityScanContextFromSeed } from '../../core/unity/scan-context.js';
|
|
13
|
-
import { resolveUnityBindings } from '../../core/unity/resolver.js';
|
|
14
|
-
import { formatLazyHydrationBudgetDiagnostic, loadUnityContext, } from './unity-enrichment.js';
|
|
15
|
-
import { resolveUnityLazyConfig } from './unity-lazy-config.js';
|
|
16
|
-
import { hydrateLazyBindings } from './unity-lazy-hydrator.js';
|
|
17
|
-
import { readUnityOverlayBindings, upsertUnityOverlayBindings } from './unity-lazy-overlay.js';
|
|
18
|
-
import { readUnityParityCache, upsertUnityParityCache } from './unity-parity-cache.js';
|
|
19
|
-
import { createParityWarmupQueue } from './unity-parity-warmup-queue.js';
|
|
20
|
-
import { loadUnityParitySeed } from './unity-parity-seed-loader.js';
|
|
10
|
+
import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady } from '../core/lbug-adapter.js';
|
|
21
11
|
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
22
12
|
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
|
23
13
|
// git utilities available if needed
|
|
24
14
|
// import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
|
|
25
|
-
import { listRegisteredRepos, } from '../../storage/repo-manager.js';
|
|
15
|
+
import { listRegisteredRepos, cleanupOldKuzuFiles, } from '../../storage/repo-manager.js';
|
|
26
16
|
// AI context generation is CLI-only (gitnexus analyze)
|
|
27
17
|
// import { generateAIContextFiles } from '../../cli/ai-context.js';
|
|
28
18
|
/**
|
|
29
19
|
* Quick test-file detection for filtering impact results.
|
|
30
20
|
* Matches common test file patterns across all supported languages.
|
|
31
21
|
*/
|
|
32
|
-
function isTestFilePath(filePath) {
|
|
22
|
+
export function isTestFilePath(filePath) {
|
|
33
23
|
const p = filePath.toLowerCase().replace(/\\/g, '/');
|
|
34
24
|
return (p.includes('.test.') || p.includes('.spec.') ||
|
|
35
25
|
p.includes('__tests__/') || p.includes('__mocks__/') ||
|
|
36
26
|
p.includes('/test/') || p.includes('/tests/') ||
|
|
37
27
|
p.includes('/testing/') || p.includes('/fixtures/') ||
|
|
38
28
|
p.endsWith('_test.go') || p.endsWith('_test.py') ||
|
|
29
|
+
p.endsWith('_spec.rb') || p.endsWith('_test.rb') || p.includes('/spec/') ||
|
|
39
30
|
p.includes('/test_') || p.includes('/conftest.'));
|
|
40
31
|
}
|
|
41
32
|
function normalizePath(filePath) {
|
|
@@ -93,7 +84,7 @@ export function attachUnityHydrationMeta(payload, input) {
|
|
|
93
84
|
if (hasExpandableBindings) {
|
|
94
85
|
reasons.push('lightweight_bindings_remaining');
|
|
95
86
|
}
|
|
96
|
-
if (payload.unityDiagnostics.some((diag) => /budget exceeded/i.test(String(diag || '')))) {
|
|
87
|
+
if ((payload.unityDiagnostics || []).some((diag) => /budget exceeded/i.test(String(diag || '')))) {
|
|
97
88
|
reasons.push('budget_exceeded');
|
|
98
89
|
}
|
|
99
90
|
const isComplete = reasons.length === 0;
|
|
@@ -111,40 +102,25 @@ export function attachUnityHydrationMeta(payload, input) {
|
|
|
111
102
|
},
|
|
112
103
|
};
|
|
113
104
|
}
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
maxParallel: resolveParityWarmupMaxParallel(process.env),
|
|
117
|
-
});
|
|
118
|
-
function resolveParityWarmupMaxParallel(env) {
|
|
119
|
-
const raw = String(env.GITNEXUS_UNITY_PARITY_WARMUP_MAX_PARALLEL || '').trim();
|
|
120
|
-
const parsed = Number.parseInt(raw, 10);
|
|
121
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
122
|
-
return parsed;
|
|
123
|
-
}
|
|
124
|
-
return 2;
|
|
125
|
-
}
|
|
126
|
-
/** Valid KuzuDB node labels for safe Cypher query construction */
|
|
127
|
-
const VALID_NODE_LABELS = new Set([
|
|
105
|
+
/** Valid LadybugDB node labels for safe Cypher query construction */
|
|
106
|
+
export const VALID_NODE_LABELS = new Set([
|
|
128
107
|
'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement',
|
|
129
108
|
'Community', 'Process', 'Struct', 'Enum', 'Macro', 'Typedef', 'Union',
|
|
130
109
|
'Namespace', 'Trait', 'Impl', 'TypeAlias', 'Const', 'Static', 'Property',
|
|
131
110
|
'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module',
|
|
132
111
|
]);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return
|
|
112
|
+
/** Valid relation types for impact analysis filtering */
|
|
113
|
+
export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES']);
|
|
114
|
+
/** Regex to detect write operations in user-supplied Cypher queries */
|
|
115
|
+
export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
|
|
116
|
+
/** Check if a Cypher query contains write operations */
|
|
117
|
+
export function isWriteQuery(query) {
|
|
118
|
+
return CYPHER_WRITE_RE.test(query);
|
|
140
119
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (named !== undefined)
|
|
146
|
-
return named;
|
|
147
|
-
return row[index];
|
|
120
|
+
/** Structured error logging for query failures — replaces empty catch blocks */
|
|
121
|
+
function logQueryError(context, err) {
|
|
122
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
123
|
+
console.error(`GitNexus [${context}]: ${msg}`);
|
|
148
124
|
}
|
|
149
125
|
export class LocalBackend {
|
|
150
126
|
repos = new Map();
|
|
@@ -162,7 +138,7 @@ export class LocalBackend {
|
|
|
162
138
|
/**
|
|
163
139
|
* Re-read the global registry and update the in-memory repo map.
|
|
164
140
|
* New repos are added, existing repos are updated, removed repos are pruned.
|
|
165
|
-
*
|
|
141
|
+
* LadybugDB connections for removed repos are NOT closed (they idle-timeout naturally).
|
|
166
142
|
*/
|
|
167
143
|
async refreshRepos() {
|
|
168
144
|
const entries = await listRegisteredRepos({ validate: true });
|
|
@@ -171,19 +147,25 @@ export class LocalBackend {
|
|
|
171
147
|
const id = this.repoId(entry.name, entry.path);
|
|
172
148
|
freshIds.add(id);
|
|
173
149
|
const storagePath = entry.storagePath;
|
|
174
|
-
const
|
|
150
|
+
const lbugPath = path.join(storagePath, 'lbug');
|
|
151
|
+
// Clean up any leftover KuzuDB files from before the LadybugDB migration.
|
|
152
|
+
// If kuzu exists but lbug doesn't, warn so the user knows to re-analyze.
|
|
153
|
+
const kuzu = await cleanupOldKuzuFiles(storagePath);
|
|
154
|
+
if (kuzu.found && kuzu.needsReindex) {
|
|
155
|
+
console.error(`GitNexus: "${entry.name}" has a stale KuzuDB index. Run: gitnexus analyze ${entry.path}`);
|
|
156
|
+
}
|
|
175
157
|
const handle = {
|
|
176
158
|
id,
|
|
177
159
|
name: entry.name,
|
|
178
160
|
repoPath: entry.path,
|
|
179
161
|
storagePath,
|
|
180
|
-
|
|
162
|
+
lbugPath,
|
|
181
163
|
indexedAt: entry.indexedAt,
|
|
182
164
|
lastCommit: entry.lastCommit,
|
|
183
165
|
stats: entry.stats,
|
|
184
166
|
};
|
|
185
167
|
this.repos.set(id, handle);
|
|
186
|
-
// Build lightweight context (no
|
|
168
|
+
// Build lightweight context (no LadybugDB needed)
|
|
187
169
|
const s = entry.stats || {};
|
|
188
170
|
this.contextCache.set(id, {
|
|
189
171
|
projectName: entry.name,
|
|
@@ -284,16 +266,16 @@ export class LocalBackend {
|
|
|
284
266
|
}
|
|
285
267
|
return null; // Multiple repos, no param — ambiguous
|
|
286
268
|
}
|
|
287
|
-
// ─── Lazy
|
|
269
|
+
// ─── Lazy LadybugDB Init ────────────────────────────────────────────
|
|
288
270
|
async ensureInitialized(repoId) {
|
|
289
271
|
// Always check the actual pool — the idle timer may have evicted the connection
|
|
290
|
-
if (this.initializedRepos.has(repoId) &&
|
|
272
|
+
if (this.initializedRepos.has(repoId) && isLbugReady(repoId))
|
|
291
273
|
return;
|
|
292
274
|
const handle = this.repos.get(repoId);
|
|
293
275
|
if (!handle)
|
|
294
276
|
throw new Error(`Unknown repo: ${repoId}`);
|
|
295
277
|
try {
|
|
296
|
-
await
|
|
278
|
+
await initLbug(repoId, handle.lbugPath);
|
|
297
279
|
this.initializedRepos.add(repoId);
|
|
298
280
|
}
|
|
299
281
|
catch (err) {
|
|
@@ -380,8 +362,6 @@ export class LocalBackend {
|
|
|
380
362
|
const processLimit = params.limit || 5;
|
|
381
363
|
const maxSymbolsPerProcess = params.max_symbols || 10;
|
|
382
364
|
const includeContent = params.include_content ?? false;
|
|
383
|
-
const unityResourcesMode = parseUnityResourcesMode(params.unity_resources);
|
|
384
|
-
const unityHydrationMode = parseUnityHydrationMode(params.unity_hydration_mode);
|
|
385
365
|
const searchQuery = params.query.trim();
|
|
386
366
|
// Step 1: Run hybrid search to get matching symbols
|
|
387
367
|
const searchLimit = processLimit * maxSymbolsPerProcess; // fetch enough raw results
|
|
@@ -432,44 +412,49 @@ export class LocalBackend {
|
|
|
432
412
|
});
|
|
433
413
|
continue;
|
|
434
414
|
}
|
|
435
|
-
const escaped = sym.nodeId.replace(/'/g, "''");
|
|
436
415
|
// Find processes this symbol participates in
|
|
437
416
|
let processRows = [];
|
|
438
417
|
try {
|
|
439
|
-
processRows = await
|
|
440
|
-
MATCH (n {id:
|
|
418
|
+
processRows = await executeParameterized(repo.id, `
|
|
419
|
+
MATCH (n {id: $nodeId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
441
420
|
RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
442
|
-
|
|
421
|
+
`, { nodeId: sym.nodeId });
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
logQueryError('query:process-lookup', e);
|
|
443
425
|
}
|
|
444
|
-
catch { /* symbol might not be in any process */ }
|
|
445
426
|
// Get cluster membership + cohesion (cohesion used as internal ranking signal)
|
|
446
427
|
let cohesion = 0;
|
|
447
428
|
let module;
|
|
448
429
|
try {
|
|
449
|
-
const cohesionRows = await
|
|
450
|
-
MATCH (n {id:
|
|
430
|
+
const cohesionRows = await executeParameterized(repo.id, `
|
|
431
|
+
MATCH (n {id: $nodeId})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
451
432
|
RETURN c.cohesion AS cohesion, c.heuristicLabel AS module
|
|
452
433
|
LIMIT 1
|
|
453
|
-
|
|
434
|
+
`, { nodeId: sym.nodeId });
|
|
454
435
|
if (cohesionRows.length > 0) {
|
|
455
436
|
cohesion = (cohesionRows[0].cohesion ?? cohesionRows[0][0]) || 0;
|
|
456
437
|
module = cohesionRows[0].module ?? cohesionRows[0][1];
|
|
457
438
|
}
|
|
458
439
|
}
|
|
459
|
-
catch {
|
|
440
|
+
catch (e) {
|
|
441
|
+
logQueryError('query:cluster-info', e);
|
|
442
|
+
}
|
|
460
443
|
// Optionally fetch content
|
|
461
444
|
let content;
|
|
462
445
|
if (includeContent) {
|
|
463
446
|
try {
|
|
464
|
-
const contentRows = await
|
|
465
|
-
MATCH (n {id:
|
|
447
|
+
const contentRows = await executeParameterized(repo.id, `
|
|
448
|
+
MATCH (n {id: $nodeId})
|
|
466
449
|
RETURN n.content AS content
|
|
467
|
-
|
|
450
|
+
`, { nodeId: sym.nodeId });
|
|
468
451
|
if (contentRows.length > 0) {
|
|
469
452
|
content = contentRows[0].content ?? contentRows[0][0];
|
|
470
453
|
}
|
|
471
454
|
}
|
|
472
|
-
catch {
|
|
455
|
+
catch (e) {
|
|
456
|
+
logQueryError('query:content-fetch', e);
|
|
457
|
+
}
|
|
473
458
|
}
|
|
474
459
|
const symbolEntry = {
|
|
475
460
|
id: sym.nodeId,
|
|
@@ -480,15 +465,6 @@ export class LocalBackend {
|
|
|
480
465
|
endLine: sym.endLine,
|
|
481
466
|
...(module ? { module } : {}),
|
|
482
467
|
...(includeContent && content ? { content } : {}),
|
|
483
|
-
...((unityResourcesMode !== 'off' && sym.nodeId && sym.type === 'Class')
|
|
484
|
-
? await this.hydrateUnityContext(repo, {
|
|
485
|
-
symbolUid: sym.nodeId,
|
|
486
|
-
symbolName: sym.name || '',
|
|
487
|
-
symbolFilePath: sym.filePath || '',
|
|
488
|
-
payload: await loadUnityContext(repo.id, sym.nodeId, (query) => executeQuery(repo.id, query)),
|
|
489
|
-
hydrationMode: unityHydrationMode,
|
|
490
|
-
})
|
|
491
|
-
: {}),
|
|
492
468
|
};
|
|
493
469
|
if (processRows.length === 0) {
|
|
494
470
|
// Symbol not in any process — goes to definitions
|
|
@@ -562,117 +538,62 @@ export class LocalBackend {
|
|
|
562
538
|
};
|
|
563
539
|
}
|
|
564
540
|
/**
|
|
565
|
-
* BM25 keyword search helper - uses
|
|
541
|
+
* BM25 keyword search helper - uses LadybugDB FTS for always-fresh results
|
|
566
542
|
*/
|
|
567
543
|
async bm25Search(repo, query, limit) {
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
{ table: 'Method', index: 'method_fts', type: 'Method' },
|
|
573
|
-
{ table: 'Interface', index: 'interface_fts', type: 'Interface' },
|
|
574
|
-
];
|
|
575
|
-
const symbolHits = [];
|
|
576
|
-
for (const spec of symbolTables) {
|
|
577
|
-
try {
|
|
578
|
-
const rows = await executeQuery(repo.id, `
|
|
579
|
-
CALL QUERY_FTS_INDEX('${spec.table}', '${spec.index}', '${escapedQuery}', conjunctive := false)
|
|
580
|
-
RETURN node.id AS id, node.name AS name, node.filePath AS filePath, node.startLine AS startLine, node.endLine AS endLine, score AS score
|
|
581
|
-
ORDER BY score DESC
|
|
582
|
-
LIMIT ${limit}
|
|
583
|
-
`);
|
|
584
|
-
for (const row of rows) {
|
|
585
|
-
const nodeId = getRowValue(row, 'id', 0);
|
|
586
|
-
if (!nodeId)
|
|
587
|
-
continue;
|
|
588
|
-
symbolHits.push({
|
|
589
|
-
nodeId,
|
|
590
|
-
name: getRowValue(row, 'name', 1) || '',
|
|
591
|
-
type: spec.type,
|
|
592
|
-
filePath: getRowValue(row, 'filePath', 2) || '',
|
|
593
|
-
startLine: getRowValue(row, 'startLine', 3),
|
|
594
|
-
endLine: getRowValue(row, 'endLine', 4),
|
|
595
|
-
bm25Score: Number(getRowValue(row, 'score', 5) ?? 0),
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
catch {
|
|
600
|
-
// Missing FTS index for this table is expected in some repos/languages.
|
|
601
|
-
}
|
|
544
|
+
const { searchFTSFromLbug } = await import('../../core/search/bm25-index.js');
|
|
545
|
+
let bm25Results;
|
|
546
|
+
try {
|
|
547
|
+
bm25Results = await searchFTSFromLbug(query, limit, repo.id);
|
|
602
548
|
}
|
|
603
|
-
|
|
604
|
-
.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
for (const token of meaningfulTokens) {
|
|
549
|
+
catch (err) {
|
|
550
|
+
console.error('GitNexus: BM25/FTS search failed (FTS indexes may not exist) -', err.message);
|
|
551
|
+
return [];
|
|
552
|
+
}
|
|
553
|
+
const results = [];
|
|
554
|
+
for (const bm25Result of bm25Results) {
|
|
555
|
+
const fullPath = bm25Result.filePath;
|
|
611
556
|
try {
|
|
612
|
-
const
|
|
557
|
+
const symbols = await executeParameterized(repo.id, `
|
|
613
558
|
MATCH (n)
|
|
614
|
-
WHERE
|
|
615
|
-
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
616
|
-
LIMIT
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
559
|
+
WHERE n.filePath = $filePath
|
|
560
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
561
|
+
LIMIT 3
|
|
562
|
+
`, { filePath: fullPath });
|
|
563
|
+
if (symbols.length > 0) {
|
|
564
|
+
for (const sym of symbols) {
|
|
565
|
+
results.push({
|
|
566
|
+
nodeId: sym.id || sym[0],
|
|
567
|
+
name: sym.name || sym[1],
|
|
568
|
+
type: sym.type || sym[2],
|
|
569
|
+
filePath: sym.filePath || sym[3],
|
|
570
|
+
startLine: sym.startLine || sym[4],
|
|
571
|
+
endLine: sym.endLine || sym[5],
|
|
572
|
+
bm25Score: bm25Result.score,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
const fileName = fullPath.split('/').pop() || fullPath;
|
|
578
|
+
results.push({
|
|
579
|
+
name: fileName,
|
|
580
|
+
type: 'File',
|
|
581
|
+
filePath: bm25Result.filePath,
|
|
582
|
+
bm25Score: bm25Result.score,
|
|
630
583
|
});
|
|
631
584
|
}
|
|
632
585
|
}
|
|
633
586
|
catch {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
if (symbolHits.length > 0) {
|
|
638
|
-
const filteredByName = meaningfulTokens.length > 0
|
|
639
|
-
? symbolHits.filter(hit => meaningfulTokens.some(t => (hit.name || '').toLowerCase().includes(t)))
|
|
640
|
-
: symbolHits;
|
|
641
|
-
const candidateHits = filteredByName.length > 0 ? filteredByName : symbolHits;
|
|
642
|
-
const deduped = new Map();
|
|
643
|
-
for (const hit of candidateHits) {
|
|
644
|
-
const existing = deduped.get(hit.nodeId);
|
|
645
|
-
if (!existing || hit.bm25Score > existing.bm25Score) {
|
|
646
|
-
deduped.set(hit.nodeId, hit);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return Array.from(deduped.values())
|
|
650
|
-
.sort((a, b) => b.bm25Score - a.bm25Score)
|
|
651
|
-
.slice(0, limit);
|
|
652
|
-
}
|
|
653
|
-
// Fallback to file-level FTS when symbol indexes are missing/empty.
|
|
654
|
-
try {
|
|
655
|
-
const fileRows = await executeQuery(repo.id, `
|
|
656
|
-
CALL QUERY_FTS_INDEX('File', 'file_fts', '${escapedQuery}', conjunctive := false)
|
|
657
|
-
RETURN node.filePath AS filePath, score AS score
|
|
658
|
-
ORDER BY score DESC
|
|
659
|
-
LIMIT ${limit}
|
|
660
|
-
`);
|
|
661
|
-
return fileRows.map((row) => {
|
|
662
|
-
const filePath = getRowValue(row, 'filePath', 0) || '';
|
|
663
|
-
const fileName = filePath.split('/').pop() || filePath;
|
|
664
|
-
return {
|
|
587
|
+
const fileName = fullPath.split('/').pop() || fullPath;
|
|
588
|
+
results.push({
|
|
665
589
|
name: fileName,
|
|
666
590
|
type: 'File',
|
|
667
|
-
filePath,
|
|
668
|
-
bm25Score:
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
catch (err) {
|
|
673
|
-
console.error('GitNexus: BM25/FTS search failed (FTS indexes may not exist) -', err.message);
|
|
674
|
-
return [];
|
|
591
|
+
filePath: bm25Result.filePath,
|
|
592
|
+
bm25Score: bm25Result.score,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
675
595
|
}
|
|
596
|
+
return results;
|
|
676
597
|
}
|
|
677
598
|
/**
|
|
678
599
|
* Semantic vector search helper
|
|
@@ -709,11 +630,10 @@ export class LocalBackend {
|
|
|
709
630
|
if (!VALID_NODE_LABELS.has(label))
|
|
710
631
|
continue;
|
|
711
632
|
try {
|
|
712
|
-
const escapedId = nodeId.replace(/'/g, "''");
|
|
713
633
|
const nodeQuery = label === 'File'
|
|
714
|
-
? `MATCH (n:File {id:
|
|
715
|
-
: `MATCH (n:\`${label}\` {id:
|
|
716
|
-
const nodeRows = await
|
|
634
|
+
? `MATCH (n:File {id: $nodeId}) RETURN n.name AS name, n.filePath AS filePath`
|
|
635
|
+
: `MATCH (n:\`${label}\` {id: $nodeId}) RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine`;
|
|
636
|
+
const nodeRows = await executeParameterized(repo.id, nodeQuery, { nodeId });
|
|
717
637
|
if (nodeRows.length > 0) {
|
|
718
638
|
const nodeRow = nodeRows[0];
|
|
719
639
|
results.push({
|
|
@@ -742,8 +662,12 @@ export class LocalBackend {
|
|
|
742
662
|
}
|
|
743
663
|
async cypher(repo, params) {
|
|
744
664
|
await this.ensureInitialized(repo.id);
|
|
745
|
-
if (!
|
|
746
|
-
return { error: '
|
|
665
|
+
if (!isLbugReady(repo.id)) {
|
|
666
|
+
return { error: 'LadybugDB not ready. Index may be corrupted.' };
|
|
667
|
+
}
|
|
668
|
+
// Block write operations (defense-in-depth — DB is already read-only)
|
|
669
|
+
if (CYPHER_WRITE_RE.test(params.query)) {
|
|
670
|
+
return { error: 'Write operations (CREATE, DELETE, SET, MERGE, REMOVE, DROP, ALTER, COPY, DETACH) are not allowed. The knowledge graph is read-only.' };
|
|
747
671
|
}
|
|
748
672
|
try {
|
|
749
673
|
const result = await executeQuery(repo.id, params.query);
|
|
@@ -760,29 +684,7 @@ export class LocalBackend {
|
|
|
760
684
|
formatCypherAsMarkdown(result) {
|
|
761
685
|
if (!Array.isArray(result) || result.length === 0)
|
|
762
686
|
return result;
|
|
763
|
-
const
|
|
764
|
-
if (!row || typeof row !== 'object')
|
|
765
|
-
return row;
|
|
766
|
-
const next = { ...row };
|
|
767
|
-
if (Object.prototype.hasOwnProperty.call(next, 'type')) {
|
|
768
|
-
const typeValue = next.type;
|
|
769
|
-
if (typeValue === null || typeValue === undefined || (typeof typeValue === 'string' && typeValue.trim() === '')) {
|
|
770
|
-
const inferred = inferTypeFromId(next.id ?? next.uid ?? next.sourceId ?? next.targetId);
|
|
771
|
-
if (inferred)
|
|
772
|
-
next.type = inferred;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
if (Object.prototype.hasOwnProperty.call(next, 'kind')) {
|
|
776
|
-
const kindValue = next.kind;
|
|
777
|
-
if (kindValue === null || kindValue === undefined || (typeof kindValue === 'string' && kindValue.trim() === '')) {
|
|
778
|
-
const inferred = inferTypeFromId(next.id ?? next.uid ?? next.sourceId ?? next.targetId);
|
|
779
|
-
if (inferred)
|
|
780
|
-
next.kind = inferred;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
return next;
|
|
784
|
-
});
|
|
785
|
-
const firstRow = normalizedRows[0];
|
|
687
|
+
const firstRow = result[0];
|
|
786
688
|
if (typeof firstRow !== 'object' || firstRow === null)
|
|
787
689
|
return result;
|
|
788
690
|
const keys = Object.keys(firstRow);
|
|
@@ -790,7 +692,7 @@ export class LocalBackend {
|
|
|
790
692
|
return result;
|
|
791
693
|
const header = '| ' + keys.join(' | ') + ' |';
|
|
792
694
|
const separator = '| ' + keys.map(() => '---').join(' | ') + ' |';
|
|
793
|
-
const dataRows =
|
|
695
|
+
const dataRows = result.map((row) => '| ' + keys.map(k => {
|
|
794
696
|
const v = row[k];
|
|
795
697
|
if (v === null || v === undefined)
|
|
796
698
|
return '';
|
|
@@ -800,13 +702,13 @@ export class LocalBackend {
|
|
|
800
702
|
}).join(' | ') + ' |');
|
|
801
703
|
return {
|
|
802
704
|
markdown: [header, separator, ...dataRows].join('\n'),
|
|
803
|
-
row_count:
|
|
705
|
+
row_count: result.length,
|
|
804
706
|
};
|
|
805
707
|
}
|
|
806
708
|
/**
|
|
807
709
|
* Aggregate same-named clusters: group by heuristicLabel, sum symbols,
|
|
808
710
|
* weighted-average cohesion, filter out tiny clusters (<5 symbols).
|
|
809
|
-
* Raw communities stay intact in
|
|
711
|
+
* Raw communities stay intact in LadybugDB for Cypher queries.
|
|
810
712
|
*/
|
|
811
713
|
aggregateClusters(clusters) {
|
|
812
714
|
const groups = new Map();
|
|
@@ -902,40 +804,39 @@ export class LocalBackend {
|
|
|
902
804
|
async context(repo, params) {
|
|
903
805
|
await this.ensureInitialized(repo.id);
|
|
904
806
|
const { name, uid, file_path, include_content } = params;
|
|
905
|
-
const unityResourcesMode = parseUnityResourcesMode(params.unity_resources);
|
|
906
|
-
const unityHydrationMode = parseUnityHydrationMode(params.unity_hydration_mode);
|
|
907
807
|
if (!name && !uid) {
|
|
908
808
|
return { error: 'Either "name" or "uid" parameter is required.' };
|
|
909
809
|
}
|
|
910
810
|
// Step 1: Find the symbol
|
|
911
811
|
let symbols;
|
|
912
812
|
if (uid) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
|
|
813
|
+
symbols = await executeParameterized(repo.id, `
|
|
814
|
+
MATCH (n {id: $uid})
|
|
815
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
|
|
917
816
|
LIMIT 1
|
|
918
|
-
|
|
817
|
+
`, { uid });
|
|
919
818
|
}
|
|
920
819
|
else {
|
|
921
|
-
const escaped = name.replace(/'/g, "''");
|
|
922
820
|
const isQualified = name.includes('/') || name.includes(':');
|
|
923
821
|
let whereClause;
|
|
822
|
+
let queryParams;
|
|
924
823
|
if (file_path) {
|
|
925
|
-
|
|
926
|
-
|
|
824
|
+
whereClause = `WHERE n.name = $symName AND n.filePath CONTAINS $filePath`;
|
|
825
|
+
queryParams = { symName: name, filePath: file_path };
|
|
927
826
|
}
|
|
928
827
|
else if (isQualified) {
|
|
929
|
-
whereClause = `WHERE n.id =
|
|
828
|
+
whereClause = `WHERE n.id = $symName OR n.name = $symName`;
|
|
829
|
+
queryParams = { symName: name };
|
|
930
830
|
}
|
|
931
831
|
else {
|
|
932
|
-
whereClause = `WHERE n.name =
|
|
832
|
+
whereClause = `WHERE n.name = $symName`;
|
|
833
|
+
queryParams = { symName: name };
|
|
933
834
|
}
|
|
934
|
-
symbols = await
|
|
835
|
+
symbols = await executeParameterized(repo.id, `
|
|
935
836
|
MATCH (n) ${whereClause}
|
|
936
|
-
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
|
|
837
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
|
|
937
838
|
LIMIT 10
|
|
938
|
-
|
|
839
|
+
`, queryParams);
|
|
939
840
|
}
|
|
940
841
|
if (symbols.length === 0) {
|
|
941
842
|
return { error: `Symbol '${name || uid}' not found` };
|
|
@@ -946,109 +847,52 @@ export class LocalBackend {
|
|
|
946
847
|
status: 'ambiguous',
|
|
947
848
|
message: `Found ${symbols.length} symbols matching '${name}'. Use uid or file_path to disambiguate.`,
|
|
948
849
|
candidates: symbols.map((s) => ({
|
|
949
|
-
uid:
|
|
950
|
-
name:
|
|
951
|
-
kind:
|
|
952
|
-
filePath:
|
|
953
|
-
line:
|
|
850
|
+
uid: s.id || s[0],
|
|
851
|
+
name: s.name || s[1],
|
|
852
|
+
kind: s.type || s[2],
|
|
853
|
+
filePath: s.filePath || s[3],
|
|
854
|
+
line: s.startLine || s[4],
|
|
954
855
|
})),
|
|
955
856
|
};
|
|
956
857
|
}
|
|
957
858
|
// Step 3: Build full context
|
|
958
859
|
const sym = symbols[0];
|
|
959
|
-
const
|
|
960
|
-
const symId = symNodeId.replace(/'/g, "''");
|
|
961
|
-
const symKind = inferTypeFromId(symNodeId) || 'CodeElement';
|
|
962
|
-
const symFilePath = getRowValue(sym, 'filePath', 2) || '';
|
|
860
|
+
const symId = sym.id || sym[0];
|
|
963
861
|
// Categorized incoming refs
|
|
964
|
-
|
|
965
|
-
MATCH (caller)-[r:CodeRelation]->(n {id:
|
|
862
|
+
const incomingRows = await executeParameterized(repo.id, `
|
|
863
|
+
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
|
|
966
864
|
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
967
|
-
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath
|
|
865
|
+
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
968
866
|
LIMIT 30
|
|
969
|
-
|
|
867
|
+
`, { symId });
|
|
970
868
|
// Categorized outgoing refs
|
|
971
|
-
|
|
972
|
-
MATCH (n {id:
|
|
869
|
+
const outgoingRows = await executeParameterized(repo.id, `
|
|
870
|
+
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
|
|
973
871
|
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
974
|
-
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath
|
|
872
|
+
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
|
975
873
|
LIMIT 30
|
|
976
|
-
|
|
874
|
+
`, { symId });
|
|
977
875
|
// Process participation
|
|
978
876
|
let processRows = [];
|
|
979
877
|
try {
|
|
980
|
-
processRows = await
|
|
981
|
-
MATCH (n {id:
|
|
878
|
+
processRows = await executeParameterized(repo.id, `
|
|
879
|
+
MATCH (n {id: $symId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
982
880
|
RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
|
983
|
-
|
|
881
|
+
`, { symId });
|
|
984
882
|
}
|
|
985
|
-
catch {
|
|
986
|
-
|
|
987
|
-
// When direct edges are empty, fall back to file-scoped references/processes.
|
|
988
|
-
if ((symKind === 'Class' || symKind === 'Interface') && symFilePath) {
|
|
989
|
-
const escapedPath = symFilePath.replace(/'/g, "''");
|
|
990
|
-
if (incomingRows.length === 0) {
|
|
991
|
-
try {
|
|
992
|
-
incomingRows = await executeQuery(repo.id, `
|
|
993
|
-
MATCH (caller)-[r:CodeRelation]->(n)
|
|
994
|
-
WHERE n.filePath = '${escapedPath}' AND r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
995
|
-
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath
|
|
996
|
-
LIMIT 30
|
|
997
|
-
`);
|
|
998
|
-
}
|
|
999
|
-
catch { /* ignore fallback failures */ }
|
|
1000
|
-
}
|
|
1001
|
-
if (outgoingRows.length === 0) {
|
|
1002
|
-
try {
|
|
1003
|
-
outgoingRows = await executeQuery(repo.id, `
|
|
1004
|
-
MATCH (n)-[r:CodeRelation]->(target)
|
|
1005
|
-
WHERE n.filePath = '${escapedPath}' AND r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
1006
|
-
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath
|
|
1007
|
-
LIMIT 30
|
|
1008
|
-
`);
|
|
1009
|
-
}
|
|
1010
|
-
catch { /* ignore fallback failures */ }
|
|
1011
|
-
}
|
|
1012
|
-
if (processRows.length === 0) {
|
|
1013
|
-
try {
|
|
1014
|
-
const scopedProcessRows = await executeQuery(repo.id, `
|
|
1015
|
-
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
1016
|
-
WHERE n.filePath = '${escapedPath}'
|
|
1017
|
-
RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
|
1018
|
-
LIMIT 200
|
|
1019
|
-
`);
|
|
1020
|
-
const minStepByProcess = new Map();
|
|
1021
|
-
for (const row of scopedProcessRows) {
|
|
1022
|
-
const pid = getRowValue(row, 'pid', 0);
|
|
1023
|
-
if (!pid)
|
|
1024
|
-
continue;
|
|
1025
|
-
const step = Number(getRowValue(row, 'step', 2) ?? Number.MAX_SAFE_INTEGER);
|
|
1026
|
-
const existing = minStepByProcess.get(pid);
|
|
1027
|
-
if (!existing || step < existing.step) {
|
|
1028
|
-
minStepByProcess.set(pid, {
|
|
1029
|
-
pid,
|
|
1030
|
-
label: getRowValue(row, 'label', 1),
|
|
1031
|
-
step,
|
|
1032
|
-
stepCount: getRowValue(row, 'stepCount', 3),
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
processRows = Array.from(minStepByProcess.values());
|
|
1037
|
-
}
|
|
1038
|
-
catch { /* ignore fallback failures */ }
|
|
1039
|
-
}
|
|
883
|
+
catch (e) {
|
|
884
|
+
logQueryError('context:process-participation', e);
|
|
1040
885
|
}
|
|
1041
886
|
// Helper to categorize refs
|
|
1042
887
|
const categorize = (rows) => {
|
|
1043
888
|
const cats = {};
|
|
1044
889
|
for (const row of rows) {
|
|
1045
|
-
const relType =
|
|
1046
|
-
const uid = getRowValue(row, 'uid', 1);
|
|
890
|
+
const relType = (row.relType || row[0] || '').toLowerCase();
|
|
1047
891
|
const entry = {
|
|
1048
|
-
uid,
|
|
1049
|
-
name:
|
|
1050
|
-
filePath:
|
|
1051
|
-
kind:
|
|
892
|
+
uid: row.uid || row[1],
|
|
893
|
+
name: row.name || row[2],
|
|
894
|
+
filePath: row.filePath || row[3],
|
|
895
|
+
kind: row.kind || row[4],
|
|
1052
896
|
};
|
|
1053
897
|
if (!cats[relType])
|
|
1054
898
|
cats[relType] = [];
|
|
@@ -1056,38 +900,26 @@ export class LocalBackend {
|
|
|
1056
900
|
}
|
|
1057
901
|
return cats;
|
|
1058
902
|
};
|
|
1059
|
-
|
|
903
|
+
return {
|
|
1060
904
|
status: 'found',
|
|
1061
905
|
symbol: {
|
|
1062
|
-
uid:
|
|
1063
|
-
name:
|
|
1064
|
-
kind:
|
|
1065
|
-
filePath:
|
|
1066
|
-
startLine:
|
|
1067
|
-
endLine:
|
|
1068
|
-
...(include_content &&
|
|
906
|
+
uid: sym.id || sym[0],
|
|
907
|
+
name: sym.name || sym[1],
|
|
908
|
+
kind: sym.type || sym[2],
|
|
909
|
+
filePath: sym.filePath || sym[3],
|
|
910
|
+
startLine: sym.startLine || sym[4],
|
|
911
|
+
endLine: sym.endLine || sym[5],
|
|
912
|
+
...(include_content && (sym.content || sym[6]) ? { content: sym.content || sym[6] } : {}),
|
|
1069
913
|
},
|
|
1070
914
|
incoming: categorize(incomingRows),
|
|
1071
915
|
outgoing: categorize(outgoingRows),
|
|
1072
916
|
processes: processRows.map((r) => ({
|
|
1073
|
-
id:
|
|
1074
|
-
name:
|
|
1075
|
-
step_index:
|
|
1076
|
-
step_count:
|
|
917
|
+
id: r.pid || r[0],
|
|
918
|
+
name: r.label || r[1],
|
|
919
|
+
step_index: r.step || r[2],
|
|
920
|
+
step_count: r.stepCount || r[3],
|
|
1077
921
|
})),
|
|
1078
922
|
};
|
|
1079
|
-
if (unityResourcesMode !== 'off' && symNodeId && symKind === 'Class') {
|
|
1080
|
-
const unityContext = await loadUnityContext(repo.id, symNodeId, (query) => executeQuery(repo.id, query));
|
|
1081
|
-
const hydratedUnityContext = await this.hydrateUnityContext(repo, {
|
|
1082
|
-
symbolUid: symNodeId,
|
|
1083
|
-
symbolName: getRowValue(sym, 'name', 1) || '',
|
|
1084
|
-
symbolFilePath: symFilePath,
|
|
1085
|
-
payload: unityContext,
|
|
1086
|
-
hydrationMode: unityHydrationMode,
|
|
1087
|
-
});
|
|
1088
|
-
Object.assign(result, hydratedUnityContext);
|
|
1089
|
-
}
|
|
1090
|
-
return result;
|
|
1091
923
|
}
|
|
1092
924
|
/**
|
|
1093
925
|
* Legacy explore — kept for backwards compatibility with resources.ts.
|
|
@@ -1100,13 +932,11 @@ export class LocalBackend {
|
|
|
1100
932
|
return this.context(repo, { name });
|
|
1101
933
|
}
|
|
1102
934
|
if (type === 'cluster') {
|
|
1103
|
-
const
|
|
1104
|
-
const clusterQuery = `
|
|
935
|
+
const clusters = await executeParameterized(repo.id, `
|
|
1105
936
|
MATCH (c:Community)
|
|
1106
|
-
WHERE c.label =
|
|
937
|
+
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
|
|
1107
938
|
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1108
|
-
|
|
1109
|
-
const clusters = await executeQuery(repo.id, clusterQuery);
|
|
939
|
+
`, { clusterName: name });
|
|
1110
940
|
if (clusters.length === 0)
|
|
1111
941
|
return { error: `Cluster '${name}' not found` };
|
|
1112
942
|
const rawClusters = clusters.map((c) => ({
|
|
@@ -1119,12 +949,12 @@ export class LocalBackend {
|
|
|
1119
949
|
totalSymbols += s;
|
|
1120
950
|
weightedCohesion += (c.cohesion || 0) * s;
|
|
1121
951
|
}
|
|
1122
|
-
const members = await
|
|
952
|
+
const members = await executeParameterized(repo.id, `
|
|
1123
953
|
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
1124
|
-
WHERE c.label =
|
|
1125
|
-
RETURN DISTINCT n.
|
|
954
|
+
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
|
|
955
|
+
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1126
956
|
LIMIT 30
|
|
1127
|
-
|
|
957
|
+
`, { clusterName: name });
|
|
1128
958
|
return {
|
|
1129
959
|
cluster: {
|
|
1130
960
|
id: rawClusters[0].id,
|
|
@@ -1135,250 +965,38 @@ export class LocalBackend {
|
|
|
1135
965
|
subCommunities: rawClusters.length,
|
|
1136
966
|
},
|
|
1137
967
|
members: members.map((m) => ({
|
|
1138
|
-
name:
|
|
1139
|
-
type: inferTypeFromId(getRowValue(m, 'id', 0)) || 'CodeElement',
|
|
1140
|
-
filePath: getRowValue(m, 'filePath', 2),
|
|
968
|
+
name: m.name || m[0], type: m.type || m[1], filePath: m.filePath || m[2],
|
|
1141
969
|
})),
|
|
1142
970
|
};
|
|
1143
971
|
}
|
|
1144
972
|
if (type === 'process') {
|
|
1145
|
-
const processes = await
|
|
973
|
+
const processes = await executeParameterized(repo.id, `
|
|
1146
974
|
MATCH (p:Process)
|
|
1147
|
-
WHERE p.label =
|
|
975
|
+
WHERE p.label = $processName OR p.heuristicLabel = $processName
|
|
1148
976
|
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1149
977
|
LIMIT 1
|
|
1150
|
-
|
|
978
|
+
`, { processName: name });
|
|
1151
979
|
if (processes.length === 0)
|
|
1152
980
|
return { error: `Process '${name}' not found` };
|
|
1153
981
|
const proc = processes[0];
|
|
1154
982
|
const procId = proc.id || proc[0];
|
|
1155
|
-
const steps = await
|
|
1156
|
-
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id:
|
|
1157
|
-
RETURN n.
|
|
983
|
+
const steps = await executeParameterized(repo.id, `
|
|
984
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
|
|
985
|
+
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
1158
986
|
ORDER BY r.step
|
|
1159
|
-
|
|
987
|
+
`, { procId });
|
|
1160
988
|
return {
|
|
1161
989
|
process: {
|
|
1162
990
|
id: procId, label: proc.label || proc[1], heuristicLabel: proc.heuristicLabel || proc[2],
|
|
1163
991
|
processType: proc.processType || proc[3], stepCount: proc.stepCount || proc[4],
|
|
1164
992
|
},
|
|
1165
993
|
steps: steps.map((s) => ({
|
|
1166
|
-
step:
|
|
1167
|
-
name: getRowValue(s, 'name', 1),
|
|
1168
|
-
type: inferTypeFromId(getRowValue(s, 'id', 0)) || 'CodeElement',
|
|
1169
|
-
filePath: getRowValue(s, 'filePath', 2),
|
|
994
|
+
step: s.step || s[3], name: s.name || s[0], type: s.type || s[1], filePath: s.filePath || s[2],
|
|
1170
995
|
})),
|
|
1171
996
|
};
|
|
1172
997
|
}
|
|
1173
998
|
return { error: 'Invalid type. Use: symbol, cluster, or process' };
|
|
1174
999
|
}
|
|
1175
|
-
async hydrateUnityContext(repo, input) {
|
|
1176
|
-
const startedAt = Date.now();
|
|
1177
|
-
if (input.hydrationMode === 'compact') {
|
|
1178
|
-
const compactPayload = await this.hydrateUnityContextCompact(repo, input);
|
|
1179
|
-
const withMeta = attachUnityHydrationMeta(compactPayload, {
|
|
1180
|
-
requestedMode: 'compact',
|
|
1181
|
-
effectiveMode: 'compact',
|
|
1182
|
-
elapsedMs: Date.now() - startedAt,
|
|
1183
|
-
fallbackToCompact: false,
|
|
1184
|
-
hasExpandableBindings: compactPayload.resourceBindings.some((binding) => binding.lightweight || binding.componentObjectId === 'summary'),
|
|
1185
|
-
});
|
|
1186
|
-
if (withMeta.hydrationMeta?.needsParityRetry) {
|
|
1187
|
-
this.scheduleParityWarmup(repo, input);
|
|
1188
|
-
}
|
|
1189
|
-
return withMeta;
|
|
1190
|
-
}
|
|
1191
|
-
const parityResult = await this.hydrateUnityContextParity(repo, input);
|
|
1192
|
-
return attachUnityHydrationMeta(parityResult.payload, {
|
|
1193
|
-
requestedMode: 'parity',
|
|
1194
|
-
effectiveMode: parityResult.effectiveMode,
|
|
1195
|
-
elapsedMs: Date.now() - startedAt,
|
|
1196
|
-
fallbackToCompact: parityResult.fallbackToCompact,
|
|
1197
|
-
hasExpandableBindings: parityResult.payload.resourceBindings.some((binding) => binding.lightweight || binding.componentObjectId === 'summary'),
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
buildParityWarmupKey(repo, symbolUid) {
|
|
1201
|
-
return `${repo.storagePath}::${repo.lastCommit}::${symbolUid}`;
|
|
1202
|
-
}
|
|
1203
|
-
scheduleParityWarmup(repo, input) {
|
|
1204
|
-
if (!this.shouldEnableParityWarmup()) {
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
if (!input.symbolUid || !input.symbolName || !input.symbolFilePath) {
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
void parityWarmupQueue.run(() => this.getOrRunParityHydration(repo, input))
|
|
1211
|
-
.then(() => undefined)
|
|
1212
|
-
.catch(() => undefined);
|
|
1213
|
-
}
|
|
1214
|
-
shouldEnableParityWarmup() {
|
|
1215
|
-
const raw = String(process.env.GITNEXUS_UNITY_PARITY_WARMUP || '').trim().toLowerCase();
|
|
1216
|
-
return raw === '1' || raw === 'true' || raw === 'on';
|
|
1217
|
-
}
|
|
1218
|
-
async getOrRunParityHydration(repo, input) {
|
|
1219
|
-
const key = this.buildParityWarmupKey(repo, input.symbolUid);
|
|
1220
|
-
const existing = inFlightParityHydration.get(key);
|
|
1221
|
-
if (existing) {
|
|
1222
|
-
return existing;
|
|
1223
|
-
}
|
|
1224
|
-
const pending = (async () => {
|
|
1225
|
-
const cached = await readUnityParityCache(repo.storagePath, repo.lastCommit, input.symbolUid);
|
|
1226
|
-
if (cached) {
|
|
1227
|
-
return cached;
|
|
1228
|
-
}
|
|
1229
|
-
const payload = await this.computeParityPayload(repo, input);
|
|
1230
|
-
await upsertUnityParityCache(repo.storagePath, repo.lastCommit, input.symbolUid, payload);
|
|
1231
|
-
return payload;
|
|
1232
|
-
})().finally(() => {
|
|
1233
|
-
inFlightParityHydration.delete(key);
|
|
1234
|
-
});
|
|
1235
|
-
inFlightParityHydration.set(key, pending);
|
|
1236
|
-
return pending;
|
|
1237
|
-
}
|
|
1238
|
-
async computeParityPayload(repo, input) {
|
|
1239
|
-
const symbolDeclarations = [{ symbol: input.symbolName, scriptPath: input.symbolFilePath }];
|
|
1240
|
-
const paritySeed = await loadUnityParitySeed(repo.storagePath);
|
|
1241
|
-
const seededScanContext = paritySeed
|
|
1242
|
-
? buildUnityScanContextFromSeed({
|
|
1243
|
-
seed: paritySeed,
|
|
1244
|
-
symbolDeclarations,
|
|
1245
|
-
})
|
|
1246
|
-
: null;
|
|
1247
|
-
let resolved = await resolveUnityBindings({
|
|
1248
|
-
repoRoot: repo.repoPath,
|
|
1249
|
-
symbol: input.symbolName,
|
|
1250
|
-
scanContext: seededScanContext || await buildUnityScanContext({
|
|
1251
|
-
repoRoot: repo.repoPath,
|
|
1252
|
-
symbolDeclarations,
|
|
1253
|
-
}),
|
|
1254
|
-
deepParseLargeResources: true,
|
|
1255
|
-
});
|
|
1256
|
-
if (seededScanContext
|
|
1257
|
-
&& resolved.resourceBindings.length === 0
|
|
1258
|
-
&& input.payload.resourceBindings.length > 0) {
|
|
1259
|
-
const fallbackScanContext = await buildUnityScanContext({
|
|
1260
|
-
repoRoot: repo.repoPath,
|
|
1261
|
-
symbolDeclarations,
|
|
1262
|
-
});
|
|
1263
|
-
resolved = await resolveUnityBindings({
|
|
1264
|
-
repoRoot: repo.repoPath,
|
|
1265
|
-
symbol: input.symbolName,
|
|
1266
|
-
scanContext: fallbackScanContext,
|
|
1267
|
-
deepParseLargeResources: true,
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1270
|
-
if (resolved.resourceBindings.length === 0 && input.payload.resourceBindings.length > 0) {
|
|
1271
|
-
throw new Error('parity-expand returned zero bindings');
|
|
1272
|
-
}
|
|
1273
|
-
const baseNonLightweight = input.payload.resourceBindings.filter((binding) => !binding.lightweight && binding.componentObjectId !== 'summary');
|
|
1274
|
-
const mergedBindings = mergeParityUnityBindings(baseNonLightweight, resolved.resourceBindings);
|
|
1275
|
-
return this.toUnityContextPayload(mergedBindings, [
|
|
1276
|
-
...input.payload.unityDiagnostics,
|
|
1277
|
-
...resolved.unityDiagnostics,
|
|
1278
|
-
]);
|
|
1279
|
-
}
|
|
1280
|
-
async hydrateUnityContextParity(repo, input) {
|
|
1281
|
-
try {
|
|
1282
|
-
return {
|
|
1283
|
-
payload: await this.getOrRunParityHydration(repo, input),
|
|
1284
|
-
effectiveMode: 'parity',
|
|
1285
|
-
fallbackToCompact: false,
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
catch (error) {
|
|
1289
|
-
const compactFallback = await this.hydrateUnityContextCompact(repo, input);
|
|
1290
|
-
const message = String(error instanceof Error ? error.message : error);
|
|
1291
|
-
return {
|
|
1292
|
-
payload: {
|
|
1293
|
-
...compactFallback,
|
|
1294
|
-
unityDiagnostics: [
|
|
1295
|
-
...compactFallback.unityDiagnostics,
|
|
1296
|
-
/parity-expand returned zero bindings/i.test(message)
|
|
1297
|
-
? 'parity-expand returned zero bindings; fell back to compact hydration'
|
|
1298
|
-
: `parity-expand failed: ${message}`,
|
|
1299
|
-
],
|
|
1300
|
-
},
|
|
1301
|
-
effectiveMode: 'compact',
|
|
1302
|
-
fallbackToCompact: true,
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
async hydrateUnityContextCompact(repo, input) {
|
|
1307
|
-
const lightweightPaths = [...new Set(input.payload.resourceBindings
|
|
1308
|
-
.filter((binding) => binding.lightweight || binding.componentObjectId === 'summary')
|
|
1309
|
-
.map((binding) => normalizePath(binding.resourcePath))
|
|
1310
|
-
.filter((value) => value.length > 0))];
|
|
1311
|
-
if (lightweightPaths.length === 0) {
|
|
1312
|
-
return input.payload;
|
|
1313
|
-
}
|
|
1314
|
-
const overlayHits = await readUnityOverlayBindings(repo.storagePath, repo.lastCommit, input.symbolUid, lightweightPaths);
|
|
1315
|
-
const pendingPaths = lightweightPaths.filter((resourcePath) => !overlayHits.has(resourcePath));
|
|
1316
|
-
const resolvedByPath = new Map(overlayHits);
|
|
1317
|
-
const unityDiagnostics = [...input.payload.unityDiagnostics];
|
|
1318
|
-
if (pendingPaths.length > 0) {
|
|
1319
|
-
try {
|
|
1320
|
-
const cfg = resolveUnityLazyConfig(process.env);
|
|
1321
|
-
const hydration = await hydrateLazyBindings({
|
|
1322
|
-
pendingPaths,
|
|
1323
|
-
config: cfg,
|
|
1324
|
-
dedupeKey: `${input.symbolUid}::${pendingPaths.slice().sort().join('|')}`,
|
|
1325
|
-
resolveBatch: async (resourcePaths) => {
|
|
1326
|
-
const scopedPaths = [
|
|
1327
|
-
input.symbolFilePath,
|
|
1328
|
-
`${input.symbolFilePath}.meta`,
|
|
1329
|
-
...resourcePaths,
|
|
1330
|
-
...resourcePaths.map((resourcePath) => `${resourcePath}.meta`),
|
|
1331
|
-
].map(normalizePath);
|
|
1332
|
-
const scanContext = await buildUnityScanContext({
|
|
1333
|
-
repoRoot: repo.repoPath,
|
|
1334
|
-
scopedPaths,
|
|
1335
|
-
symbolDeclarations: [{ symbol: input.symbolName, scriptPath: input.symbolFilePath }],
|
|
1336
|
-
});
|
|
1337
|
-
const resolved = await resolveUnityBindings({
|
|
1338
|
-
repoRoot: repo.repoPath,
|
|
1339
|
-
symbol: input.symbolName,
|
|
1340
|
-
scanContext,
|
|
1341
|
-
resourcePathAllowlist: resourcePaths,
|
|
1342
|
-
deepParseLargeResources: true,
|
|
1343
|
-
});
|
|
1344
|
-
unityDiagnostics.push(...resolved.unityDiagnostics);
|
|
1345
|
-
const byPath = new Map();
|
|
1346
|
-
for (const resourcePath of resourcePaths) {
|
|
1347
|
-
byPath.set(resourcePath, resolved.resourceBindings.filter((binding) => normalizePath(binding.resourcePath) === normalizePath(resourcePath)));
|
|
1348
|
-
}
|
|
1349
|
-
return byPath;
|
|
1350
|
-
},
|
|
1351
|
-
});
|
|
1352
|
-
const freshByPath = hydration.resolvedByPath;
|
|
1353
|
-
if (hydration.timedOut) {
|
|
1354
|
-
unityDiagnostics.push(formatLazyHydrationBudgetDiagnostic(hydration.elapsedMs));
|
|
1355
|
-
}
|
|
1356
|
-
const hydrationExtras = hydration.diagnostics.filter((diag) => !/budget exceeded/i.test(diag));
|
|
1357
|
-
if (hydrationExtras.length > 0) {
|
|
1358
|
-
unityDiagnostics.push(...hydrationExtras);
|
|
1359
|
-
}
|
|
1360
|
-
await upsertUnityOverlayBindings(repo.storagePath, repo.lastCommit, input.symbolUid, freshByPath);
|
|
1361
|
-
for (const [resourcePath, bindings] of freshByPath.entries()) {
|
|
1362
|
-
resolvedByPath.set(resourcePath, bindings);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
catch (error) {
|
|
1366
|
-
unityDiagnostics.push(`lazy-expand failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
const mergedBindings = mergeUnityBindings(input.payload.resourceBindings, resolvedByPath);
|
|
1370
|
-
return this.toUnityContextPayload(mergedBindings, unityDiagnostics);
|
|
1371
|
-
}
|
|
1372
|
-
toUnityContextPayload(resourceBindings, unityDiagnostics) {
|
|
1373
|
-
return {
|
|
1374
|
-
resourceBindings,
|
|
1375
|
-
serializedFields: {
|
|
1376
|
-
scalarFields: resourceBindings.flatMap((binding) => binding.serializedFields.scalarFields),
|
|
1377
|
-
referenceFields: resourceBindings.flatMap((binding) => binding.serializedFields.referenceFields),
|
|
1378
|
-
},
|
|
1379
|
-
unityDiagnostics,
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
1000
|
/**
|
|
1383
1001
|
* Detect changes — git-diff based impact analysis.
|
|
1384
1002
|
* Maps changed lines to indexed symbols, then finds affected processes.
|
|
@@ -1386,29 +1004,29 @@ export class LocalBackend {
|
|
|
1386
1004
|
async detectChanges(repo, params) {
|
|
1387
1005
|
await this.ensureInitialized(repo.id);
|
|
1388
1006
|
const scope = params.scope || 'unstaged';
|
|
1389
|
-
const {
|
|
1390
|
-
// Build git diff
|
|
1391
|
-
let
|
|
1007
|
+
const { execFileSync } = await import('child_process');
|
|
1008
|
+
// Build git diff args based on scope (using execFileSync to avoid shell injection)
|
|
1009
|
+
let diffArgs;
|
|
1392
1010
|
switch (scope) {
|
|
1393
1011
|
case 'staged':
|
|
1394
|
-
|
|
1012
|
+
diffArgs = ['diff', '--staged', '--name-only'];
|
|
1395
1013
|
break;
|
|
1396
1014
|
case 'all':
|
|
1397
|
-
|
|
1015
|
+
diffArgs = ['diff', 'HEAD', '--name-only'];
|
|
1398
1016
|
break;
|
|
1399
1017
|
case 'compare':
|
|
1400
1018
|
if (!params.base_ref)
|
|
1401
1019
|
return { error: 'base_ref is required for "compare" scope' };
|
|
1402
|
-
|
|
1020
|
+
diffArgs = ['diff', params.base_ref, '--name-only'];
|
|
1403
1021
|
break;
|
|
1404
1022
|
case 'unstaged':
|
|
1405
1023
|
default:
|
|
1406
|
-
|
|
1024
|
+
diffArgs = ['diff', '--name-only'];
|
|
1407
1025
|
break;
|
|
1408
1026
|
}
|
|
1409
1027
|
let changedFiles;
|
|
1410
1028
|
try {
|
|
1411
|
-
const output =
|
|
1029
|
+
const output = execFileSync('git', diffArgs, { cwd: repo.repoPath, encoding: 'utf-8' });
|
|
1412
1030
|
changedFiles = output.trim().split('\n').filter(f => f.length > 0);
|
|
1413
1031
|
}
|
|
1414
1032
|
catch (err) {
|
|
@@ -1424,35 +1042,35 @@ export class LocalBackend {
|
|
|
1424
1042
|
// Map changed files to indexed symbols
|
|
1425
1043
|
const changedSymbols = [];
|
|
1426
1044
|
for (const file of changedFiles) {
|
|
1427
|
-
const
|
|
1045
|
+
const normalizedFile = file.replace(/\\/g, '/');
|
|
1428
1046
|
try {
|
|
1429
|
-
const symbols = await
|
|
1430
|
-
MATCH (n) WHERE n.filePath CONTAINS
|
|
1431
|
-
RETURN n.id AS id, n.name AS name, n.filePath AS filePath
|
|
1047
|
+
const symbols = await executeParameterized(repo.id, `
|
|
1048
|
+
MATCH (n) WHERE n.filePath CONTAINS $filePath
|
|
1049
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1432
1050
|
LIMIT 20
|
|
1433
|
-
|
|
1051
|
+
`, { filePath: normalizedFile });
|
|
1434
1052
|
for (const sym of symbols) {
|
|
1435
|
-
const id = getRowValue(sym, 'id', 0) || '';
|
|
1436
1053
|
changedSymbols.push({
|
|
1437
|
-
id,
|
|
1438
|
-
name:
|
|
1439
|
-
type:
|
|
1440
|
-
filePath:
|
|
1054
|
+
id: sym.id || sym[0],
|
|
1055
|
+
name: sym.name || sym[1],
|
|
1056
|
+
type: sym.type || sym[2],
|
|
1057
|
+
filePath: sym.filePath || sym[3],
|
|
1441
1058
|
change_type: 'Modified',
|
|
1442
1059
|
});
|
|
1443
1060
|
}
|
|
1444
1061
|
}
|
|
1445
|
-
catch {
|
|
1062
|
+
catch (e) {
|
|
1063
|
+
logQueryError('detect-changes:file-symbols', e);
|
|
1064
|
+
}
|
|
1446
1065
|
}
|
|
1447
1066
|
// Find affected processes
|
|
1448
1067
|
const affectedProcesses = new Map();
|
|
1449
1068
|
for (const sym of changedSymbols) {
|
|
1450
|
-
const escaped = sym.id.replace(/'/g, "''");
|
|
1451
1069
|
try {
|
|
1452
|
-
const procs = await
|
|
1453
|
-
MATCH (n {id:
|
|
1070
|
+
const procs = await executeParameterized(repo.id, `
|
|
1071
|
+
MATCH (n {id: $nodeId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
1454
1072
|
RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
1455
|
-
|
|
1073
|
+
`, { nodeId: sym.id });
|
|
1456
1074
|
for (const proc of procs) {
|
|
1457
1075
|
const pid = proc.pid || proc[0];
|
|
1458
1076
|
if (!affectedProcesses.has(pid)) {
|
|
@@ -1470,7 +1088,9 @@ export class LocalBackend {
|
|
|
1470
1088
|
});
|
|
1471
1089
|
}
|
|
1472
1090
|
}
|
|
1473
|
-
catch {
|
|
1091
|
+
catch (e) {
|
|
1092
|
+
logQueryError('detect-changes:process-lookup', e);
|
|
1093
|
+
}
|
|
1474
1094
|
}
|
|
1475
1095
|
const processCount = affectedProcesses.size;
|
|
1476
1096
|
const risk = processCount === 0 ? 'low' : processCount <= 5 ? 'medium' : processCount <= 15 ? 'high' : 'critical';
|
|
@@ -1497,6 +1117,14 @@ export class LocalBackend {
|
|
|
1497
1117
|
if (!params.symbol_name && !params.symbol_uid) {
|
|
1498
1118
|
return { error: 'Either symbol_name or symbol_uid is required.' };
|
|
1499
1119
|
}
|
|
1120
|
+
/** Guard: ensure a file path resolves within the repo root (prevents path traversal) */
|
|
1121
|
+
const assertSafePath = (filePath) => {
|
|
1122
|
+
const full = path.resolve(repo.repoPath, filePath);
|
|
1123
|
+
if (!full.startsWith(repo.repoPath + path.sep) && full !== repo.repoPath) {
|
|
1124
|
+
throw new Error(`Path traversal blocked: ${filePath}`);
|
|
1125
|
+
}
|
|
1126
|
+
return full;
|
|
1127
|
+
};
|
|
1500
1128
|
// Step 1: Find the target symbol (reuse context's lookup)
|
|
1501
1129
|
const lookupResult = await this.context(repo, {
|
|
1502
1130
|
name: params.symbol_name,
|
|
@@ -1525,14 +1153,17 @@ export class LocalBackend {
|
|
|
1525
1153
|
// The definition itself
|
|
1526
1154
|
if (sym.filePath && sym.startLine) {
|
|
1527
1155
|
try {
|
|
1528
|
-
const content = await fs.readFile(
|
|
1156
|
+
const content = await fs.readFile(assertSafePath(sym.filePath), 'utf-8');
|
|
1529
1157
|
const lines = content.split('\n');
|
|
1530
1158
|
const lineIdx = sym.startLine - 1;
|
|
1531
1159
|
if (lineIdx >= 0 && lineIdx < lines.length && lines[lineIdx].includes(oldName)) {
|
|
1532
|
-
|
|
1160
|
+
const defRegex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
1161
|
+
addEdit(sym.filePath, sym.startLine, lines[lineIdx].trim(), lines[lineIdx].replace(defRegex, new_name).trim(), 'graph');
|
|
1533
1162
|
}
|
|
1534
1163
|
}
|
|
1535
|
-
catch {
|
|
1164
|
+
catch (e) {
|
|
1165
|
+
logQueryError('rename:read-definition', e);
|
|
1166
|
+
}
|
|
1536
1167
|
}
|
|
1537
1168
|
// All incoming refs from graph (callers, importers, etc.)
|
|
1538
1169
|
const allIncoming = [
|
|
@@ -1546,7 +1177,7 @@ export class LocalBackend {
|
|
|
1546
1177
|
if (!ref.filePath)
|
|
1547
1178
|
continue;
|
|
1548
1179
|
try {
|
|
1549
|
-
const content = await fs.readFile(
|
|
1180
|
+
const content = await fs.readFile(assertSafePath(ref.filePath), 'utf-8');
|
|
1550
1181
|
const lines = content.split('\n');
|
|
1551
1182
|
for (let i = 0; i < lines.length; i++) {
|
|
1552
1183
|
if (lines[i].includes(oldName)) {
|
|
@@ -1556,37 +1187,50 @@ export class LocalBackend {
|
|
|
1556
1187
|
}
|
|
1557
1188
|
}
|
|
1558
1189
|
}
|
|
1559
|
-
catch {
|
|
1190
|
+
catch (e) {
|
|
1191
|
+
logQueryError('rename:read-ref', e);
|
|
1192
|
+
}
|
|
1560
1193
|
}
|
|
1561
1194
|
// Step 3: Text search for refs the graph might have missed
|
|
1562
1195
|
let astSearchEdits = 0;
|
|
1563
1196
|
const graphFiles = new Set([sym.filePath, ...allIncoming.map(r => r.filePath)].filter(Boolean));
|
|
1564
1197
|
// Simple text search across the repo for the old name (in files not already covered by graph)
|
|
1565
1198
|
try {
|
|
1566
|
-
const {
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1199
|
+
const { execFileSync } = await import('child_process');
|
|
1200
|
+
const rgArgs = [
|
|
1201
|
+
'-l',
|
|
1202
|
+
'--type-add', 'code:*.{ts,tsx,js,jsx,py,go,rs,java,c,h,cpp,cc,cxx,hpp,hxx,hh,cs,php,swift}',
|
|
1203
|
+
'-t', 'code',
|
|
1204
|
+
`\\b${oldName}\\b`,
|
|
1205
|
+
'.',
|
|
1206
|
+
];
|
|
1207
|
+
const output = execFileSync('rg', rgArgs, { cwd: repo.repoPath, encoding: 'utf-8', timeout: 5000 });
|
|
1569
1208
|
const files = output.trim().split('\n').filter(f => f.length > 0);
|
|
1570
1209
|
for (const file of files) {
|
|
1571
1210
|
const normalizedFile = file.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
1572
1211
|
if (graphFiles.has(normalizedFile))
|
|
1573
1212
|
continue; // already covered by graph
|
|
1574
1213
|
try {
|
|
1575
|
-
const content = await fs.readFile(
|
|
1214
|
+
const content = await fs.readFile(assertSafePath(normalizedFile), 'utf-8');
|
|
1576
1215
|
const lines = content.split('\n');
|
|
1577
1216
|
const regex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
1578
1217
|
for (let i = 0; i < lines.length; i++) {
|
|
1218
|
+
regex.lastIndex = 0;
|
|
1579
1219
|
if (regex.test(lines[i])) {
|
|
1220
|
+
regex.lastIndex = 0;
|
|
1580
1221
|
addEdit(normalizedFile, i + 1, lines[i].trim(), lines[i].replace(regex, new_name).trim(), 'text_search');
|
|
1581
1222
|
astSearchEdits++;
|
|
1582
|
-
regex.lastIndex = 0; // reset regex
|
|
1583
1223
|
}
|
|
1584
1224
|
}
|
|
1585
1225
|
}
|
|
1586
|
-
catch {
|
|
1226
|
+
catch (e) {
|
|
1227
|
+
logQueryError('rename:text-search-read', e);
|
|
1228
|
+
}
|
|
1587
1229
|
}
|
|
1588
1230
|
}
|
|
1589
|
-
catch {
|
|
1231
|
+
catch (e) {
|
|
1232
|
+
logQueryError('rename:ripgrep', e);
|
|
1233
|
+
}
|
|
1590
1234
|
// Step 4: Apply or preview
|
|
1591
1235
|
const allChanges = Array.from(changes.values());
|
|
1592
1236
|
const totalEdits = allChanges.reduce((sum, c) => sum + c.edits.length, 0);
|
|
@@ -1594,13 +1238,15 @@ export class LocalBackend {
|
|
|
1594
1238
|
// Apply edits to files
|
|
1595
1239
|
for (const change of allChanges) {
|
|
1596
1240
|
try {
|
|
1597
|
-
const fullPath =
|
|
1241
|
+
const fullPath = assertSafePath(change.file_path);
|
|
1598
1242
|
let content = await fs.readFile(fullPath, 'utf-8');
|
|
1599
1243
|
const regex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
1600
1244
|
content = content.replace(regex, new_name);
|
|
1601
1245
|
await fs.writeFile(fullPath, content, 'utf-8');
|
|
1602
1246
|
}
|
|
1603
|
-
catch {
|
|
1247
|
+
catch (e) {
|
|
1248
|
+
logQueryError('rename:apply-edit', e);
|
|
1249
|
+
}
|
|
1604
1250
|
}
|
|
1605
1251
|
}
|
|
1606
1252
|
return {
|
|
@@ -1616,122 +1262,83 @@ export class LocalBackend {
|
|
|
1616
1262
|
};
|
|
1617
1263
|
}
|
|
1618
1264
|
async impact(repo, params) {
|
|
1265
|
+
try {
|
|
1266
|
+
return await this._impactImpl(repo, params);
|
|
1267
|
+
}
|
|
1268
|
+
catch (err) {
|
|
1269
|
+
// Return structured error instead of crashing (#321)
|
|
1270
|
+
return {
|
|
1271
|
+
error: (err instanceof Error ? err.message : String(err)) || 'Impact analysis failed',
|
|
1272
|
+
target: { name: params.target },
|
|
1273
|
+
direction: params.direction,
|
|
1274
|
+
impactedCount: 0,
|
|
1275
|
+
risk: 'UNKNOWN',
|
|
1276
|
+
suggestion: 'The graph query failed — try gitnexus context <symbol> as a fallback',
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async _impactImpl(repo, params) {
|
|
1619
1281
|
await this.ensureInitialized(repo.id);
|
|
1620
1282
|
const { target, direction } = params;
|
|
1621
|
-
const maxDepth = params.maxDepth
|
|
1622
|
-
const
|
|
1623
|
-
? params.relationTypes
|
|
1283
|
+
const maxDepth = params.maxDepth || 3;
|
|
1284
|
+
const rawRelTypes = params.relationTypes && params.relationTypes.length > 0
|
|
1285
|
+
? params.relationTypes.filter(t => VALID_RELATION_TYPES.has(t))
|
|
1624
1286
|
: ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
|
1287
|
+
const relationTypes = rawRelTypes.length > 0 ? rawRelTypes : ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
|
1625
1288
|
const includeTests = params.includeTests ?? false;
|
|
1626
|
-
const minConfidence = params.minConfidence ?? 0
|
|
1289
|
+
const minConfidence = params.minConfidence ?? 0;
|
|
1627
1290
|
const relTypeFilter = relationTypes.map(t => `'${t}'`).join(', ');
|
|
1628
1291
|
const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
LIMIT 1
|
|
1636
|
-
`);
|
|
1637
|
-
}
|
|
1638
|
-
else {
|
|
1639
|
-
const escapedTarget = target.replace(/'/g, "''");
|
|
1640
|
-
const fileFilter = params.file_path?.trim()
|
|
1641
|
-
? ` AND n.filePath CONTAINS '${params.file_path.trim().replace(/'/g, "''")}'`
|
|
1642
|
-
: '';
|
|
1643
|
-
targets = await executeQuery(repo.id, `
|
|
1644
|
-
MATCH (n)
|
|
1645
|
-
WHERE n.name = '${escapedTarget}'${fileFilter}
|
|
1646
|
-
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine
|
|
1647
|
-
LIMIT 20
|
|
1648
|
-
`);
|
|
1649
|
-
}
|
|
1292
|
+
const targets = await executeParameterized(repo.id, `
|
|
1293
|
+
MATCH (n)
|
|
1294
|
+
WHERE n.name = $targetName
|
|
1295
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1296
|
+
LIMIT 1
|
|
1297
|
+
`, { targetName: target });
|
|
1650
1298
|
if (targets.length === 0)
|
|
1651
1299
|
return { error: `Target '${target}' not found` };
|
|
1652
|
-
if (targets.length > 1 && !params.target_uid) {
|
|
1653
|
-
return {
|
|
1654
|
-
status: 'ambiguous',
|
|
1655
|
-
message: `Found ${targets.length} symbols matching '${target}'. Use target_uid or file_path to disambiguate.`,
|
|
1656
|
-
candidates: targets.map((s) => ({
|
|
1657
|
-
uid: getRowValue(s, 'id', 0),
|
|
1658
|
-
name: getRowValue(s, 'name', 1),
|
|
1659
|
-
kind: inferTypeFromId(getRowValue(s, 'id', 0)) || 'CodeElement',
|
|
1660
|
-
filePath: getRowValue(s, 'filePath', 2),
|
|
1661
|
-
line: getRowValue(s, 'startLine', 3),
|
|
1662
|
-
})),
|
|
1663
|
-
};
|
|
1664
|
-
}
|
|
1665
1300
|
const sym = targets[0];
|
|
1666
|
-
const symId =
|
|
1667
|
-
const symType = inferTypeFromId(symId) || 'CodeElement';
|
|
1668
|
-
let seedIds = [symId];
|
|
1669
|
-
// Class/interface references are frequently attached to member symbols.
|
|
1670
|
-
// Seed traversal with symbols from the same file to approximate class blast radius.
|
|
1671
|
-
if ((symType === 'Class' || symType === 'Interface')) {
|
|
1672
|
-
const targetFilePath = getRowValue(sym, 'filePath', 2);
|
|
1673
|
-
if (targetFilePath) {
|
|
1674
|
-
try {
|
|
1675
|
-
const escapedPath = targetFilePath.replace(/'/g, "''");
|
|
1676
|
-
const seedRows = await executeQuery(repo.id, `
|
|
1677
|
-
MATCH (n)
|
|
1678
|
-
WHERE n.filePath = '${escapedPath}'
|
|
1679
|
-
RETURN n.id AS id
|
|
1680
|
-
LIMIT 200
|
|
1681
|
-
`);
|
|
1682
|
-
const seedSet = new Set([symId]);
|
|
1683
|
-
for (const row of seedRows) {
|
|
1684
|
-
const id = getRowValue(row, 'id', 0);
|
|
1685
|
-
if (!id)
|
|
1686
|
-
continue;
|
|
1687
|
-
if (id.startsWith('File:') || id.startsWith('Folder:') || id.startsWith('Community:') || id.startsWith('Process:')) {
|
|
1688
|
-
continue;
|
|
1689
|
-
}
|
|
1690
|
-
seedSet.add(id);
|
|
1691
|
-
}
|
|
1692
|
-
seedIds = Array.from(seedSet);
|
|
1693
|
-
}
|
|
1694
|
-
catch {
|
|
1695
|
-
// fallback to class node only
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1301
|
+
const symId = sym.id || sym[0];
|
|
1699
1302
|
const impacted = [];
|
|
1700
|
-
const visited = new Set(
|
|
1701
|
-
let frontier = [
|
|
1303
|
+
const visited = new Set([symId]);
|
|
1304
|
+
let frontier = [symId];
|
|
1305
|
+
let traversalComplete = true;
|
|
1702
1306
|
for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
|
|
1703
1307
|
const nextFrontier = [];
|
|
1704
1308
|
// Batch frontier nodes into a single Cypher query per depth level
|
|
1705
1309
|
const idList = frontier.map(id => `'${id.replace(/'/g, "''")}'`).join(', ');
|
|
1706
1310
|
const query = direction === 'upstream'
|
|
1707
|
-
? `MATCH (caller)-[r:CodeRelation]->(n) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, caller.id AS id, caller.name AS name, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence`
|
|
1708
|
-
: `MATCH (n)-[r:CodeRelation]->(callee) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, callee.id AS id, callee.name AS name, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`;
|
|
1311
|
+
? `MATCH (caller)-[r:CodeRelation]->(n) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, caller.id AS id, caller.name AS name, labels(caller)[0] AS type, caller.filePath AS filePath, r.type AS relType, r.confidence AS confidence`
|
|
1312
|
+
: `MATCH (n)-[r:CodeRelation]->(callee) WHERE n.id IN [${idList}] AND r.type IN [${relTypeFilter}]${confidenceFilter} RETURN n.id AS sourceId, callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`;
|
|
1709
1313
|
try {
|
|
1710
1314
|
const related = await executeQuery(repo.id, query);
|
|
1711
1315
|
for (const rel of related) {
|
|
1712
|
-
const relId =
|
|
1713
|
-
const filePath =
|
|
1714
|
-
const relNodeType = inferTypeFromId(relId) || 'CodeElement';
|
|
1316
|
+
const relId = rel.id || rel[1];
|
|
1317
|
+
const filePath = rel.filePath || rel[4] || '';
|
|
1715
1318
|
if (!includeTests && isTestFilePath(filePath))
|
|
1716
1319
|
continue;
|
|
1717
|
-
if (relNodeType === 'File' || relNodeType === 'Folder' || relNodeType === 'Community' || relNodeType === 'Process')
|
|
1718
|
-
continue;
|
|
1719
1320
|
if (!visited.has(relId)) {
|
|
1720
1321
|
visited.add(relId);
|
|
1721
1322
|
nextFrontier.push(relId);
|
|
1722
1323
|
impacted.push({
|
|
1723
1324
|
depth,
|
|
1724
1325
|
id: relId,
|
|
1725
|
-
name:
|
|
1726
|
-
type:
|
|
1326
|
+
name: rel.name || rel[2],
|
|
1327
|
+
type: rel.type || rel[3],
|
|
1727
1328
|
filePath,
|
|
1728
|
-
relationType:
|
|
1729
|
-
confidence:
|
|
1329
|
+
relationType: rel.relType || rel[5],
|
|
1330
|
+
confidence: rel.confidence || rel[6] || 1.0,
|
|
1730
1331
|
});
|
|
1731
1332
|
}
|
|
1732
1333
|
}
|
|
1733
1334
|
}
|
|
1734
|
-
catch {
|
|
1335
|
+
catch (e) {
|
|
1336
|
+
logQueryError('impact:depth-traversal', e);
|
|
1337
|
+
// Break out of depth loop on query failure but return partial results
|
|
1338
|
+
// collected so far, rather than silently swallowing the error (#321)
|
|
1339
|
+
traversalComplete = false;
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1735
1342
|
frontier = nextFrontier;
|
|
1736
1343
|
}
|
|
1737
1344
|
const grouped = {};
|
|
@@ -1801,13 +1408,14 @@ export class LocalBackend {
|
|
|
1801
1408
|
return {
|
|
1802
1409
|
target: {
|
|
1803
1410
|
id: symId,
|
|
1804
|
-
name:
|
|
1805
|
-
type:
|
|
1806
|
-
filePath:
|
|
1411
|
+
name: sym.name || sym[1],
|
|
1412
|
+
type: sym.type || sym[2],
|
|
1413
|
+
filePath: sym.filePath || sym[3],
|
|
1807
1414
|
},
|
|
1808
1415
|
direction,
|
|
1809
1416
|
impactedCount: impacted.length,
|
|
1810
1417
|
risk,
|
|
1418
|
+
...(!traversalComplete && { partial: true }),
|
|
1811
1419
|
summary: {
|
|
1812
1420
|
direct: directCount,
|
|
1813
1421
|
processes_affected: processCount,
|
|
@@ -1882,13 +1490,11 @@ export class LocalBackend {
|
|
|
1882
1490
|
async queryClusterDetail(name, repoName) {
|
|
1883
1491
|
const repo = await this.resolveRepo(repoName);
|
|
1884
1492
|
await this.ensureInitialized(repo.id);
|
|
1885
|
-
const
|
|
1886
|
-
const clusterQuery = `
|
|
1493
|
+
const clusters = await executeParameterized(repo.id, `
|
|
1887
1494
|
MATCH (c:Community)
|
|
1888
|
-
WHERE c.label =
|
|
1495
|
+
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
|
|
1889
1496
|
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1890
|
-
|
|
1891
|
-
const clusters = await executeQuery(repo.id, clusterQuery);
|
|
1497
|
+
`, { clusterName: name });
|
|
1892
1498
|
if (clusters.length === 0)
|
|
1893
1499
|
return { error: `Cluster '${name}' not found` };
|
|
1894
1500
|
const rawClusters = clusters.map((c) => ({
|
|
@@ -1901,12 +1507,12 @@ export class LocalBackend {
|
|
|
1901
1507
|
totalSymbols += s;
|
|
1902
1508
|
weightedCohesion += (c.cohesion || 0) * s;
|
|
1903
1509
|
}
|
|
1904
|
-
const members = await
|
|
1510
|
+
const members = await executeParameterized(repo.id, `
|
|
1905
1511
|
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
1906
|
-
WHERE c.label =
|
|
1907
|
-
RETURN DISTINCT n.
|
|
1512
|
+
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
|
|
1513
|
+
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1908
1514
|
LIMIT 30
|
|
1909
|
-
|
|
1515
|
+
`, { clusterName: name });
|
|
1910
1516
|
return {
|
|
1911
1517
|
cluster: {
|
|
1912
1518
|
id: rawClusters[0].id,
|
|
@@ -1917,9 +1523,7 @@ export class LocalBackend {
|
|
|
1917
1523
|
subCommunities: rawClusters.length,
|
|
1918
1524
|
},
|
|
1919
1525
|
members: members.map((m) => ({
|
|
1920
|
-
name:
|
|
1921
|
-
type: inferTypeFromId(getRowValue(m, 'id', 0)) || 'CodeElement',
|
|
1922
|
-
filePath: getRowValue(m, 'filePath', 2),
|
|
1526
|
+
name: m.name || m[0], type: m.type || m[1], filePath: m.filePath || m[2],
|
|
1923
1527
|
})),
|
|
1924
1528
|
};
|
|
1925
1529
|
}
|
|
@@ -1930,37 +1534,33 @@ export class LocalBackend {
|
|
|
1930
1534
|
async queryProcessDetail(name, repoName) {
|
|
1931
1535
|
const repo = await this.resolveRepo(repoName);
|
|
1932
1536
|
await this.ensureInitialized(repo.id);
|
|
1933
|
-
const
|
|
1934
|
-
const processes = await executeQuery(repo.id, `
|
|
1537
|
+
const processes = await executeParameterized(repo.id, `
|
|
1935
1538
|
MATCH (p:Process)
|
|
1936
|
-
WHERE p.label =
|
|
1539
|
+
WHERE p.label = $processName OR p.heuristicLabel = $processName
|
|
1937
1540
|
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1938
1541
|
LIMIT 1
|
|
1939
|
-
|
|
1542
|
+
`, { processName: name });
|
|
1940
1543
|
if (processes.length === 0)
|
|
1941
1544
|
return { error: `Process '${name}' not found` };
|
|
1942
1545
|
const proc = processes[0];
|
|
1943
1546
|
const procId = proc.id || proc[0];
|
|
1944
|
-
const steps = await
|
|
1945
|
-
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id:
|
|
1946
|
-
RETURN n.
|
|
1547
|
+
const steps = await executeParameterized(repo.id, `
|
|
1548
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
|
|
1549
|
+
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
1947
1550
|
ORDER BY r.step
|
|
1948
|
-
|
|
1551
|
+
`, { procId });
|
|
1949
1552
|
return {
|
|
1950
1553
|
process: {
|
|
1951
1554
|
id: procId, label: proc.label || proc[1], heuristicLabel: proc.heuristicLabel || proc[2],
|
|
1952
1555
|
processType: proc.processType || proc[3], stepCount: proc.stepCount || proc[4],
|
|
1953
1556
|
},
|
|
1954
1557
|
steps: steps.map((s) => ({
|
|
1955
|
-
step:
|
|
1956
|
-
name: getRowValue(s, 'name', 1),
|
|
1957
|
-
type: inferTypeFromId(getRowValue(s, 'id', 0)) || 'CodeElement',
|
|
1958
|
-
filePath: getRowValue(s, 'filePath', 2),
|
|
1558
|
+
step: s.step || s[3], name: s.name || s[0], type: s.type || s[1], filePath: s.filePath || s[2],
|
|
1959
1559
|
})),
|
|
1960
1560
|
};
|
|
1961
1561
|
}
|
|
1962
1562
|
async disconnect() {
|
|
1963
|
-
await
|
|
1563
|
+
await closeLbug(); // close all connections
|
|
1964
1564
|
// Note: we intentionally do NOT call disposeEmbedder() here.
|
|
1965
1565
|
// ONNX Runtime's native cleanup segfaults on macOS and some Linux configs,
|
|
1966
1566
|
// and importing the embedder module on Node v24+ crashes if onnxruntime
|