gitnexus 1.4.9 → 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 +13 -13
- package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
- package/dist/core/ingestion/utils/language-detection.js +0 -70
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, isWriteQuery } from '../core/lbug-adapter.js';
|
|
10
|
+
import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, isWriteQuery, } from '../core/lbug-adapter.js';
|
|
11
11
|
export { isWriteQuery };
|
|
12
12
|
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
13
13
|
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
|
@@ -22,25 +22,70 @@ import { listRegisteredRepos, cleanupOldKuzuFiles, } from '../../storage/repo-ma
|
|
|
22
22
|
*/
|
|
23
23
|
export function isTestFilePath(filePath) {
|
|
24
24
|
const p = filePath.toLowerCase().replace(/\\/g, '/');
|
|
25
|
-
return (p.includes('.test.') ||
|
|
26
|
-
p.includes('
|
|
27
|
-
p.includes('/
|
|
28
|
-
p.includes('/
|
|
29
|
-
p.
|
|
30
|
-
p.
|
|
31
|
-
p.includes('/
|
|
25
|
+
return (p.includes('.test.') ||
|
|
26
|
+
p.includes('.spec.') ||
|
|
27
|
+
p.includes('__tests__/') ||
|
|
28
|
+
p.includes('__mocks__/') ||
|
|
29
|
+
p.includes('/test/') ||
|
|
30
|
+
p.includes('/tests/') ||
|
|
31
|
+
p.includes('/testing/') ||
|
|
32
|
+
p.includes('/fixtures/') ||
|
|
33
|
+
p.endsWith('_test.go') ||
|
|
34
|
+
p.endsWith('_test.py') ||
|
|
35
|
+
p.endsWith('_spec.rb') ||
|
|
36
|
+
p.endsWith('_test.rb') ||
|
|
37
|
+
p.includes('/spec/') ||
|
|
38
|
+
p.includes('/test_') ||
|
|
39
|
+
p.includes('/conftest.'));
|
|
32
40
|
}
|
|
33
41
|
/** Valid LadybugDB node labels for safe Cypher query construction */
|
|
34
42
|
export const VALID_NODE_LABELS = new Set([
|
|
35
|
-
'File',
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
43
|
+
'File',
|
|
44
|
+
'Folder',
|
|
45
|
+
'Function',
|
|
46
|
+
'Class',
|
|
47
|
+
'Interface',
|
|
48
|
+
'Method',
|
|
49
|
+
'CodeElement',
|
|
50
|
+
'Community',
|
|
51
|
+
'Process',
|
|
52
|
+
'Struct',
|
|
53
|
+
'Enum',
|
|
54
|
+
'Macro',
|
|
55
|
+
'Typedef',
|
|
56
|
+
'Union',
|
|
57
|
+
'Namespace',
|
|
58
|
+
'Trait',
|
|
59
|
+
'Impl',
|
|
60
|
+
'TypeAlias',
|
|
61
|
+
'Const',
|
|
62
|
+
'Static',
|
|
63
|
+
'Property',
|
|
64
|
+
'Record',
|
|
65
|
+
'Delegate',
|
|
66
|
+
'Annotation',
|
|
67
|
+
'Constructor',
|
|
68
|
+
'Template',
|
|
69
|
+
'Module',
|
|
39
70
|
'Route',
|
|
40
71
|
'Tool',
|
|
41
72
|
]);
|
|
42
73
|
/** Valid relation types for impact analysis filtering */
|
|
43
|
-
export const VALID_RELATION_TYPES = new Set([
|
|
74
|
+
export const VALID_RELATION_TYPES = new Set([
|
|
75
|
+
'CALLS',
|
|
76
|
+
'IMPORTS',
|
|
77
|
+
'EXTENDS',
|
|
78
|
+
'IMPLEMENTS',
|
|
79
|
+
'HAS_METHOD',
|
|
80
|
+
'HAS_PROPERTY',
|
|
81
|
+
'OVERRIDES',
|
|
82
|
+
'ACCESSES',
|
|
83
|
+
'HANDLES_ROUTE',
|
|
84
|
+
'FETCHES',
|
|
85
|
+
'HANDLES_TOOL',
|
|
86
|
+
'ENTRY_POINT_OF',
|
|
87
|
+
'WRAPS',
|
|
88
|
+
]);
|
|
44
89
|
/**
|
|
45
90
|
* Per-relation-type confidence floor for impact analysis.
|
|
46
91
|
*
|
|
@@ -187,10 +232,10 @@ export class LocalBackend {
|
|
|
187
232
|
throw new Error('No indexed repositories. Run: gitnexus analyze');
|
|
188
233
|
}
|
|
189
234
|
if (repoParam) {
|
|
190
|
-
const names = [...this.repos.values()].map(h => h.name);
|
|
235
|
+
const names = [...this.repos.values()].map((h) => h.name);
|
|
191
236
|
throw new Error(`Repository "${repoParam}" not found. Available: ${names.join(', ')}`);
|
|
192
237
|
}
|
|
193
|
-
const names = [...this.repos.values()].map(h => h.name);
|
|
238
|
+
const names = [...this.repos.values()].map((h) => h.name);
|
|
194
239
|
throw new Error(`Multiple repositories indexed. Specify which one with the "repo" parameter. Available: ${names.join(', ')}`);
|
|
195
240
|
}
|
|
196
241
|
/**
|
|
@@ -306,7 +351,7 @@ export class LocalBackend {
|
|
|
306
351
|
*/
|
|
307
352
|
async listRepos() {
|
|
308
353
|
await this.refreshRepos();
|
|
309
|
-
return [...this.repos.values()].map(h => ({
|
|
354
|
+
return [...this.repos.values()].map((h) => ({
|
|
310
355
|
name: h.name,
|
|
311
356
|
path: h.repoPath,
|
|
312
357
|
indexedAt: h.indexedAt,
|
|
@@ -516,14 +561,14 @@ export class LocalBackend {
|
|
|
516
561
|
}
|
|
517
562
|
// Step 3: Rank processes by aggregate score + internal cohesion boost
|
|
518
563
|
const rankedProcesses = Array.from(processMap.values())
|
|
519
|
-
.map(p => ({
|
|
564
|
+
.map((p) => ({
|
|
520
565
|
...p,
|
|
521
|
-
priority: p.totalScore +
|
|
566
|
+
priority: p.totalScore + p.cohesionBoost * 0.1, // cohesion as subtle ranking signal
|
|
522
567
|
}))
|
|
523
568
|
.sort((a, b) => b.priority - a.priority)
|
|
524
569
|
.slice(0, processLimit);
|
|
525
570
|
// Step 4: Build response
|
|
526
|
-
const processes = rankedProcesses.map(p => ({
|
|
571
|
+
const processes = rankedProcesses.map((p) => ({
|
|
527
572
|
id: p.id,
|
|
528
573
|
summary: p.heuristicLabel || p.label,
|
|
529
574
|
priority: Math.round(p.priority * 1000) / 1000,
|
|
@@ -531,13 +576,13 @@ export class LocalBackend {
|
|
|
531
576
|
process_type: p.processType,
|
|
532
577
|
step_count: p.stepCount,
|
|
533
578
|
}));
|
|
534
|
-
const processSymbols = rankedProcesses.flatMap(p => p.symbols.slice(0, maxSymbolsPerProcess).map(s => ({
|
|
579
|
+
const processSymbols = rankedProcesses.flatMap((p) => p.symbols.slice(0, maxSymbolsPerProcess).map((s) => ({
|
|
535
580
|
...s,
|
|
536
581
|
// remove internal fields
|
|
537
582
|
})));
|
|
538
583
|
// Deduplicate process_symbols by id
|
|
539
584
|
const seen = new Set();
|
|
540
|
-
const dedupedSymbols = processSymbols.filter(s => {
|
|
585
|
+
const dedupedSymbols = processSymbols.filter((s) => {
|
|
541
586
|
if (seen.has(s.id))
|
|
542
587
|
return false;
|
|
543
588
|
seen.add(s.id);
|
|
@@ -547,7 +592,9 @@ export class LocalBackend {
|
|
|
547
592
|
processes,
|
|
548
593
|
process_symbols: dedupedSymbols,
|
|
549
594
|
definitions: definitions.slice(0, 20), // cap standalone definitions
|
|
550
|
-
...(!ftsUsed && {
|
|
595
|
+
...(!ftsUsed && {
|
|
596
|
+
warning: 'FTS extension unavailable - keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.',
|
|
597
|
+
}),
|
|
551
598
|
};
|
|
552
599
|
}
|
|
553
600
|
/**
|
|
@@ -563,7 +610,7 @@ export class LocalBackend {
|
|
|
563
610
|
console.error('GitNexus: BM25/FTS search failed (FTS indexes may not exist) -', err.message);
|
|
564
611
|
return { results: [], ftsUsed: false };
|
|
565
612
|
}
|
|
566
|
-
const ftsUsed = bm25Results.length === 0 ||
|
|
613
|
+
const ftsUsed = bm25Results.length === 0 || bm25Results[0]?.ftsUsed !== false;
|
|
567
614
|
const results = [];
|
|
568
615
|
for (const bm25Result of bm25Results) {
|
|
569
616
|
const fullPath = bm25Result.filePath;
|
|
@@ -681,7 +728,9 @@ export class LocalBackend {
|
|
|
681
728
|
}
|
|
682
729
|
// Block write operations (defense-in-depth — DB is already read-only)
|
|
683
730
|
if (isWriteQuery(params.query)) {
|
|
684
|
-
return {
|
|
731
|
+
return {
|
|
732
|
+
error: 'Write operations (CREATE, DELETE, SET, MERGE, REMOVE, DROP, ALTER, COPY, DETACH) are not allowed. The knowledge graph is read-only.',
|
|
733
|
+
};
|
|
685
734
|
}
|
|
686
735
|
try {
|
|
687
736
|
const result = await executeQuery(repo.id, params.query);
|
|
@@ -706,14 +755,18 @@ export class LocalBackend {
|
|
|
706
755
|
return result;
|
|
707
756
|
const header = '| ' + keys.join(' | ') + ' |';
|
|
708
757
|
const separator = '| ' + keys.map(() => '---').join(' | ') + ' |';
|
|
709
|
-
const dataRows = result.map((row) => '| ' +
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
758
|
+
const dataRows = result.map((row) => '| ' +
|
|
759
|
+
keys
|
|
760
|
+
.map((k) => {
|
|
761
|
+
const v = row[k];
|
|
762
|
+
if (v === null || v === undefined)
|
|
763
|
+
return '';
|
|
764
|
+
if (typeof v === 'object')
|
|
765
|
+
return JSON.stringify(v);
|
|
766
|
+
return String(v);
|
|
767
|
+
})
|
|
768
|
+
.join(' | ') +
|
|
769
|
+
' |');
|
|
717
770
|
return {
|
|
718
771
|
markdown: [header, separator, ...dataRows].join('\n'),
|
|
719
772
|
row_count: result.length,
|
|
@@ -732,7 +785,12 @@ export class LocalBackend {
|
|
|
732
785
|
const cohesion = c.cohesion || 0;
|
|
733
786
|
const existing = groups.get(label);
|
|
734
787
|
if (!existing) {
|
|
735
|
-
groups.set(label, {
|
|
788
|
+
groups.set(label, {
|
|
789
|
+
ids: [c.id],
|
|
790
|
+
totalSymbols: symbols,
|
|
791
|
+
weightedCohesion: cohesion * symbols,
|
|
792
|
+
largest: c,
|
|
793
|
+
});
|
|
736
794
|
}
|
|
737
795
|
else {
|
|
738
796
|
existing.ids.push(c.id);
|
|
@@ -752,7 +810,7 @@ export class LocalBackend {
|
|
|
752
810
|
cohesion: g.totalSymbols > 0 ? g.weightedCohesion / g.totalSymbols : 0,
|
|
753
811
|
subCommunities: g.ids.length,
|
|
754
812
|
}))
|
|
755
|
-
.filter(c => c.symbolCount >= 5)
|
|
813
|
+
.filter((c) => c.symbolCount >= 5)
|
|
756
814
|
.sort((a, b) => b.symbolCount - a.symbolCount);
|
|
757
815
|
}
|
|
758
816
|
async overview(repo, params) {
|
|
@@ -914,7 +972,7 @@ export class LocalBackend {
|
|
|
914
972
|
const sym = symbols[0];
|
|
915
973
|
const symId = sym.id || sym[0];
|
|
916
974
|
// Categorized incoming refs
|
|
917
|
-
|
|
975
|
+
const incomingRows = await executeParameterized(repo.id, `
|
|
918
976
|
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
|
|
919
977
|
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
|
|
920
978
|
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
@@ -940,7 +998,9 @@ export class LocalBackend {
|
|
|
940
998
|
`, { symId });
|
|
941
999
|
isClassLike = typeCheck.length > 0;
|
|
942
1000
|
}
|
|
943
|
-
catch {
|
|
1001
|
+
catch {
|
|
1002
|
+
/* not a Class/Interface node */
|
|
1003
|
+
}
|
|
944
1004
|
}
|
|
945
1005
|
else if (!isClassLike) {
|
|
946
1006
|
isClassLike = symRawType === 'Class' || symRawType === 'Interface';
|
|
@@ -1022,7 +1082,7 @@ export class LocalBackend {
|
|
|
1022
1082
|
symbol: {
|
|
1023
1083
|
uid: sym.id || sym[0],
|
|
1024
1084
|
name: sym.name || sym[1],
|
|
1025
|
-
kind: isClassLike ?
|
|
1085
|
+
kind: isClassLike ? resolvedLabel || 'Class' : sym.type || sym[2],
|
|
1026
1086
|
filePath: sym.filePath || sym[3],
|
|
1027
1087
|
startLine: sym.startLine || sym[4],
|
|
1028
1088
|
endLine: sym.endLine || sym[5],
|
|
@@ -1057,8 +1117,11 @@ export class LocalBackend {
|
|
|
1057
1117
|
if (clusters.length === 0)
|
|
1058
1118
|
return { error: `Cluster '${name}' not found` };
|
|
1059
1119
|
const rawClusters = clusters.map((c) => ({
|
|
1060
|
-
id: c.id || c[0],
|
|
1061
|
-
|
|
1120
|
+
id: c.id || c[0],
|
|
1121
|
+
label: c.label || c[1],
|
|
1122
|
+
heuristicLabel: c.heuristicLabel || c[2],
|
|
1123
|
+
cohesion: c.cohesion || c[3],
|
|
1124
|
+
symbolCount: c.symbolCount || c[4],
|
|
1062
1125
|
}));
|
|
1063
1126
|
let totalSymbols = 0, weightedCohesion = 0;
|
|
1064
1127
|
for (const c of rawClusters) {
|
|
@@ -1082,7 +1145,9 @@ export class LocalBackend {
|
|
|
1082
1145
|
subCommunities: rawClusters.length,
|
|
1083
1146
|
},
|
|
1084
1147
|
members: members.map((m) => ({
|
|
1085
|
-
name: m.name || m[0],
|
|
1148
|
+
name: m.name || m[0],
|
|
1149
|
+
type: m.type || m[1],
|
|
1150
|
+
filePath: m.filePath || m[2],
|
|
1086
1151
|
})),
|
|
1087
1152
|
};
|
|
1088
1153
|
}
|
|
@@ -1104,11 +1169,17 @@ export class LocalBackend {
|
|
|
1104
1169
|
`, { procId });
|
|
1105
1170
|
return {
|
|
1106
1171
|
process: {
|
|
1107
|
-
id: procId,
|
|
1108
|
-
|
|
1172
|
+
id: procId,
|
|
1173
|
+
label: proc.label || proc[1],
|
|
1174
|
+
heuristicLabel: proc.heuristicLabel || proc[2],
|
|
1175
|
+
processType: proc.processType || proc[3],
|
|
1176
|
+
stepCount: proc.stepCount || proc[4],
|
|
1109
1177
|
},
|
|
1110
1178
|
steps: steps.map((s) => ({
|
|
1111
|
-
step: s.step || s[3],
|
|
1179
|
+
step: s.step || s[3],
|
|
1180
|
+
name: s.name || s[0],
|
|
1181
|
+
type: s.type || s[1],
|
|
1182
|
+
filePath: s.filePath || s[2],
|
|
1112
1183
|
})),
|
|
1113
1184
|
};
|
|
1114
1185
|
}
|
|
@@ -1144,14 +1215,22 @@ export class LocalBackend {
|
|
|
1144
1215
|
let changedFiles;
|
|
1145
1216
|
try {
|
|
1146
1217
|
const output = execFileSync('git', diffArgs, { cwd: repo.repoPath, encoding: 'utf-8' });
|
|
1147
|
-
changedFiles = output
|
|
1218
|
+
changedFiles = output
|
|
1219
|
+
.trim()
|
|
1220
|
+
.split('\n')
|
|
1221
|
+
.filter((f) => f.length > 0);
|
|
1148
1222
|
}
|
|
1149
1223
|
catch (err) {
|
|
1150
1224
|
return { error: `Git diff failed: ${err.message}` };
|
|
1151
1225
|
}
|
|
1152
1226
|
if (changedFiles.length === 0) {
|
|
1153
1227
|
return {
|
|
1154
|
-
summary: {
|
|
1228
|
+
summary: {
|
|
1229
|
+
changed_count: 0,
|
|
1230
|
+
affected_count: 0,
|
|
1231
|
+
risk_level: 'none',
|
|
1232
|
+
message: 'No changes detected.',
|
|
1233
|
+
},
|
|
1155
1234
|
changed_symbols: [],
|
|
1156
1235
|
affected_processes: [],
|
|
1157
1236
|
};
|
|
@@ -1210,7 +1289,13 @@ export class LocalBackend {
|
|
|
1210
1289
|
}
|
|
1211
1290
|
}
|
|
1212
1291
|
const processCount = affectedProcesses.size;
|
|
1213
|
-
const risk = processCount === 0
|
|
1292
|
+
const risk = processCount === 0
|
|
1293
|
+
? 'low'
|
|
1294
|
+
: processCount <= 5
|
|
1295
|
+
? 'medium'
|
|
1296
|
+
: processCount <= 15
|
|
1297
|
+
? 'high'
|
|
1298
|
+
: 'critical';
|
|
1214
1299
|
return {
|
|
1215
1300
|
summary: {
|
|
1216
1301
|
changed_count: changedSymbols.length,
|
|
@@ -1298,7 +1383,9 @@ export class LocalBackend {
|
|
|
1298
1383
|
const lines = content.split('\n');
|
|
1299
1384
|
for (let i = 0; i < lines.length; i++) {
|
|
1300
1385
|
if (lines[i].includes(oldName)) {
|
|
1301
|
-
addEdit(ref.filePath, i + 1, lines[i].trim(), lines[i]
|
|
1386
|
+
addEdit(ref.filePath, i + 1, lines[i].trim(), lines[i]
|
|
1387
|
+
.replace(new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g'), new_name)
|
|
1388
|
+
.trim(), 'graph');
|
|
1302
1389
|
graphEdits++;
|
|
1303
1390
|
break; // one edit per file from graph refs
|
|
1304
1391
|
}
|
|
@@ -1310,19 +1397,28 @@ export class LocalBackend {
|
|
|
1310
1397
|
}
|
|
1311
1398
|
// Step 3: Text search for refs the graph might have missed
|
|
1312
1399
|
let astSearchEdits = 0;
|
|
1313
|
-
const graphFiles = new Set([sym.filePath, ...allIncoming.map(r => r.filePath)].filter(Boolean));
|
|
1400
|
+
const graphFiles = new Set([sym.filePath, ...allIncoming.map((r) => r.filePath)].filter(Boolean));
|
|
1314
1401
|
// Simple text search across the repo for the old name (in files not already covered by graph)
|
|
1315
1402
|
try {
|
|
1316
1403
|
const { execFileSync } = await import('child_process');
|
|
1317
1404
|
const rgArgs = [
|
|
1318
1405
|
'-l',
|
|
1319
|
-
'--type-add',
|
|
1320
|
-
'
|
|
1406
|
+
'--type-add',
|
|
1407
|
+
'code:*.{ts,tsx,js,jsx,py,go,rs,java,c,h,cpp,cc,cxx,hpp,hxx,hh,cs,php,swift}',
|
|
1408
|
+
'-t',
|
|
1409
|
+
'code',
|
|
1321
1410
|
`\\b${oldName}\\b`,
|
|
1322
1411
|
'.',
|
|
1323
1412
|
];
|
|
1324
|
-
const output = execFileSync('rg', rgArgs, {
|
|
1325
|
-
|
|
1413
|
+
const output = execFileSync('rg', rgArgs, {
|
|
1414
|
+
cwd: repo.repoPath,
|
|
1415
|
+
encoding: 'utf-8',
|
|
1416
|
+
timeout: 5000,
|
|
1417
|
+
});
|
|
1418
|
+
const files = output
|
|
1419
|
+
.trim()
|
|
1420
|
+
.split('\n')
|
|
1421
|
+
.filter((f) => f.length > 0);
|
|
1326
1422
|
for (const file of files) {
|
|
1327
1423
|
const normalizedFile = file.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
1328
1424
|
if (graphFiles.has(normalizedFile))
|
|
@@ -1399,12 +1495,12 @@ export class LocalBackend {
|
|
|
1399
1495
|
const { target, direction } = params;
|
|
1400
1496
|
const maxDepth = params.maxDepth || 3;
|
|
1401
1497
|
const rawRelTypes = params.relationTypes && params.relationTypes.length > 0
|
|
1402
|
-
? params.relationTypes.filter(t => VALID_RELATION_TYPES.has(t))
|
|
1498
|
+
? params.relationTypes.filter((t) => VALID_RELATION_TYPES.has(t))
|
|
1403
1499
|
: ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
|
1404
1500
|
const relationTypes = rawRelTypes.length > 0 ? rawRelTypes : ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
|
|
1405
1501
|
const includeTests = params.includeTests ?? false;
|
|
1406
1502
|
const minConfidence = params.minConfidence ?? 0;
|
|
1407
|
-
const relTypeFilter = relationTypes.map(t => `'${t}'`).join(', ');
|
|
1503
|
+
const relTypeFilter = relationTypes.map((t) => `'${t}'`).join(', ');
|
|
1408
1504
|
const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
|
|
1409
1505
|
// Resolve target by name, preferring Class/Interface over Constructor
|
|
1410
1506
|
// (fix #480: Java class and constructor share the same name).
|
|
@@ -1438,7 +1534,9 @@ export class LocalBackend {
|
|
|
1438
1534
|
symType = priorityToLabel[best.priority ?? best[3]] ?? '';
|
|
1439
1535
|
}
|
|
1440
1536
|
}
|
|
1441
|
-
catch {
|
|
1537
|
+
catch {
|
|
1538
|
+
/* fall through to unlabeled match */
|
|
1539
|
+
}
|
|
1442
1540
|
// Fall back to unlabeled match for any other node type
|
|
1443
1541
|
if (!sym) {
|
|
1444
1542
|
const rows = await executeParameterized(repo.id, `
|
|
@@ -1503,7 +1601,7 @@ export class LocalBackend {
|
|
|
1503
1601
|
for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
|
|
1504
1602
|
const nextFrontier = [];
|
|
1505
1603
|
// Batch frontier nodes into a single Cypher query per depth level
|
|
1506
|
-
const idList = frontier.map(id => `'${id.replace(/'/g, "''")}'`).join(', ');
|
|
1604
|
+
const idList = frontier.map((id) => `'${id.replace(/'/g, "''")}'`).join(', ');
|
|
1507
1605
|
const query = direction === 'upstream'
|
|
1508
1606
|
? `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`
|
|
1509
1607
|
: `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`;
|
|
@@ -1572,7 +1670,7 @@ export class LocalBackend {
|
|
|
1572
1670
|
let chunksProcessed = 0;
|
|
1573
1671
|
for (let i = 0; i < impacted.length && chunksProcessed < MAX_CHUNKS; i += CHUNK_SIZE, chunksProcessed++) {
|
|
1574
1672
|
const chunk = impacted.slice(i, i + CHUNK_SIZE);
|
|
1575
|
-
const ids = chunk.map(item => String(item.id ?? ''));
|
|
1673
|
+
const ids = chunk.map((item) => String(item.id ?? ''));
|
|
1576
1674
|
try {
|
|
1577
1675
|
// Use parameterized list to avoid building long query strings
|
|
1578
1676
|
const rows = await executeParameterized(repo.id, `
|
|
@@ -1593,14 +1691,16 @@ export class LocalBackend {
|
|
|
1593
1691
|
// Normalize epName: prefer epName, fall back to other columns, and
|
|
1594
1692
|
// ensure we don't keep an empty string (labels(...) can return "").
|
|
1595
1693
|
const epNameRaw = row.epName ?? row[7] ?? row.name ?? row[1] ?? 'unknown';
|
|
1596
|
-
const epName =
|
|
1694
|
+
const epName = typeof epNameRaw === 'string' && epNameRaw.trim().length > 0
|
|
1695
|
+
? epNameRaw.trim()
|
|
1696
|
+
: 'unknown';
|
|
1597
1697
|
// Normalize epType: labels(ep)[0] can return an empty string in
|
|
1598
1698
|
// some DBs (LadybugDB). Using nullish coalescing (??) preserves
|
|
1599
1699
|
// empty strings, which results in empty `type` values being
|
|
1600
1700
|
// propagated. Treat empty-string labels as missing and fall back
|
|
1601
1701
|
// to the next candidate or a sensible default.
|
|
1602
1702
|
const epTypeRaw = row.epType ?? row[8] ?? '';
|
|
1603
|
-
const epType =
|
|
1703
|
+
const epType = typeof epTypeRaw === 'string' && epTypeRaw.trim().length > 0
|
|
1604
1704
|
? epTypeRaw.trim()
|
|
1605
1705
|
: 'Function';
|
|
1606
1706
|
const epFilePath = row.epFilePath ?? row[9] ?? '';
|
|
@@ -1638,7 +1738,7 @@ export class LocalBackend {
|
|
|
1638
1738
|
if (processesMissingMinStep.size > 0) {
|
|
1639
1739
|
try {
|
|
1640
1740
|
const pIds = Array.from(processesMissingMinStep);
|
|
1641
|
-
const allImpactedIds = impacted.map(it => String(it.id ?? ''));
|
|
1741
|
+
const allImpactedIds = impacted.map((it) => String(it.id ?? ''));
|
|
1642
1742
|
const missingRows = await executeParameterized(repo.id, `
|
|
1643
1743
|
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
1644
1744
|
WHERE p.id IN $pIds AND s.id IN $ids
|
|
@@ -1667,7 +1767,7 @@ export class LocalBackend {
|
|
|
1667
1767
|
traversalComplete = false;
|
|
1668
1768
|
}
|
|
1669
1769
|
affectedProcesses = Array.from(entryPointMap.values())
|
|
1670
|
-
.map(ep => ({
|
|
1770
|
+
.map((ep) => ({
|
|
1671
1771
|
...ep,
|
|
1672
1772
|
earliest_broken_step: ep.earliest_broken_step === Infinity ? null : ep.earliest_broken_step,
|
|
1673
1773
|
}))
|
|
@@ -1741,7 +1841,7 @@ export class LocalBackend {
|
|
|
1741
1841
|
.map(([name, hits]) => ({ name, hits }))
|
|
1742
1842
|
.sort((a, b) => b.hits - a.hits)
|
|
1743
1843
|
.slice(0, 20);
|
|
1744
|
-
const directModuleRows = Array.from(directModuleSet).map(name => ({ name }));
|
|
1844
|
+
const directModuleRows = Array.from(directModuleSet).map((name) => ({ name }));
|
|
1745
1845
|
// Build affectedModules in the same shape as original implementation
|
|
1746
1846
|
const directModuleNameSet = new Set(directModuleRows.map((r) => r.name || r[0]));
|
|
1747
1847
|
affectedModules = moduleRows.map((r) => {
|
|
@@ -1761,7 +1861,10 @@ export class LocalBackend {
|
|
|
1761
1861
|
if (directCount >= 30 || processCount >= 5 || moduleCount >= 5 || impacted.length >= 200) {
|
|
1762
1862
|
risk = 'CRITICAL';
|
|
1763
1863
|
}
|
|
1764
|
-
else if (directCount >= 15 ||
|
|
1864
|
+
else if (directCount >= 15 ||
|
|
1865
|
+
processCount >= 3 ||
|
|
1866
|
+
moduleCount >= 3 ||
|
|
1867
|
+
impacted.length >= 100) {
|
|
1765
1868
|
risk = 'HIGH';
|
|
1766
1869
|
}
|
|
1767
1870
|
else if (directCount >= 5 || impacted.length >= 30) {
|
|
@@ -1805,7 +1908,7 @@ export class LocalBackend {
|
|
|
1805
1908
|
`, params);
|
|
1806
1909
|
// Strip wrapping quotes from DB array elements — CSV COPY stores ['key'] which
|
|
1807
1910
|
// LadybugDB may return as "'key'" rather than "key"
|
|
1808
|
-
const stripQuotes = (keys) => keys ? keys.map(k => k.replace(/^['"]|['"]$/g, '')) : null;
|
|
1911
|
+
const stripQuotes = (keys) => keys ? keys.map((k) => k.replace(/^['"]|['"]$/g, '')) : null;
|
|
1809
1912
|
const routeMap = new Map();
|
|
1810
1913
|
for (const row of rows) {
|
|
1811
1914
|
const id = row.routeId ?? row[0];
|
|
@@ -1818,7 +1921,15 @@ export class LocalBackend {
|
|
|
1818
1921
|
const consumerFile = row.consumerFile ?? row[7];
|
|
1819
1922
|
const fetchReason = row.fetchReason ?? row[8] ?? null;
|
|
1820
1923
|
if (!routeMap.has(id)) {
|
|
1821
|
-
routeMap.set(id, {
|
|
1924
|
+
routeMap.set(id, {
|
|
1925
|
+
id,
|
|
1926
|
+
name,
|
|
1927
|
+
filePath,
|
|
1928
|
+
responseKeys,
|
|
1929
|
+
errorKeys,
|
|
1930
|
+
middleware,
|
|
1931
|
+
consumers: [],
|
|
1932
|
+
});
|
|
1822
1933
|
}
|
|
1823
1934
|
if (consumerName && consumerFile) {
|
|
1824
1935
|
// Parse accessed keys from reason field: "fetch-url-match|keys:data,pagination|fetches:3"
|
|
@@ -1827,7 +1938,7 @@ export class LocalBackend {
|
|
|
1827
1938
|
if (fetchReason) {
|
|
1828
1939
|
const keysMatch = fetchReason.match(/\|keys:([^|]+)/);
|
|
1829
1940
|
if (keysMatch) {
|
|
1830
|
-
accessedKeys = keysMatch[1].split(',').filter(k => k.length > 0);
|
|
1941
|
+
accessedKeys = keysMatch[1].split(',').filter((k) => k.length > 0);
|
|
1831
1942
|
}
|
|
1832
1943
|
const fetchesMatch = fetchReason.match(/\|fetches:(\d+)/);
|
|
1833
1944
|
if (fetchesMatch) {
|
|
@@ -1873,7 +1984,9 @@ export class LocalBackend {
|
|
|
1873
1984
|
list.push(name);
|
|
1874
1985
|
}
|
|
1875
1986
|
}
|
|
1876
|
-
catch {
|
|
1987
|
+
catch {
|
|
1988
|
+
/* no ENTRY_POINT_OF edges yet */
|
|
1989
|
+
}
|
|
1877
1990
|
return result;
|
|
1878
1991
|
}
|
|
1879
1992
|
async routeMap(repo, params) {
|
|
@@ -1882,12 +1995,19 @@ export class LocalBackend {
|
|
|
1882
1995
|
const queryParams = params.route ? { route: params.route } : {};
|
|
1883
1996
|
const routes = await this.fetchRoutesWithConsumers(repo.id, routeFilter, queryParams);
|
|
1884
1997
|
if (routes.length === 0) {
|
|
1885
|
-
return {
|
|
1998
|
+
return {
|
|
1999
|
+
routes: [],
|
|
2000
|
+
total: 0,
|
|
2001
|
+
message: params.route
|
|
2002
|
+
? `No routes matching "${params.route}"`
|
|
2003
|
+
: 'No routes found in this project.',
|
|
2004
|
+
};
|
|
1886
2005
|
}
|
|
1887
|
-
const flowMap = await this.fetchLinkedFlowsBatch(repo.id, routes.map(r => r.id));
|
|
2006
|
+
const flowMap = await this.fetchLinkedFlowsBatch(repo.id, routes.map((r) => r.id));
|
|
1888
2007
|
return {
|
|
1889
|
-
routes: routes.map(r => ({
|
|
1890
|
-
route: r.name,
|
|
2008
|
+
routes: routes.map((r) => ({
|
|
2009
|
+
route: r.name,
|
|
2010
|
+
handler: r.filePath,
|
|
1891
2011
|
middleware: r.middleware || [],
|
|
1892
2012
|
consumers: r.consumers,
|
|
1893
2013
|
flows: flowMap.get(r.id) || [],
|
|
@@ -1901,8 +2021,10 @@ export class LocalBackend {
|
|
|
1901
2021
|
const queryParams = params.route ? { route: params.route } : {};
|
|
1902
2022
|
const allRoutes = await this.fetchRoutesWithConsumers(repo.id, routeFilter, queryParams);
|
|
1903
2023
|
const results = allRoutes
|
|
1904
|
-
.filter(r => ((r.responseKeys && r.responseKeys.length > 0) ||
|
|
1905
|
-
|
|
2024
|
+
.filter((r) => ((r.responseKeys && r.responseKeys.length > 0) ||
|
|
2025
|
+
(r.errorKeys && r.errorKeys.length > 0)) &&
|
|
2026
|
+
r.consumers.length > 0)
|
|
2027
|
+
.map((r) => {
|
|
1906
2028
|
// Keys already normalized by fetchRoutesWithConsumers (quotes stripped)
|
|
1907
2029
|
const responseKeys = r.responseKeys ?? [];
|
|
1908
2030
|
const errorKeys = r.errorKeys ?? [];
|
|
@@ -1910,24 +2032,33 @@ export class LocalBackend {
|
|
|
1910
2032
|
const allKnownKeys = new Set([...responseKeys, ...errorKeys]);
|
|
1911
2033
|
// Check each consumer's accessed keys against the route's response shape
|
|
1912
2034
|
const responseKeySet = new Set(responseKeys);
|
|
1913
|
-
const consumers = r.consumers.map(c => {
|
|
2035
|
+
const consumers = r.consumers.map((c) => {
|
|
1914
2036
|
if (!c.accessedKeys || c.accessedKeys.length === 0) {
|
|
1915
2037
|
return { name: c.name, filePath: c.filePath };
|
|
1916
2038
|
}
|
|
1917
|
-
const mismatched = c.accessedKeys.filter(k => !allKnownKeys.has(k));
|
|
2039
|
+
const mismatched = c.accessedKeys.filter((k) => !allKnownKeys.has(k));
|
|
1918
2040
|
// Keys in allKnownKeys but not in responseKeys — error-path access (e.g., .error from errorKeys)
|
|
1919
|
-
const errorPathKeys = c.accessedKeys.filter(k => allKnownKeys.has(k) && !responseKeySet.has(k));
|
|
2041
|
+
const errorPathKeys = c.accessedKeys.filter((k) => allKnownKeys.has(k) && !responseKeySet.has(k));
|
|
1920
2042
|
const isMultiFetch = (c.fetchCount ?? 1) > 1;
|
|
1921
2043
|
return {
|
|
1922
2044
|
name: c.name,
|
|
1923
2045
|
filePath: c.filePath,
|
|
1924
2046
|
accessedKeys: c.accessedKeys,
|
|
1925
|
-
...(mismatched.length > 0
|
|
2047
|
+
...(mismatched.length > 0
|
|
2048
|
+
? {
|
|
2049
|
+
mismatched,
|
|
2050
|
+
mismatchConfidence: isMultiFetch ? 'low' : 'high',
|
|
2051
|
+
}
|
|
2052
|
+
: {}),
|
|
1926
2053
|
...(errorPathKeys.length > 0 ? { errorPathKeys } : {}),
|
|
1927
|
-
...(isMultiFetch
|
|
2054
|
+
...(isMultiFetch
|
|
2055
|
+
? {
|
|
2056
|
+
attributionNote: `This file fetches ${c.fetchCount} routes — accessed keys may belong to a different route.`,
|
|
2057
|
+
}
|
|
2058
|
+
: {}),
|
|
1928
2059
|
};
|
|
1929
2060
|
});
|
|
1930
|
-
const hasMismatches = consumers.some(c => 'mismatched' in c && c.mismatched.length > 0);
|
|
2061
|
+
const hasMismatches = consumers.some((c) => 'mismatched' in c && c.mismatched.length > 0);
|
|
1931
2062
|
return {
|
|
1932
2063
|
route: r.name,
|
|
1933
2064
|
handler: r.filePath,
|
|
@@ -1937,7 +2068,7 @@ export class LocalBackend {
|
|
|
1937
2068
|
...(hasMismatches ? { status: 'MISMATCH' } : {}),
|
|
1938
2069
|
};
|
|
1939
2070
|
});
|
|
1940
|
-
const mismatchCount = results.filter(r => r.status === 'MISMATCH').length;
|
|
2071
|
+
const mismatchCount = results.filter((r) => r.status === 'MISMATCH').length;
|
|
1941
2072
|
return {
|
|
1942
2073
|
routes: results,
|
|
1943
2074
|
total: results.length,
|
|
@@ -1960,7 +2091,11 @@ export class LocalBackend {
|
|
|
1960
2091
|
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.description AS description
|
|
1961
2092
|
`, queryParams);
|
|
1962
2093
|
if (rows.length === 0) {
|
|
1963
|
-
return {
|
|
2094
|
+
return {
|
|
2095
|
+
tools: [],
|
|
2096
|
+
total: 0,
|
|
2097
|
+
message: params.tool ? `No tools matching "${params.tool}"` : 'No tool definitions found.',
|
|
2098
|
+
};
|
|
1964
2099
|
}
|
|
1965
2100
|
const toolIds = rows.map((r) => r.id ?? r[0]);
|
|
1966
2101
|
const flowMap = await this.fetchLinkedFlowsBatch(repo.id, toolIds);
|
|
@@ -1998,7 +2133,7 @@ export class LocalBackend {
|
|
|
1998
2133
|
const target = params.route || params.file;
|
|
1999
2134
|
return { error: `No routes found matching "${target}".` };
|
|
2000
2135
|
}
|
|
2001
|
-
const flowMap = await this.fetchLinkedFlowsBatch(repo.id, routes.map(r => r.id));
|
|
2136
|
+
const flowMap = await this.fetchLinkedFlowsBatch(repo.id, routes.map((r) => r.id));
|
|
2002
2137
|
// Count how many routes share the same handler file (for middleware partial detection)
|
|
2003
2138
|
const routeCountByHandler = new Map();
|
|
2004
2139
|
for (const r of routes) {
|
|
@@ -2006,17 +2141,21 @@ export class LocalBackend {
|
|
|
2006
2141
|
routeCountByHandler.set(r.filePath, (routeCountByHandler.get(r.filePath) ?? 0) + 1);
|
|
2007
2142
|
}
|
|
2008
2143
|
}
|
|
2009
|
-
const results = routes.map(r => {
|
|
2144
|
+
const results = routes.map((r) => {
|
|
2010
2145
|
// Keys already normalized by fetchRoutesWithConsumers (quotes stripped)
|
|
2011
2146
|
const responseKeys = r.responseKeys ?? [];
|
|
2012
2147
|
const errorKeys = r.errorKeys ?? [];
|
|
2013
2148
|
const allKnownKeys = new Set([...responseKeys, ...errorKeys]);
|
|
2014
2149
|
// Build consumer list with mismatch detection
|
|
2015
|
-
const consumers = r.consumers.map(c => ({
|
|
2150
|
+
const consumers = r.consumers.map((c) => ({
|
|
2016
2151
|
name: c.name,
|
|
2017
2152
|
file: c.filePath,
|
|
2018
2153
|
accesses: c.accessedKeys ?? [],
|
|
2019
|
-
...(c.fetchCount && c.fetchCount > 1
|
|
2154
|
+
...(c.fetchCount && c.fetchCount > 1
|
|
2155
|
+
? {
|
|
2156
|
+
attributionNote: `This file fetches ${c.fetchCount} routes — accessed keys may belong to a different route.`,
|
|
2157
|
+
}
|
|
2158
|
+
: {}),
|
|
2020
2159
|
}));
|
|
2021
2160
|
// Detect mismatches: consumer accesses keys not in response shape
|
|
2022
2161
|
const mismatches = [];
|
|
@@ -2073,10 +2212,12 @@ export class LocalBackend {
|
|
|
2073
2212
|
error: errorKeys,
|
|
2074
2213
|
},
|
|
2075
2214
|
middleware: middlewareArr,
|
|
2076
|
-
...(middlewarePartial
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2215
|
+
...(middlewarePartial
|
|
2216
|
+
? {
|
|
2217
|
+
middlewareDetection: 'partial',
|
|
2218
|
+
middlewareNote: 'Middleware captured from first HTTP method export only — other methods in this handler may use different middleware chains.',
|
|
2219
|
+
}
|
|
2220
|
+
: {}),
|
|
2080
2221
|
consumers,
|
|
2081
2222
|
...(mismatches.length > 0 ? { mismatches } : {}),
|
|
2082
2223
|
executionFlows: flows,
|
|
@@ -2166,8 +2307,11 @@ export class LocalBackend {
|
|
|
2166
2307
|
if (clusters.length === 0)
|
|
2167
2308
|
return { error: `Cluster '${name}' not found` };
|
|
2168
2309
|
const rawClusters = clusters.map((c) => ({
|
|
2169
|
-
id: c.id || c[0],
|
|
2170
|
-
|
|
2310
|
+
id: c.id || c[0],
|
|
2311
|
+
label: c.label || c[1],
|
|
2312
|
+
heuristicLabel: c.heuristicLabel || c[2],
|
|
2313
|
+
cohesion: c.cohesion || c[3],
|
|
2314
|
+
symbolCount: c.symbolCount || c[4],
|
|
2171
2315
|
}));
|
|
2172
2316
|
let totalSymbols = 0, weightedCohesion = 0;
|
|
2173
2317
|
for (const c of rawClusters) {
|
|
@@ -2191,7 +2335,9 @@ export class LocalBackend {
|
|
|
2191
2335
|
subCommunities: rawClusters.length,
|
|
2192
2336
|
},
|
|
2193
2337
|
members: members.map((m) => ({
|
|
2194
|
-
name: m.name || m[0],
|
|
2338
|
+
name: m.name || m[0],
|
|
2339
|
+
type: m.type || m[1],
|
|
2340
|
+
filePath: m.filePath || m[2],
|
|
2195
2341
|
})),
|
|
2196
2342
|
};
|
|
2197
2343
|
}
|
|
@@ -2219,11 +2365,17 @@ export class LocalBackend {
|
|
|
2219
2365
|
`, { procId });
|
|
2220
2366
|
return {
|
|
2221
2367
|
process: {
|
|
2222
|
-
id: procId,
|
|
2223
|
-
|
|
2368
|
+
id: procId,
|
|
2369
|
+
label: proc.label || proc[1],
|
|
2370
|
+
heuristicLabel: proc.heuristicLabel || proc[2],
|
|
2371
|
+
processType: proc.processType || proc[3],
|
|
2372
|
+
stepCount: proc.stepCount || proc[4],
|
|
2224
2373
|
},
|
|
2225
2374
|
steps: steps.map((s) => ({
|
|
2226
|
-
step: s.step || s[3],
|
|
2375
|
+
step: s.step || s[3],
|
|
2376
|
+
name: s.name || s[0],
|
|
2377
|
+
type: s.type || s[1],
|
|
2378
|
+
filePath: s.filePath || s[2],
|
|
2227
2379
|
})),
|
|
2228
2380
|
};
|
|
2229
2381
|
}
|