@kodus/kodus-graph 0.2.8 → 0.2.10
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/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/analysis/blast-radius.d.ts +2 -0
- package/dist/analysis/blast-radius.js +55 -0
- package/dist/analysis/communities.d.ts +28 -0
- package/dist/analysis/communities.js +100 -0
- package/dist/analysis/context-builder.d.ts +34 -0
- package/dist/analysis/context-builder.js +92 -0
- package/dist/analysis/diff.d.ts +41 -0
- package/dist/analysis/diff.js +155 -0
- package/dist/analysis/enrich.d.ts +5 -0
- package/dist/analysis/enrich.js +126 -0
- package/dist/analysis/flows.d.ts +27 -0
- package/dist/analysis/flows.js +86 -0
- package/dist/analysis/inheritance.d.ts +3 -0
- package/dist/analysis/inheritance.js +31 -0
- package/dist/analysis/prompt-formatter.d.ts +2 -0
- package/dist/analysis/prompt-formatter.js +173 -0
- package/dist/analysis/risk-score.d.ts +4 -0
- package/dist/analysis/risk-score.js +51 -0
- package/dist/analysis/search.d.ts +11 -0
- package/dist/analysis/search.js +64 -0
- package/dist/analysis/test-gaps.d.ts +2 -0
- package/dist/analysis/test-gaps.js +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +210 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.js +116 -0
- package/dist/commands/communities.d.ts +8 -0
- package/dist/commands/communities.js +9 -0
- package/dist/commands/context.d.ts +12 -0
- package/dist/commands/context.js +130 -0
- package/dist/commands/diff.d.ts +9 -0
- package/dist/commands/diff.js +89 -0
- package/dist/commands/flows.d.ts +8 -0
- package/dist/commands/flows.js +9 -0
- package/dist/commands/parse.d.ts +11 -0
- package/dist/commands/parse.js +101 -0
- package/dist/commands/search.d.ts +12 -0
- package/dist/commands/search.js +27 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +154 -0
- package/dist/graph/builder.d.ts +6 -0
- package/dist/graph/builder.js +248 -0
- package/dist/graph/edges.d.ts +23 -0
- package/dist/graph/edges.js +159 -0
- package/dist/graph/json-writer.d.ts +9 -0
- package/dist/graph/json-writer.js +38 -0
- package/dist/graph/loader.d.ts +13 -0
- package/dist/graph/loader.js +101 -0
- package/dist/graph/merger.d.ts +7 -0
- package/dist/graph/merger.js +18 -0
- package/dist/graph/types.d.ts +252 -0
- package/dist/graph/types.js +1 -0
- package/dist/parser/batch.d.ts +5 -0
- package/dist/parser/batch.js +93 -0
- package/dist/parser/discovery.d.ts +7 -0
- package/dist/parser/discovery.js +61 -0
- package/dist/parser/extractor.d.ts +4 -0
- package/dist/parser/extractor.js +33 -0
- package/dist/parser/extractors/generic.d.ts +8 -0
- package/dist/parser/extractors/generic.js +471 -0
- package/dist/parser/extractors/python.d.ts +8 -0
- package/dist/parser/extractors/python.js +133 -0
- package/dist/parser/extractors/ruby.d.ts +8 -0
- package/dist/parser/extractors/ruby.js +153 -0
- package/dist/parser/extractors/typescript.d.ts +10 -0
- package/dist/parser/extractors/typescript.js +365 -0
- package/dist/parser/languages.d.ts +32 -0
- package/dist/parser/languages.js +304 -0
- package/dist/resolver/call-resolver.d.ts +36 -0
- package/dist/resolver/call-resolver.js +178 -0
- package/dist/resolver/external-detector.d.ts +11 -0
- package/dist/resolver/external-detector.js +820 -0
- package/dist/resolver/fs-cache.d.ts +8 -0
- package/dist/resolver/fs-cache.js +36 -0
- package/dist/resolver/import-map.d.ts +12 -0
- package/dist/resolver/import-map.js +21 -0
- package/dist/resolver/import-resolver.d.ts +19 -0
- package/dist/resolver/import-resolver.js +310 -0
- package/dist/resolver/languages/csharp.d.ts +3 -0
- package/dist/resolver/languages/csharp.js +94 -0
- package/dist/resolver/languages/go.d.ts +3 -0
- package/dist/resolver/languages/go.js +197 -0
- package/dist/resolver/languages/java.d.ts +1 -0
- package/dist/resolver/languages/java.js +193 -0
- package/dist/resolver/languages/php.d.ts +3 -0
- package/dist/resolver/languages/php.js +75 -0
- package/dist/resolver/languages/python.d.ts +11 -0
- package/dist/resolver/languages/python.js +127 -0
- package/dist/resolver/languages/ruby.d.ts +24 -0
- package/dist/resolver/languages/ruby.js +110 -0
- package/dist/resolver/languages/rust.d.ts +1 -0
- package/dist/resolver/languages/rust.js +197 -0
- package/dist/resolver/languages/typescript.d.ts +35 -0
- package/dist/resolver/languages/typescript.js +416 -0
- package/dist/resolver/re-export-resolver.d.ts +24 -0
- package/dist/resolver/re-export-resolver.js +57 -0
- package/dist/resolver/symbol-table.d.ts +17 -0
- package/dist/resolver/symbol-table.js +60 -0
- package/dist/shared/extract-calls.d.ts +26 -0
- package/dist/shared/extract-calls.js +57 -0
- package/dist/shared/file-hash.d.ts +3 -0
- package/dist/shared/file-hash.js +10 -0
- package/dist/shared/filters.d.ts +3 -0
- package/dist/shared/filters.js +240 -0
- package/dist/shared/logger.d.ts +6 -0
- package/dist/shared/logger.js +17 -0
- package/dist/shared/qualified-name.d.ts +1 -0
- package/dist/shared/qualified-name.js +9 -0
- package/dist/shared/safe-path.d.ts +6 -0
- package/dist/shared/safe-path.js +29 -0
- package/dist/shared/schemas.d.ts +43 -0
- package/dist/shared/schemas.js +30 -0
- package/dist/shared/temp.d.ts +11 -0
- package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
- package/package.json +20 -6
- package/src/analysis/blast-radius.ts +0 -54
- package/src/analysis/communities.ts +0 -135
- package/src/analysis/context-builder.ts +0 -130
- package/src/analysis/diff.ts +0 -169
- package/src/analysis/enrich.ts +0 -110
- package/src/analysis/flows.ts +0 -112
- package/src/analysis/inheritance.ts +0 -34
- package/src/analysis/prompt-formatter.ts +0 -175
- package/src/analysis/risk-score.ts +0 -62
- package/src/analysis/search.ts +0 -76
- package/src/analysis/test-gaps.ts +0 -21
- package/src/cli.ts +0 -210
- package/src/commands/analyze.ts +0 -128
- package/src/commands/communities.ts +0 -19
- package/src/commands/context.ts +0 -182
- package/src/commands/diff.ts +0 -96
- package/src/commands/flows.ts +0 -19
- package/src/commands/parse.ts +0 -124
- package/src/commands/search.ts +0 -41
- package/src/commands/update.ts +0 -166
- package/src/graph/builder.ts +0 -209
- package/src/graph/edges.ts +0 -101
- package/src/graph/json-writer.ts +0 -43
- package/src/graph/loader.ts +0 -113
- package/src/graph/merger.ts +0 -25
- package/src/graph/types.ts +0 -283
- package/src/parser/batch.ts +0 -82
- package/src/parser/discovery.ts +0 -75
- package/src/parser/extractor.ts +0 -37
- package/src/parser/extractors/generic.ts +0 -132
- package/src/parser/extractors/python.ts +0 -133
- package/src/parser/extractors/ruby.ts +0 -147
- package/src/parser/extractors/typescript.ts +0 -350
- package/src/parser/languages.ts +0 -122
- package/src/resolver/call-resolver.ts +0 -244
- package/src/resolver/import-map.ts +0 -27
- package/src/resolver/import-resolver.ts +0 -72
- package/src/resolver/languages/csharp.ts +0 -7
- package/src/resolver/languages/go.ts +0 -7
- package/src/resolver/languages/java.ts +0 -7
- package/src/resolver/languages/php.ts +0 -7
- package/src/resolver/languages/python.ts +0 -35
- package/src/resolver/languages/ruby.ts +0 -21
- package/src/resolver/languages/rust.ts +0 -7
- package/src/resolver/languages/typescript.ts +0 -168
- package/src/resolver/re-export-resolver.ts +0 -66
- package/src/resolver/symbol-table.ts +0 -67
- package/src/shared/extract-calls.ts +0 -75
- package/src/shared/file-hash.ts +0 -12
- package/src/shared/filters.ts +0 -243
- package/src/shared/logger.ts +0 -17
- package/src/shared/qualified-name.ts +0 -5
- package/src/shared/safe-path.ts +0 -31
- package/src/shared/schemas.ts +0 -32
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ImportEdge, RawGraph } from './types';
|
|
2
|
+
interface DerivedEdge {
|
|
3
|
+
source: string;
|
|
4
|
+
target: string;
|
|
5
|
+
file?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DerivedEdges {
|
|
8
|
+
inherits: DerivedEdge[];
|
|
9
|
+
implements: DerivedEdge[];
|
|
10
|
+
testedBy: DerivedEdge[];
|
|
11
|
+
contains: DerivedEdge[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Extract the "stem" from a test file name by stripping test-related
|
|
15
|
+
* prefixes/suffixes. Returns null if no test pattern was found.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractTestStem(testFile: string): string | null;
|
|
18
|
+
export declare function deriveEdges(graph: RawGraph, importEdges: ImportEdge[], symbolTable?: {
|
|
19
|
+
lookupGlobal(name: string): string[];
|
|
20
|
+
}, importMap?: {
|
|
21
|
+
lookup(file: string, name: string): string | null;
|
|
22
|
+
}): DerivedEdges;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { basename, extname } from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Extract the "stem" from a test file name by stripping test-related
|
|
4
|
+
* prefixes/suffixes. Returns null if no test pattern was found.
|
|
5
|
+
*/
|
|
6
|
+
export function extractTestStem(testFile) {
|
|
7
|
+
const base = basename(testFile, extname(testFile));
|
|
8
|
+
const cleaned = base
|
|
9
|
+
.replace(/_spec$/, '') // user_spec → user (Ruby/RSpec)
|
|
10
|
+
.replace(/_test$/, '') // user_test → user (Python/Go)
|
|
11
|
+
.replace(/^test_/, '') // test_user → user (Python)
|
|
12
|
+
.replace(/\.test$/, '') // user.test → user (JS/TS)
|
|
13
|
+
.replace(/\.spec$/, '') // user.spec → user (JS/TS)
|
|
14
|
+
.replace(/-test$/, '') // user-test → user
|
|
15
|
+
.replace(/-spec$/, '') // user-spec → user
|
|
16
|
+
.replace(/^spec_/, '') // spec_user → user
|
|
17
|
+
.replace(/Test$/, '') // UserTest → User (Java)
|
|
18
|
+
.replace(/Spec$/, ''); // UserSpec → User (Scala)
|
|
19
|
+
if (!cleaned || cleaned === base) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return cleaned;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a bare type name (e.g. "User", "IAuthService") to its qualified name
|
|
26
|
+
* using import map, same-file lookup, and global symbol table.
|
|
27
|
+
* Returns null if the name cannot be resolved (external class/interface).
|
|
28
|
+
*/
|
|
29
|
+
function resolveTypeName(name, file, graph, symbolTable, importMap) {
|
|
30
|
+
// 1. Check import map — was it imported in this file?
|
|
31
|
+
const importedFrom = importMap?.lookup(file, name);
|
|
32
|
+
if (importedFrom) {
|
|
33
|
+
// Look up the qualified name in the imported file
|
|
34
|
+
const candidates = symbolTable?.lookupGlobal(name) ?? [];
|
|
35
|
+
const match = candidates.find((q) => q.startsWith(`${importedFrom}::`));
|
|
36
|
+
if (match) {
|
|
37
|
+
return match;
|
|
38
|
+
}
|
|
39
|
+
// If importedFrom is not a local file (not in graph), it's an external package — skip
|
|
40
|
+
const isLocal = graph.classes.some((c) => c.file === importedFrom) ||
|
|
41
|
+
graph.interfaces.some((i) => i.file === importedFrom) ||
|
|
42
|
+
graph.functions.some((f) => f.file === importedFrom);
|
|
43
|
+
if (!isLocal) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// Fallback: construct qualified name from local import target
|
|
47
|
+
return `${importedFrom}::${name}`;
|
|
48
|
+
}
|
|
49
|
+
// 2. Check same file — class or interface defined in same file
|
|
50
|
+
const sameFileClass = graph.classes.find((other) => other.name === name && other.file === file);
|
|
51
|
+
if (sameFileClass) {
|
|
52
|
+
return sameFileClass.qualified;
|
|
53
|
+
}
|
|
54
|
+
const sameFileInterface = graph.interfaces.find((other) => other.name === name && other.file === file);
|
|
55
|
+
if (sameFileInterface) {
|
|
56
|
+
return sameFileInterface.qualified;
|
|
57
|
+
}
|
|
58
|
+
// 3. Check global symbol table — unique match only
|
|
59
|
+
const globalCandidates = symbolTable?.lookupGlobal(name) ?? [];
|
|
60
|
+
if (globalCandidates.length === 1) {
|
|
61
|
+
return globalCandidates[0];
|
|
62
|
+
}
|
|
63
|
+
// 4. External class/interface (React.Component, Error, etc.) — unresolvable
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export function deriveEdges(graph, importEdges, symbolTable, importMap) {
|
|
67
|
+
// INHERITS: class extends another class — resolve to qualified names
|
|
68
|
+
const inherits = [];
|
|
69
|
+
for (const c of graph.classes) {
|
|
70
|
+
if (!c.extends) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const resolved = resolveTypeName(c.extends, c.file, graph, symbolTable, importMap);
|
|
74
|
+
if (resolved) {
|
|
75
|
+
inherits.push({ source: c.qualified, target: resolved, file: c.file });
|
|
76
|
+
}
|
|
77
|
+
// Skip unresolvable external classes (React.Component, Error, etc.)
|
|
78
|
+
}
|
|
79
|
+
// IMPLEMENTS: class implements interface(s) — resolve to qualified names
|
|
80
|
+
const implements_ = [];
|
|
81
|
+
for (const c of graph.classes) {
|
|
82
|
+
for (const iface of c.implements) {
|
|
83
|
+
const resolved = resolveTypeName(iface, c.file, graph, symbolTable, importMap);
|
|
84
|
+
if (resolved) {
|
|
85
|
+
implements_.push({ source: c.qualified, target: resolved, file: c.file });
|
|
86
|
+
}
|
|
87
|
+
// Skip unresolvable external interfaces
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// TESTED_BY: two heuristics, deduplicated
|
|
91
|
+
const testFiles = new Set(graph.tests.map((t) => t.file));
|
|
92
|
+
const testedBySet = new Set();
|
|
93
|
+
const testedBy = [];
|
|
94
|
+
const addTestedBy = (source, target) => {
|
|
95
|
+
const key = `${source}|${target}`;
|
|
96
|
+
if (testedBySet.has(key)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
testedBySet.add(key);
|
|
100
|
+
testedBy.push({ source, target });
|
|
101
|
+
};
|
|
102
|
+
// Heuristic 1: Resolved imports from test files (high signal)
|
|
103
|
+
for (const e of importEdges) {
|
|
104
|
+
if (testFiles.has(e.source) && e.resolved) {
|
|
105
|
+
addTestedBy(e.target, e.source);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Heuristic 2: File-name matching (catches Ruby, Python, and any
|
|
109
|
+
// language where imports don't resolve)
|
|
110
|
+
const allSourceFiles = new Set();
|
|
111
|
+
for (const f of graph.functions) {
|
|
112
|
+
allSourceFiles.add(f.file);
|
|
113
|
+
}
|
|
114
|
+
for (const c of graph.classes) {
|
|
115
|
+
allSourceFiles.add(c.file);
|
|
116
|
+
}
|
|
117
|
+
for (const i of graph.interfaces) {
|
|
118
|
+
allSourceFiles.add(i.file);
|
|
119
|
+
}
|
|
120
|
+
for (const e of graph.enums) {
|
|
121
|
+
allSourceFiles.add(e.file);
|
|
122
|
+
}
|
|
123
|
+
for (const tf of testFiles) {
|
|
124
|
+
allSourceFiles.delete(tf);
|
|
125
|
+
}
|
|
126
|
+
const sourceByBase = new Map();
|
|
127
|
+
for (const file of allSourceFiles) {
|
|
128
|
+
const base = basename(file, extname(file));
|
|
129
|
+
const list = sourceByBase.get(base);
|
|
130
|
+
if (list) {
|
|
131
|
+
list.push(file);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
sourceByBase.set(base, [file]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const testFile of testFiles) {
|
|
138
|
+
const stem = extractTestStem(testFile);
|
|
139
|
+
if (!stem) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const matches = sourceByBase.get(stem);
|
|
143
|
+
if (!matches) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
for (const sourceFile of matches) {
|
|
147
|
+
addTestedBy(sourceFile, testFile);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// CONTAINS: file contains function/class
|
|
151
|
+
const contains = [];
|
|
152
|
+
for (const f of graph.functions) {
|
|
153
|
+
contains.push({ source: f.file, target: f.qualified });
|
|
154
|
+
}
|
|
155
|
+
for (const c of graph.classes) {
|
|
156
|
+
contains.push({ source: c.file, target: c.qualified });
|
|
157
|
+
}
|
|
158
|
+
return { inherits, implements: implements_, testedBy, contains };
|
|
159
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GraphEdge, GraphNode, ParseMetadata } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Write graph output as JSON to disk using incremental serialization.
|
|
4
|
+
*
|
|
5
|
+
* Instead of JSON.stringify on the full output (which creates a ~100-300 MB
|
|
6
|
+
* string for large repos), this writes each node/edge individually.
|
|
7
|
+
* Peak memory: only one JSON.stringify(singleNode) string at a time (~1 KB).
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeGraphJSON(out: string, metadata: ParseMetadata, nodes: GraphNode[], edges: GraphEdge[]): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { closeSync, openSync, writeSync } from 'fs';
|
|
2
|
+
/**
|
|
3
|
+
* Write graph output as JSON to disk using incremental serialization.
|
|
4
|
+
*
|
|
5
|
+
* Instead of JSON.stringify on the full output (which creates a ~100-300 MB
|
|
6
|
+
* string for large repos), this writes each node/edge individually.
|
|
7
|
+
* Peak memory: only one JSON.stringify(singleNode) string at a time (~1 KB).
|
|
8
|
+
*/
|
|
9
|
+
export function writeGraphJSON(out, metadata, nodes, edges) {
|
|
10
|
+
const fd = openSync(out, 'w');
|
|
11
|
+
try {
|
|
12
|
+
writeSync(fd, '{"metadata":');
|
|
13
|
+
writeSync(fd, JSON.stringify(metadata));
|
|
14
|
+
// Nodes
|
|
15
|
+
writeSync(fd, ',"nodes":[');
|
|
16
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
17
|
+
if (i > 0) {
|
|
18
|
+
writeSync(fd, ',');
|
|
19
|
+
}
|
|
20
|
+
writeSync(fd, '\n');
|
|
21
|
+
writeSync(fd, JSON.stringify(nodes[i]));
|
|
22
|
+
}
|
|
23
|
+
writeSync(fd, '\n]');
|
|
24
|
+
// Edges
|
|
25
|
+
writeSync(fd, ',"edges":[');
|
|
26
|
+
for (let i = 0; i < edges.length; i++) {
|
|
27
|
+
if (i > 0) {
|
|
28
|
+
writeSync(fd, ',');
|
|
29
|
+
}
|
|
30
|
+
writeSync(fd, '\n');
|
|
31
|
+
writeSync(fd, JSON.stringify(edges[i]));
|
|
32
|
+
}
|
|
33
|
+
writeSync(fd, '\n]}');
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
closeSync(fd);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GraphData, GraphEdge, GraphNode, ParseMetadata } from './types';
|
|
2
|
+
export interface IndexedGraph {
|
|
3
|
+
nodes: GraphNode[];
|
|
4
|
+
edges: GraphEdge[];
|
|
5
|
+
byQualified: Map<string, GraphNode>;
|
|
6
|
+
byFile: Map<string, GraphNode[]>;
|
|
7
|
+
adjacency: Map<string, GraphEdge[]>;
|
|
8
|
+
reverseAdjacency: Map<string, GraphEdge[]>;
|
|
9
|
+
edgesByKind: Map<string, GraphEdge[]>;
|
|
10
|
+
metadata: ParseMetadata;
|
|
11
|
+
}
|
|
12
|
+
export declare function indexGraph(data: GraphData, metadata?: ParseMetadata): IndexedGraph;
|
|
13
|
+
export declare function loadGraph(path: string): IndexedGraph;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/graph/loader.ts
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const ParseOutputSchema = z.object({
|
|
5
|
+
metadata: z.object({
|
|
6
|
+
repo_dir: z.string(),
|
|
7
|
+
files_parsed: z.number(),
|
|
8
|
+
total_nodes: z.number(),
|
|
9
|
+
total_edges: z.number(),
|
|
10
|
+
duration_ms: z.number(),
|
|
11
|
+
parse_errors: z.number(),
|
|
12
|
+
extract_errors: z.number(),
|
|
13
|
+
files_unchanged: z.number().optional(),
|
|
14
|
+
incremental: z.boolean().optional(),
|
|
15
|
+
}),
|
|
16
|
+
nodes: z.array(z.object({
|
|
17
|
+
kind: z.enum(['Function', 'Method', 'Constructor', 'Class', 'Interface', 'Enum', 'Test']),
|
|
18
|
+
name: z.string(),
|
|
19
|
+
qualified_name: z.string(),
|
|
20
|
+
file_path: z.string(),
|
|
21
|
+
line_start: z.number(),
|
|
22
|
+
line_end: z.number(),
|
|
23
|
+
language: z.string(),
|
|
24
|
+
is_test: z.boolean(),
|
|
25
|
+
file_hash: z.string().optional(),
|
|
26
|
+
parent_name: z.string().optional(),
|
|
27
|
+
params: z.string().optional(),
|
|
28
|
+
return_type: z.string().optional(),
|
|
29
|
+
modifiers: z.string().optional(),
|
|
30
|
+
})),
|
|
31
|
+
edges: z.array(z.object({
|
|
32
|
+
kind: z.enum(['CALLS', 'IMPORTS', 'INHERITS', 'IMPLEMENTS', 'TESTED_BY', 'CONTAINS']),
|
|
33
|
+
source_qualified: z.string(),
|
|
34
|
+
target_qualified: z.string(),
|
|
35
|
+
file_path: z.string(),
|
|
36
|
+
line: z.number(),
|
|
37
|
+
confidence: z.number().optional(),
|
|
38
|
+
})),
|
|
39
|
+
});
|
|
40
|
+
export function indexGraph(data, metadata) {
|
|
41
|
+
const { nodes, edges } = data;
|
|
42
|
+
const meta = metadata ?? {
|
|
43
|
+
repo_dir: '',
|
|
44
|
+
files_parsed: 0,
|
|
45
|
+
total_nodes: nodes.length,
|
|
46
|
+
total_edges: edges.length,
|
|
47
|
+
duration_ms: 0,
|
|
48
|
+
parse_errors: 0,
|
|
49
|
+
extract_errors: 0,
|
|
50
|
+
};
|
|
51
|
+
const byQualified = new Map();
|
|
52
|
+
const byFile = new Map();
|
|
53
|
+
const adjacency = new Map();
|
|
54
|
+
const reverseAdjacency = new Map();
|
|
55
|
+
const edgesByKind = new Map();
|
|
56
|
+
for (const node of nodes) {
|
|
57
|
+
byQualified.set(node.qualified_name, node);
|
|
58
|
+
const list = byFile.get(node.file_path);
|
|
59
|
+
if (list) {
|
|
60
|
+
list.push(node);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
byFile.set(node.file_path, [node]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const edge of edges) {
|
|
67
|
+
const fwd = adjacency.get(edge.source_qualified);
|
|
68
|
+
if (fwd) {
|
|
69
|
+
fwd.push(edge);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
adjacency.set(edge.source_qualified, [edge]);
|
|
73
|
+
}
|
|
74
|
+
const rev = reverseAdjacency.get(edge.target_qualified);
|
|
75
|
+
if (rev) {
|
|
76
|
+
rev.push(edge);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
reverseAdjacency.set(edge.target_qualified, [edge]);
|
|
80
|
+
}
|
|
81
|
+
const byKind = edgesByKind.get(edge.kind);
|
|
82
|
+
if (byKind) {
|
|
83
|
+
byKind.push(edge);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
edgesByKind.set(edge.kind, [edge]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { nodes, edges, byQualified, byFile, adjacency, reverseAdjacency, edgesByKind, metadata: meta };
|
|
90
|
+
}
|
|
91
|
+
export function loadGraph(path) {
|
|
92
|
+
let raw;
|
|
93
|
+
try {
|
|
94
|
+
raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
throw new Error(`Failed to read graph file: ${path} — ${String(err)}`);
|
|
98
|
+
}
|
|
99
|
+
const parsed = ParseOutputSchema.parse(raw);
|
|
100
|
+
return indexGraph({ nodes: parsed.nodes, edges: parsed.edges }, parsed.metadata);
|
|
101
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GraphData, MainGraphInput } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Merge local parse (PR changed files) with the main graph (from Postgres).
|
|
4
|
+
* Replaces all nodes/edges from changed files with the local parse.
|
|
5
|
+
* Keeps everything else from the main graph intact.
|
|
6
|
+
*/
|
|
7
|
+
export declare function mergeGraphs(mainGraph: MainGraphInput | null, localParse: GraphData, changedFiles: string[]): GraphData;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge local parse (PR changed files) with the main graph (from Postgres).
|
|
3
|
+
* Replaces all nodes/edges from changed files with the local parse.
|
|
4
|
+
* Keeps everything else from the main graph intact.
|
|
5
|
+
*/
|
|
6
|
+
export function mergeGraphs(mainGraph, localParse, changedFiles) {
|
|
7
|
+
if (!mainGraph) {
|
|
8
|
+
return localParse;
|
|
9
|
+
}
|
|
10
|
+
const changedSet = new Set(changedFiles);
|
|
11
|
+
// Keep main graph nodes/edges NOT in changed files
|
|
12
|
+
const mainNodes = mainGraph.nodes.filter((n) => !changedSet.has(n.file_path));
|
|
13
|
+
const mainEdges = mainGraph.edges.filter((e) => !changedSet.has(e.file_path));
|
|
14
|
+
return {
|
|
15
|
+
nodes: [...mainNodes, ...localParse.nodes],
|
|
16
|
+
edges: [...mainEdges, ...localParse.edges],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { ContractDiff } from '../analysis/diff';
|
|
2
|
+
export type NodeKind = 'Function' | 'Method' | 'Constructor' | 'Class' | 'Interface' | 'Enum' | 'Test';
|
|
3
|
+
export type EdgeKind = 'CALLS' | 'IMPORTS' | 'INHERITS' | 'IMPLEMENTS' | 'TESTED_BY' | 'CONTAINS';
|
|
4
|
+
export interface GraphNode {
|
|
5
|
+
kind: NodeKind;
|
|
6
|
+
ast_kind?: string;
|
|
7
|
+
name: string;
|
|
8
|
+
qualified_name: string;
|
|
9
|
+
file_path: string;
|
|
10
|
+
line_start: number;
|
|
11
|
+
line_end: number;
|
|
12
|
+
language: string;
|
|
13
|
+
parent_name?: string;
|
|
14
|
+
params?: string;
|
|
15
|
+
return_type?: string;
|
|
16
|
+
modifiers?: string;
|
|
17
|
+
is_test: boolean;
|
|
18
|
+
file_hash?: string;
|
|
19
|
+
content_hash?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface GraphEdge {
|
|
22
|
+
kind: EdgeKind;
|
|
23
|
+
source_qualified: string;
|
|
24
|
+
target_qualified: string;
|
|
25
|
+
file_path: string;
|
|
26
|
+
line: number;
|
|
27
|
+
confidence?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface GraphData {
|
|
30
|
+
nodes: GraphNode[];
|
|
31
|
+
edges: GraphEdge[];
|
|
32
|
+
}
|
|
33
|
+
export interface ParseMetadata {
|
|
34
|
+
repo_dir: string;
|
|
35
|
+
files_parsed: number;
|
|
36
|
+
total_nodes: number;
|
|
37
|
+
total_edges: number;
|
|
38
|
+
duration_ms: number;
|
|
39
|
+
parse_errors: number;
|
|
40
|
+
extract_errors: number;
|
|
41
|
+
files_unchanged?: number;
|
|
42
|
+
incremental?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface ParseOutput {
|
|
45
|
+
metadata: ParseMetadata;
|
|
46
|
+
nodes: GraphNode[];
|
|
47
|
+
edges: GraphEdge[];
|
|
48
|
+
}
|
|
49
|
+
export interface BlastRadiusResult {
|
|
50
|
+
total_functions: number;
|
|
51
|
+
total_files: number;
|
|
52
|
+
by_depth: Record<string, string[]>;
|
|
53
|
+
}
|
|
54
|
+
export interface RiskFactor {
|
|
55
|
+
weight: number;
|
|
56
|
+
value: number;
|
|
57
|
+
detail: string;
|
|
58
|
+
}
|
|
59
|
+
export interface RiskScoreResult {
|
|
60
|
+
level: 'LOW' | 'MEDIUM' | 'HIGH';
|
|
61
|
+
score: number;
|
|
62
|
+
factors: {
|
|
63
|
+
blast_radius: RiskFactor;
|
|
64
|
+
test_gaps: RiskFactor;
|
|
65
|
+
complexity: RiskFactor;
|
|
66
|
+
inheritance: RiskFactor;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export interface TestGap {
|
|
70
|
+
function: string;
|
|
71
|
+
file_path: string;
|
|
72
|
+
line_start: number;
|
|
73
|
+
}
|
|
74
|
+
export interface AnalysisOutput {
|
|
75
|
+
blast_radius: BlastRadiusResult;
|
|
76
|
+
risk_score: RiskScoreResult;
|
|
77
|
+
test_gaps: TestGap[];
|
|
78
|
+
}
|
|
79
|
+
export interface ContextMetadata {
|
|
80
|
+
changed_functions: number;
|
|
81
|
+
caller_count: number;
|
|
82
|
+
callee_count: number;
|
|
83
|
+
untested_count: number;
|
|
84
|
+
blast_radius: {
|
|
85
|
+
functions: number;
|
|
86
|
+
files: number;
|
|
87
|
+
};
|
|
88
|
+
risk_level: 'LOW' | 'MEDIUM' | 'HIGH';
|
|
89
|
+
risk_score: number;
|
|
90
|
+
}
|
|
91
|
+
export interface ContextOutput {
|
|
92
|
+
text: string;
|
|
93
|
+
metadata: ContextMetadata;
|
|
94
|
+
}
|
|
95
|
+
export interface MainGraphInput {
|
|
96
|
+
repo_id: string;
|
|
97
|
+
sha: string;
|
|
98
|
+
nodes: GraphNode[];
|
|
99
|
+
edges: GraphEdge[];
|
|
100
|
+
}
|
|
101
|
+
export interface CallerRef {
|
|
102
|
+
qualified_name: string;
|
|
103
|
+
name: string;
|
|
104
|
+
file_path: string;
|
|
105
|
+
line: number;
|
|
106
|
+
confidence: number;
|
|
107
|
+
}
|
|
108
|
+
export interface CalleeRef {
|
|
109
|
+
qualified_name: string;
|
|
110
|
+
name: string;
|
|
111
|
+
file_path: string;
|
|
112
|
+
signature: string;
|
|
113
|
+
}
|
|
114
|
+
export interface EnrichedFunction {
|
|
115
|
+
qualified_name: string;
|
|
116
|
+
name: string;
|
|
117
|
+
kind: NodeKind;
|
|
118
|
+
signature: string;
|
|
119
|
+
file_path: string;
|
|
120
|
+
line_start: number;
|
|
121
|
+
line_end: number;
|
|
122
|
+
callers: CallerRef[];
|
|
123
|
+
callees: CalleeRef[];
|
|
124
|
+
has_test_coverage: boolean;
|
|
125
|
+
diff_changes: string[];
|
|
126
|
+
contract_diffs: ContractDiff[];
|
|
127
|
+
caller_impact?: string;
|
|
128
|
+
is_new: boolean;
|
|
129
|
+
in_flows: string[];
|
|
130
|
+
}
|
|
131
|
+
export interface AffectedFlow {
|
|
132
|
+
entry_point: string;
|
|
133
|
+
type: 'test' | 'http';
|
|
134
|
+
touches_changed: string[];
|
|
135
|
+
depth: number;
|
|
136
|
+
path: string[];
|
|
137
|
+
}
|
|
138
|
+
export interface InheritanceEntry {
|
|
139
|
+
qualified_name: string;
|
|
140
|
+
file_path: string;
|
|
141
|
+
extends?: string;
|
|
142
|
+
implements: string[];
|
|
143
|
+
children: string[];
|
|
144
|
+
}
|
|
145
|
+
export interface ContextAnalysisMetadata {
|
|
146
|
+
changed_functions_count: number;
|
|
147
|
+
total_callers: number;
|
|
148
|
+
total_callees: number;
|
|
149
|
+
untested_count: number;
|
|
150
|
+
affected_flows_count: number;
|
|
151
|
+
duration_ms: number;
|
|
152
|
+
min_confidence: number;
|
|
153
|
+
}
|
|
154
|
+
export interface RawFunction {
|
|
155
|
+
name: string;
|
|
156
|
+
file: string;
|
|
157
|
+
line_start: number;
|
|
158
|
+
line_end: number;
|
|
159
|
+
params: string;
|
|
160
|
+
returnType: string;
|
|
161
|
+
kind: 'Function' | 'Method' | 'Constructor';
|
|
162
|
+
ast_kind: string;
|
|
163
|
+
className: string;
|
|
164
|
+
qualified: string;
|
|
165
|
+
content_hash?: string;
|
|
166
|
+
}
|
|
167
|
+
export interface RawClass {
|
|
168
|
+
name: string;
|
|
169
|
+
file: string;
|
|
170
|
+
line_start: number;
|
|
171
|
+
line_end: number;
|
|
172
|
+
extends: string;
|
|
173
|
+
implements: string[];
|
|
174
|
+
ast_kind: string;
|
|
175
|
+
qualified: string;
|
|
176
|
+
content_hash?: string;
|
|
177
|
+
}
|
|
178
|
+
export interface RawInterface {
|
|
179
|
+
name: string;
|
|
180
|
+
file: string;
|
|
181
|
+
line_start: number;
|
|
182
|
+
line_end: number;
|
|
183
|
+
methods: string[];
|
|
184
|
+
ast_kind: string;
|
|
185
|
+
qualified: string;
|
|
186
|
+
content_hash?: string;
|
|
187
|
+
}
|
|
188
|
+
export interface RawEnum {
|
|
189
|
+
name: string;
|
|
190
|
+
file: string;
|
|
191
|
+
line_start: number;
|
|
192
|
+
line_end: number;
|
|
193
|
+
ast_kind: string;
|
|
194
|
+
qualified: string;
|
|
195
|
+
content_hash?: string;
|
|
196
|
+
}
|
|
197
|
+
export interface RawTest {
|
|
198
|
+
name: string;
|
|
199
|
+
file: string;
|
|
200
|
+
line_start: number;
|
|
201
|
+
line_end: number;
|
|
202
|
+
ast_kind: string;
|
|
203
|
+
qualified: string;
|
|
204
|
+
content_hash?: string;
|
|
205
|
+
}
|
|
206
|
+
export interface RawImport {
|
|
207
|
+
module: string;
|
|
208
|
+
file: string;
|
|
209
|
+
line: number;
|
|
210
|
+
names: string[];
|
|
211
|
+
lang: string;
|
|
212
|
+
}
|
|
213
|
+
export interface RawReExport {
|
|
214
|
+
module: string;
|
|
215
|
+
file: string;
|
|
216
|
+
line: number;
|
|
217
|
+
}
|
|
218
|
+
export interface RawCallSite {
|
|
219
|
+
source: string;
|
|
220
|
+
callName: string;
|
|
221
|
+
line: number;
|
|
222
|
+
diField?: string;
|
|
223
|
+
resolveInClass?: string;
|
|
224
|
+
}
|
|
225
|
+
export interface RawCallEdge {
|
|
226
|
+
source: string;
|
|
227
|
+
target: string;
|
|
228
|
+
callName: string;
|
|
229
|
+
line: number;
|
|
230
|
+
confidence: number;
|
|
231
|
+
}
|
|
232
|
+
export interface ImportEdge {
|
|
233
|
+
source: string;
|
|
234
|
+
target: string;
|
|
235
|
+
resolved: boolean;
|
|
236
|
+
line: number;
|
|
237
|
+
}
|
|
238
|
+
export interface RawGraph {
|
|
239
|
+
functions: RawFunction[];
|
|
240
|
+
classes: RawClass[];
|
|
241
|
+
interfaces: RawInterface[];
|
|
242
|
+
enums: RawEnum[];
|
|
243
|
+
tests: RawTest[];
|
|
244
|
+
imports: RawImport[];
|
|
245
|
+
reExports: RawReExport[];
|
|
246
|
+
rawCalls: RawCallSite[];
|
|
247
|
+
diMaps: Map<string, Map<string, string>>;
|
|
248
|
+
}
|
|
249
|
+
export interface ParseBatchResult extends RawGraph {
|
|
250
|
+
parseErrors: number;
|
|
251
|
+
extractErrors: number;
|
|
252
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|