@abhinav2203/codeflow-core 0.1.0 → 1.0.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/dist/analyzer/build.d.ts +1 -1
- package/dist/analyzer/build.js +6 -6
- package/dist/analyzer/index.d.ts +4 -2
- package/dist/analyzer/index.js +3 -2
- package/dist/analyzer/repo-multi.d.ts +6 -0
- package/dist/analyzer/repo-multi.js +246 -0
- package/dist/analyzer/repo-multi.test.d.ts +1 -0
- package/dist/analyzer/repo-multi.test.js +208 -0
- package/dist/analyzer/repo.d.ts +1 -1
- package/dist/analyzer/repo.js +2 -2
- package/dist/analyzer/test-hook-check.d.ts +1 -0
- package/dist/analyzer/test-hook-check.js +2 -0
- package/dist/analyzer/tree-sitter-analyzer.d.ts +34 -0
- package/dist/analyzer/tree-sitter-analyzer.js +617 -0
- package/dist/analyzer/tree-sitter-loader.d.ts +8 -0
- package/dist/analyzer/tree-sitter-loader.js +97 -0
- package/dist/analyzer/tree-sitter-queries.d.ts +10 -0
- package/dist/analyzer/tree-sitter-queries.js +285 -0
- package/dist/conflicts/index.d.ts +1 -1
- package/dist/conflicts/index.js +1 -1
- package/dist/export/index.d.ts +1 -1
- package/dist/export/index.js +4 -4
- package/dist/index.d.ts +5 -5
- package/dist/index.js +5 -5
- package/dist/internal/codegen.d.ts +1 -1
- package/dist/internal/codegen.js +1 -1
- package/dist/internal/phases.d.ts +1 -1
- package/dist/internal/phases.js +1 -1
- package/dist/internal/plan.d.ts +1 -1
- package/dist/internal/plan.js +1 -1
- package/dist/internal/prd.d.ts +1 -1
- package/dist/internal/prd.js +2 -2
- package/dist/internal/utils.d.ts +1 -1
- package/dist/internal/utils.js +1 -1
- package/dist/storage/store-paths.js +1 -1
- package/dist/store-paths.d.ts +1 -1
- package/dist/store-paths.js +1 -1
- package/package.json +13 -2
package/dist/analyzer/build.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { BlueprintGraph, BuildBlueprintRequest } from "../schema/index";
|
|
1
|
+
import type { BlueprintGraph, BuildBlueprintRequest } from "../schema/index.js";
|
|
2
2
|
export declare const buildBlueprintGraph: (request: BuildBlueprintRequest) => Promise<BlueprintGraph>;
|
package/dist/analyzer/build.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { emptyContract } from "../schema/index";
|
|
3
|
-
import { parsePrd } from "../internal/prd";
|
|
4
|
-
import { withSpecDrafts } from "../internal/phases";
|
|
5
|
-
import { analyzeTypeScriptRepo } from "./repo";
|
|
6
|
-
import { createNode, dedupeEdges, mergeContracts, mergeSourceRefs } from "../internal/utils";
|
|
2
|
+
import { emptyContract } from "../schema/index.js";
|
|
3
|
+
import { parsePrd } from "../internal/prd.js";
|
|
4
|
+
import { withSpecDrafts } from "../internal/phases.js";
|
|
5
|
+
import { analyzeTypeScriptRepo } from "./repo.js";
|
|
6
|
+
import { createNode, dedupeEdges, mergeContracts, mergeSourceRefs } from "../internal/utils.js";
|
|
7
7
|
const mergeNodes = (nodes) => {
|
|
8
8
|
const map = new Map();
|
|
9
9
|
for (const node of nodes) {
|
|
10
|
-
const dedupeKey = `${node.kind}:${node.path ?? node.name}`;
|
|
10
|
+
const dedupeKey = `${node.kind}:${node.path ?? ""}:${node.name}`;
|
|
11
11
|
const existing = map.get(dedupeKey);
|
|
12
12
|
if (!existing) {
|
|
13
13
|
map.set(dedupeKey, node);
|
package/dist/analyzer/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export { buildBlueprintGraph } from "./build";
|
|
2
|
-
export { analyzeTypeScriptRepo } from "./repo";
|
|
1
|
+
export { buildBlueprintGraph } from "./build.js";
|
|
2
|
+
export { analyzeTypeScriptRepo } from "./repo.js";
|
|
3
|
+
export { analyzeRepo } from "./repo-multi.js";
|
|
4
|
+
export type { AnalyzeRepoOptions } from "./repo-multi.js";
|
package/dist/analyzer/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { buildBlueprintGraph } from "./build";
|
|
2
|
-
export { analyzeTypeScriptRepo } from "./repo";
|
|
1
|
+
export { buildBlueprintGraph } from "./build.js";
|
|
2
|
+
export { analyzeTypeScriptRepo } from "./repo.js";
|
|
3
|
+
export { analyzeRepo } from "./repo-multi.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type RepoGraphPart = Omit<import("../schema/index.js").BlueprintGraph, "projectName" | "mode" | "generatedAt">;
|
|
2
|
+
export interface AnalyzeRepoOptions {
|
|
3
|
+
excludePatterns?: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare const analyzeRepo: (repoPath: string, options?: AnalyzeRepoOptions) => Promise<RepoGraphPart>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { emptyContract } from "../schema/index.js";
|
|
4
|
+
import { createNode, dedupeEdges, mergeContracts, toPosixPath } from "../internal/utils.js";
|
|
5
|
+
import { SUPPORTED_EXTENSIONS } from "./tree-sitter-loader.js";
|
|
6
|
+
import { extractNodesFromFile } from "./tree-sitter-analyzer.js";
|
|
7
|
+
const DEFAULT_EXCLUDE_DIRS = new Set([
|
|
8
|
+
"node_modules",
|
|
9
|
+
".git",
|
|
10
|
+
"dist",
|
|
11
|
+
"build",
|
|
12
|
+
"target",
|
|
13
|
+
"__pycache__",
|
|
14
|
+
"vendor",
|
|
15
|
+
".venv"
|
|
16
|
+
]);
|
|
17
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
18
|
+
".ts", ".tsx", ".js", ".jsx",
|
|
19
|
+
".go", ".py",
|
|
20
|
+
".c", ".cpp", ".cc", ".cxx", ".h", ".hpp",
|
|
21
|
+
".rs"
|
|
22
|
+
]);
|
|
23
|
+
const walkDirectory = async (dir, excludeDirs) => {
|
|
24
|
+
const files = [];
|
|
25
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const fullPath = path.join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
if (excludeDirs.has(entry.name))
|
|
30
|
+
continue;
|
|
31
|
+
files.push(...await walkDirectory(fullPath, excludeDirs));
|
|
32
|
+
}
|
|
33
|
+
else if (entry.isFile()) {
|
|
34
|
+
const ext = path.extname(entry.name);
|
|
35
|
+
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
36
|
+
files.push(fullPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
};
|
|
42
|
+
export const analyzeRepo = async (repoPath, options) => {
|
|
43
|
+
const repoStat = await fs.stat(repoPath).catch(() => null);
|
|
44
|
+
if (!repoStat?.isDirectory()) {
|
|
45
|
+
throw new Error(`Repo path does not exist or is not a directory: ${repoPath}`);
|
|
46
|
+
}
|
|
47
|
+
const excludeDirs = new Set(DEFAULT_EXCLUDE_DIRS);
|
|
48
|
+
if (options?.excludePatterns) {
|
|
49
|
+
for (const p of options.excludePatterns)
|
|
50
|
+
excludeDirs.add(p);
|
|
51
|
+
}
|
|
52
|
+
const files = await walkDirectory(repoPath, excludeDirs);
|
|
53
|
+
const warnings = [];
|
|
54
|
+
if (files.length === 0) {
|
|
55
|
+
warnings.push(`No supported source files found under ${repoPath}. Supported: ${[...SOURCE_EXTENSIONS].join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
const allNodes = [];
|
|
58
|
+
const allSymbolIndex = new Map();
|
|
59
|
+
const allCallEdges = [];
|
|
60
|
+
const allImportEdges = [];
|
|
61
|
+
const allInheritEdges = [];
|
|
62
|
+
for (const filePath of files) {
|
|
63
|
+
const relativePath = toPosixPath(path.relative(repoPath, filePath));
|
|
64
|
+
const result = await extractNodesFromFile(filePath, relativePath);
|
|
65
|
+
allNodes.push(...result.nodes);
|
|
66
|
+
for (const [key, id] of result.symbolIndex) {
|
|
67
|
+
allSymbolIndex.set(key, id);
|
|
68
|
+
}
|
|
69
|
+
allCallEdges.push(...result.callEdges);
|
|
70
|
+
allImportEdges.push(...result.importEdges);
|
|
71
|
+
allInheritEdges.push(...result.inheritEdges);
|
|
72
|
+
}
|
|
73
|
+
const nodeMap = new Map();
|
|
74
|
+
for (const n of allNodes) {
|
|
75
|
+
nodeMap.set(n.nodeId, createNode({
|
|
76
|
+
id: n.nodeId,
|
|
77
|
+
kind: n.kind,
|
|
78
|
+
name: n.name,
|
|
79
|
+
summary: n.summary,
|
|
80
|
+
path: n.path,
|
|
81
|
+
ownerId: n.ownerId,
|
|
82
|
+
signature: n.signature || undefined,
|
|
83
|
+
contract: mergeContracts(emptyContract(), {
|
|
84
|
+
...emptyContract(),
|
|
85
|
+
summary: n.summary,
|
|
86
|
+
responsibilities: [n.summary]
|
|
87
|
+
}),
|
|
88
|
+
sourceRefs: n.sourceRefs
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
const edges = [];
|
|
92
|
+
// Build module lookup map for O(1) import resolution
|
|
93
|
+
const moduleLookup = new Map();
|
|
94
|
+
for (const node of nodeMap.values()) {
|
|
95
|
+
if (node.kind !== "module" || node.path == null)
|
|
96
|
+
continue;
|
|
97
|
+
const normalizedPath = toPosixPath(node.path);
|
|
98
|
+
const pathWithoutExtension = normalizedPath.slice(0, normalizedPath.length - path.extname(normalizedPath).length);
|
|
99
|
+
const basenameWithoutExtension = path.posix.basename(pathWithoutExtension);
|
|
100
|
+
// Map exact path
|
|
101
|
+
if (!moduleLookup.has(normalizedPath)) {
|
|
102
|
+
moduleLookup.set(normalizedPath, node);
|
|
103
|
+
}
|
|
104
|
+
// Map path without extension
|
|
105
|
+
if (!moduleLookup.has(pathWithoutExtension)) {
|
|
106
|
+
moduleLookup.set(pathWithoutExtension, node);
|
|
107
|
+
}
|
|
108
|
+
// Map basename without extension
|
|
109
|
+
if (!moduleLookup.has(basenameWithoutExtension)) {
|
|
110
|
+
moduleLookup.set(basenameWithoutExtension, node);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Helper to build deterministic import candidate paths
|
|
114
|
+
const buildImportCandidatePaths = (fromModulePath, importPath) => {
|
|
115
|
+
const normalizedImportPath = toPosixPath(importPath);
|
|
116
|
+
const importerDir = path.posix.dirname(toPosixPath(fromModulePath));
|
|
117
|
+
const isRelativeImport = normalizedImportPath.startsWith("./") || normalizedImportPath.startsWith("../");
|
|
118
|
+
const basePath = isRelativeImport
|
|
119
|
+
? path.posix.normalize(path.posix.join(importerDir, normalizedImportPath))
|
|
120
|
+
: path.posix.normalize(normalizedImportPath.replace(/^\/+/, ""));
|
|
121
|
+
const candidates = new Set();
|
|
122
|
+
const importExt = path.posix.extname(basePath);
|
|
123
|
+
// If import already has extension, use it directly
|
|
124
|
+
if (importExt) {
|
|
125
|
+
candidates.add(basePath);
|
|
126
|
+
return candidates;
|
|
127
|
+
}
|
|
128
|
+
// Try all supported extensions
|
|
129
|
+
for (const ext of SUPPORTED_EXTENSIONS) {
|
|
130
|
+
candidates.add(`${basePath}${ext}`);
|
|
131
|
+
candidates.add(path.posix.join(basePath, `index${ext}`));
|
|
132
|
+
}
|
|
133
|
+
return candidates;
|
|
134
|
+
};
|
|
135
|
+
// Match import edges to module nodes using deterministic path resolution
|
|
136
|
+
for (const imp of allImportEdges) {
|
|
137
|
+
const sourceModule = nodeMap.get(imp.fromModuleId);
|
|
138
|
+
if (sourceModule?.kind !== "module" || sourceModule.path == null) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const candidatePaths = buildImportCandidatePaths(sourceModule.path, imp.importPath);
|
|
142
|
+
let targetModule = [...nodeMap.values()].find(n => n.kind === "module" && n.path != null && candidatePaths.has(toPosixPath(n.path)));
|
|
143
|
+
// Fallback: try module lookup map
|
|
144
|
+
if (!targetModule) {
|
|
145
|
+
const normalizedImport = imp.importPath.replace(/^\.\//, "").replace(/\.js$/, "");
|
|
146
|
+
targetModule = moduleLookup.get(normalizedImport);
|
|
147
|
+
}
|
|
148
|
+
if (targetModule) {
|
|
149
|
+
edges.push({
|
|
150
|
+
from: imp.fromModuleId,
|
|
151
|
+
to: targetModule.id,
|
|
152
|
+
kind: "imports",
|
|
153
|
+
label: imp.importPath,
|
|
154
|
+
required: true,
|
|
155
|
+
confidence: 0.9
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const inh of allInheritEdges) {
|
|
160
|
+
// Get child node to extract its path
|
|
161
|
+
const childNode = nodeMap.get(inh.fromId);
|
|
162
|
+
if (!childNode || childNode.path == null) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// Try exact match in same file first
|
|
166
|
+
let parentNodeId = allSymbolIndex.get(`${childNode.path}::${inh.toName}`);
|
|
167
|
+
// Fallback: search across all files
|
|
168
|
+
if (!parentNodeId) {
|
|
169
|
+
parentNodeId = [...allSymbolIndex.entries()].find(([key]) => key.endsWith(`::${inh.toName}`))?.[1];
|
|
170
|
+
}
|
|
171
|
+
if (parentNodeId) {
|
|
172
|
+
edges.push({
|
|
173
|
+
from: inh.fromId,
|
|
174
|
+
to: parentNodeId,
|
|
175
|
+
kind: "inherits",
|
|
176
|
+
required: true,
|
|
177
|
+
confidence: 0.95
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const call of allCallEdges) {
|
|
182
|
+
const targetId = [...allSymbolIndex.entries()].find(([key]) => key.endsWith(`::${call.toName}`))?.[1];
|
|
183
|
+
if (targetId && targetId !== call.fromId) {
|
|
184
|
+
edges.push({
|
|
185
|
+
from: call.fromId,
|
|
186
|
+
to: targetId,
|
|
187
|
+
kind: "calls",
|
|
188
|
+
label: call.callText,
|
|
189
|
+
required: true,
|
|
190
|
+
confidence: 0.85
|
|
191
|
+
});
|
|
192
|
+
const caller = nodeMap.get(call.fromId);
|
|
193
|
+
const target = nodeMap.get(targetId);
|
|
194
|
+
if (caller && target) {
|
|
195
|
+
nodeMap.set(call.fromId, {
|
|
196
|
+
...caller,
|
|
197
|
+
contract: mergeContracts(caller.contract, {
|
|
198
|
+
...emptyContract(),
|
|
199
|
+
calls: [{ target: target.name, kind: "calls", description: call.callText }],
|
|
200
|
+
dependencies: [target.name]
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Post-process: link Go methods (extracted as standalone functions) to their struct types
|
|
207
|
+
for (const node of nodeMap.values()) {
|
|
208
|
+
if (node.kind === "function" && node.name.includes(".") && !node.ownerId) {
|
|
209
|
+
const [ownerName] = node.name.split(".");
|
|
210
|
+
const classNode = [...nodeMap.values()].find(c => c.kind === "class" && c.name === ownerName);
|
|
211
|
+
if (classNode) {
|
|
212
|
+
nodeMap.set(node.id, { ...node, ownerId: classNode.id });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Post-process: aggregate methods into their owner class's contract
|
|
217
|
+
for (const node of nodeMap.values()) {
|
|
218
|
+
if (node.kind !== "function" || !node.ownerId)
|
|
219
|
+
continue;
|
|
220
|
+
const ownerNode = nodeMap.get(node.ownerId);
|
|
221
|
+
if (!ownerNode || ownerNode.kind !== "class")
|
|
222
|
+
continue;
|
|
223
|
+
const methodSpec = {
|
|
224
|
+
name: node.name.split(".").pop() || node.name,
|
|
225
|
+
signature: node.signature || undefined,
|
|
226
|
+
summary: node.summary,
|
|
227
|
+
inputs: node.contract.inputs,
|
|
228
|
+
outputs: node.contract.outputs,
|
|
229
|
+
sideEffects: node.contract.sideEffects,
|
|
230
|
+
calls: node.contract.calls
|
|
231
|
+
};
|
|
232
|
+
nodeMap.set(ownerNode.id, {
|
|
233
|
+
...ownerNode,
|
|
234
|
+
contract: {
|
|
235
|
+
...ownerNode.contract,
|
|
236
|
+
methods: [...ownerNode.contract.methods, methodSpec]
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
nodes: [...nodeMap.values()],
|
|
242
|
+
edges: dedupeEdges(edges),
|
|
243
|
+
workflows: [],
|
|
244
|
+
warnings
|
|
245
|
+
};
|
|
246
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { analyzeRepo } from "./repo-multi.js";
|
|
5
|
+
import { resetLoader } from "./tree-sitter-loader.js";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const FIXTURES_DIR = path.resolve(__dirname, "../../test-fixtures");
|
|
8
|
+
describe("analyzeRepo", () => {
|
|
9
|
+
afterAll(() => {
|
|
10
|
+
resetLoader();
|
|
11
|
+
});
|
|
12
|
+
describe("Go", () => {
|
|
13
|
+
it("extracts functions, classes, and calls from Go repo", async () => {
|
|
14
|
+
const result = await analyzeRepo(path.join(FIXTURES_DIR, "sample-go"));
|
|
15
|
+
const nodeNames = result.nodes.map(n => n.name);
|
|
16
|
+
const nodeKinds = new Map(result.nodes.map(n => [n.id, n.kind]));
|
|
17
|
+
// Module node
|
|
18
|
+
expect(result.nodes.some(n => n.kind === "module" && n.name === "main.go")).toBe(true);
|
|
19
|
+
// Function nodes
|
|
20
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "ProcessData")).toBe(true);
|
|
21
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "formatString")).toBe(true);
|
|
22
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "main")).toBe(true);
|
|
23
|
+
// Class (struct) node
|
|
24
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "UserService")).toBe(true);
|
|
25
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "Database")).toBe(true);
|
|
26
|
+
// Methods
|
|
27
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.GetUser")).toBe(true);
|
|
28
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.SaveUser")).toBe(true);
|
|
29
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "Database.Connect")).toBe(true);
|
|
30
|
+
// All nodes have required fields
|
|
31
|
+
for (const node of result.nodes) {
|
|
32
|
+
expect(node.id).toBeDefined();
|
|
33
|
+
expect(node.kind).toBeDefined();
|
|
34
|
+
expect(node.name).toBeDefined();
|
|
35
|
+
expect(node.summary).toBeDefined();
|
|
36
|
+
expect(node.path).toBeDefined();
|
|
37
|
+
expect(node.sourceRefs).toBeDefined();
|
|
38
|
+
expect(node.sourceRefs.length).toBeGreaterThan(0);
|
|
39
|
+
expect(node.sourceRefs[0].kind).toBe("repo");
|
|
40
|
+
}
|
|
41
|
+
// Calls edges
|
|
42
|
+
const callEdges = result.edges.filter(e => e.kind === "calls");
|
|
43
|
+
expect(callEdges.length).toBeGreaterThan(0);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe("Python", () => {
|
|
47
|
+
it("extracts functions, classes, methods, and calls from Python repo", async () => {
|
|
48
|
+
const result = await analyzeRepo(path.join(FIXTURES_DIR, "sample-python"));
|
|
49
|
+
// Module node
|
|
50
|
+
expect(result.nodes.some(n => n.kind === "module" && n.name === "service.py")).toBe(true);
|
|
51
|
+
// Functions
|
|
52
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "load_config")).toBe(true);
|
|
53
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "process_data")).toBe(true);
|
|
54
|
+
// Classes
|
|
55
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "UserService")).toBe(true);
|
|
56
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "BaseService")).toBe(true);
|
|
57
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "TaskService")).toBe(true);
|
|
58
|
+
// Methods
|
|
59
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.get_user")).toBe(true);
|
|
60
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.save_user")).toBe(true);
|
|
61
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "TaskService.create_task")).toBe(true);
|
|
62
|
+
// Calls edges exist
|
|
63
|
+
const callEdges = result.edges.filter(e => e.kind === "calls");
|
|
64
|
+
expect(callEdges.length).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("C", () => {
|
|
68
|
+
it("extracts functions and calls from C repo", async () => {
|
|
69
|
+
const result = await analyzeRepo(path.join(FIXTURES_DIR, "sample-c"));
|
|
70
|
+
// Module node
|
|
71
|
+
expect(result.nodes.some(n => n.kind === "module" && n.name === "main.c")).toBe(true);
|
|
72
|
+
// Functions
|
|
73
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "process_data")).toBe(true);
|
|
74
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "calculate_sum")).toBe(true);
|
|
75
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "print_result")).toBe(true);
|
|
76
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "main")).toBe(true);
|
|
77
|
+
// Calls edges
|
|
78
|
+
const callEdges = result.edges.filter(e => e.kind === "calls");
|
|
79
|
+
expect(callEdges.length).toBeGreaterThan(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe("C++", () => {
|
|
83
|
+
it("extracts classes, inheritance, methods, and calls from C++ repo", async () => {
|
|
84
|
+
const result = await analyzeRepo(path.join(FIXTURES_DIR, "sample-cpp"));
|
|
85
|
+
// Module
|
|
86
|
+
expect(result.nodes.some(n => n.kind === "module" && n.name === "main.cpp")).toBe(true);
|
|
87
|
+
// Classes
|
|
88
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "BaseService")).toBe(true);
|
|
89
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "UserService")).toBe(true);
|
|
90
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "TaskService")).toBe(true);
|
|
91
|
+
// Function
|
|
92
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "process_data")).toBe(true);
|
|
93
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "main")).toBe(true);
|
|
94
|
+
// Methods
|
|
95
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.getUser")).toBe(true);
|
|
96
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.saveUser")).toBe(true);
|
|
97
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "TaskService.createTask")).toBe(true);
|
|
98
|
+
// Inheritance edges
|
|
99
|
+
const inheritEdges = result.edges.filter(e => e.kind === "inherits");
|
|
100
|
+
expect(inheritEdges.length).toBeGreaterThan(0);
|
|
101
|
+
// Calls edges
|
|
102
|
+
const callEdges = result.edges.filter(e => e.kind === "calls");
|
|
103
|
+
expect(callEdges.length).toBeGreaterThan(0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe("Rust", () => {
|
|
107
|
+
it("extracts structs, impl methods, and calls from Rust repo", async () => {
|
|
108
|
+
const result = await analyzeRepo(path.join(FIXTURES_DIR, "sample-rust"));
|
|
109
|
+
// Module
|
|
110
|
+
expect(result.nodes.some(n => n.kind === "module" && n.name === "main.rs")).toBe(true);
|
|
111
|
+
// Functions
|
|
112
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "process_data")).toBe(true);
|
|
113
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "calculate_total")).toBe(true);
|
|
114
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "main")).toBe(true);
|
|
115
|
+
// Structs/classes
|
|
116
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "UserService")).toBe(true);
|
|
117
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "TaskService")).toBe(true);
|
|
118
|
+
// Methods
|
|
119
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.get_user")).toBe(true);
|
|
120
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.save_user")).toBe(true);
|
|
121
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "TaskService.create_task")).toBe(true);
|
|
122
|
+
// Calls edges
|
|
123
|
+
const callEdges = result.edges.filter(e => e.kind === "calls");
|
|
124
|
+
expect(callEdges.length).toBeGreaterThan(0);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe("TypeScript", () => {
|
|
128
|
+
it("extracts classes, inheritance, methods, imports, and calls from TS repo", async () => {
|
|
129
|
+
const result = await analyzeRepo(path.join(FIXTURES_DIR, "sample-ts"));
|
|
130
|
+
// Module nodes
|
|
131
|
+
expect(result.nodes.some(n => n.kind === "module" && n.name === "service.ts")).toBe(true);
|
|
132
|
+
expect(result.nodes.some(n => n.kind === "module" && n.name === "base-service.ts")).toBe(true);
|
|
133
|
+
// Functions
|
|
134
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "processData")).toBe(true);
|
|
135
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "calculateSum")).toBe(true);
|
|
136
|
+
// Classes
|
|
137
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "UserService")).toBe(true);
|
|
138
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "BaseService")).toBe(true);
|
|
139
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "TaskService")).toBe(true);
|
|
140
|
+
// Methods
|
|
141
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.getUser")).toBe(true);
|
|
142
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.saveUser")).toBe(true);
|
|
143
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "TaskService.createTask")).toBe(true);
|
|
144
|
+
// Inheritance edges
|
|
145
|
+
const inheritEdges = result.edges.filter(e => e.kind === "inherits");
|
|
146
|
+
expect(inheritEdges.length).toBeGreaterThan(0);
|
|
147
|
+
// Calls edges
|
|
148
|
+
const callEdges = result.edges.filter(e => e.kind === "calls");
|
|
149
|
+
expect(callEdges.length).toBeGreaterThan(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe("JavaScript", () => {
|
|
153
|
+
it("extracts classes, inheritance, methods, and calls from JS repo", async () => {
|
|
154
|
+
const result = await analyzeRepo(path.join(FIXTURES_DIR, "sample-js"));
|
|
155
|
+
// Module nodes
|
|
156
|
+
expect(result.nodes.some(n => n.kind === "module" && n.name === "service.js")).toBe(true);
|
|
157
|
+
// Functions
|
|
158
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "processData")).toBe(true);
|
|
159
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "calculateSum")).toBe(true);
|
|
160
|
+
// Classes
|
|
161
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "UserService")).toBe(true);
|
|
162
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "BaseService")).toBe(true);
|
|
163
|
+
expect(result.nodes.some(n => n.kind === "class" && n.name === "TaskService")).toBe(true);
|
|
164
|
+
// Methods
|
|
165
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.getUser")).toBe(true);
|
|
166
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "UserService.saveUser")).toBe(true);
|
|
167
|
+
expect(result.nodes.some(n => n.kind === "function" && n.name === "TaskService.createTask")).toBe(true);
|
|
168
|
+
// Inheritance edges
|
|
169
|
+
const inheritEdges = result.edges.filter(e => e.kind === "inherits");
|
|
170
|
+
expect(inheritEdges.length).toBeGreaterThan(0);
|
|
171
|
+
// Calls edges
|
|
172
|
+
const callEdges = result.edges.filter(e => e.kind === "calls");
|
|
173
|
+
expect(callEdges.length).toBeGreaterThan(0);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe("edge cases", () => {
|
|
177
|
+
it("throws for invalid path", async () => {
|
|
178
|
+
await expect(analyzeRepo("/nonexistent/path")).rejects.toThrow();
|
|
179
|
+
});
|
|
180
|
+
it("returns warnings for empty directory", async () => {
|
|
181
|
+
const emptyDir = path.join(FIXTURES_DIR, "empty-test-dir");
|
|
182
|
+
const fs = await import("node:fs/promises");
|
|
183
|
+
await fs.mkdir(emptyDir, { recursive: true });
|
|
184
|
+
const result = await analyzeRepo(emptyDir);
|
|
185
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
186
|
+
expect(result.nodes.length).toBe(0);
|
|
187
|
+
await fs.rmdir(emptyDir);
|
|
188
|
+
});
|
|
189
|
+
it("generates summaries from comments", async () => {
|
|
190
|
+
const result = await analyzeRepo(path.join(FIXTURES_DIR, "sample-go"));
|
|
191
|
+
const processDataNode = result.nodes.find(n => n.name === "ProcessData");
|
|
192
|
+
expect(processDataNode).toBeDefined();
|
|
193
|
+
expect(processDataNode.summary.length).toBeGreaterThan(0);
|
|
194
|
+
// Should contain the comment text
|
|
195
|
+
expect(processDataNode.summary.toLowerCase()).toContain("process");
|
|
196
|
+
});
|
|
197
|
+
it("excludes node_modules and other excluded dirs", async () => {
|
|
198
|
+
const testDir = path.join(FIXTURES_DIR, "sample-ts");
|
|
199
|
+
const fs = await import("node:fs/promises");
|
|
200
|
+
const nmDir = path.join(testDir, "node_modules");
|
|
201
|
+
await fs.mkdir(nmDir, { recursive: true });
|
|
202
|
+
await fs.writeFile(path.join(nmDir, "bad.ts"), "function badFunc() {}");
|
|
203
|
+
const result = await analyzeRepo(testDir);
|
|
204
|
+
expect(result.nodes.some(n => n.name.includes("node_modules"))).toBe(false);
|
|
205
|
+
await fs.rm(nmDir, { recursive: true, force: true });
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
package/dist/analyzer/repo.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlueprintGraph } from "../schema/index";
|
|
1
|
+
import type { BlueprintGraph } from "../schema/index.js";
|
|
2
2
|
type RepoGraphPart = Omit<BlueprintGraph, "projectName" | "mode" | "generatedAt">;
|
|
3
3
|
export declare const analyzeTypeScriptRepo: (repoPath: string) => Promise<RepoGraphPart>;
|
|
4
4
|
export {};
|
package/dist/analyzer/repo.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
4
|
-
import { emptyContract } from "../schema/index";
|
|
5
|
-
import { createNode, createNodeId, dedupeEdges, mergeContracts, mergeDesignCalls, toPosixPath } from "../internal/utils";
|
|
4
|
+
import { emptyContract } from "../schema/index.js";
|
|
5
|
+
import { createNode, createNodeId, dedupeEdges, mergeContracts, mergeDesignCalls, toPosixPath } from "../internal/utils.js";
|
|
6
6
|
const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
7
7
|
const EXCLUDED_SEGMENTS = ["/node_modules/", "/.next/", "/dist/", "/artifacts/", "/coverage/"];
|
|
8
8
|
const hasJsDocSummary = (node) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BlueprintEdge, BlueprintNodeKind } from "../schema/index.js";
|
|
2
|
+
interface ExtractedNode {
|
|
3
|
+
nodeId: string;
|
|
4
|
+
kind: BlueprintNodeKind;
|
|
5
|
+
name: string;
|
|
6
|
+
summary: string;
|
|
7
|
+
path: string;
|
|
8
|
+
signature: string;
|
|
9
|
+
sourceRefs: Array<{
|
|
10
|
+
kind: "repo";
|
|
11
|
+
path: string;
|
|
12
|
+
symbol?: string;
|
|
13
|
+
}>;
|
|
14
|
+
ownerId?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const extractNodesFromFile: (filePath: string, relativePath: string) => Promise<{
|
|
17
|
+
nodes: ExtractedNode[];
|
|
18
|
+
edges: BlueprintEdge[];
|
|
19
|
+
symbolIndex: Map<string, string>;
|
|
20
|
+
callEdges: Array<{
|
|
21
|
+
fromId: string;
|
|
22
|
+
toName: string;
|
|
23
|
+
callText: string;
|
|
24
|
+
}>;
|
|
25
|
+
importEdges: Array<{
|
|
26
|
+
fromModuleId: string;
|
|
27
|
+
importPath: string;
|
|
28
|
+
}>;
|
|
29
|
+
inheritEdges: Array<{
|
|
30
|
+
fromId: string;
|
|
31
|
+
toName: string;
|
|
32
|
+
}>;
|
|
33
|
+
}>;
|
|
34
|
+
export {};
|