@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.
Files changed (38) hide show
  1. package/dist/analyzer/build.d.ts +1 -1
  2. package/dist/analyzer/build.js +6 -6
  3. package/dist/analyzer/index.d.ts +4 -2
  4. package/dist/analyzer/index.js +3 -2
  5. package/dist/analyzer/repo-multi.d.ts +6 -0
  6. package/dist/analyzer/repo-multi.js +246 -0
  7. package/dist/analyzer/repo-multi.test.d.ts +1 -0
  8. package/dist/analyzer/repo-multi.test.js +208 -0
  9. package/dist/analyzer/repo.d.ts +1 -1
  10. package/dist/analyzer/repo.js +2 -2
  11. package/dist/analyzer/test-hook-check.d.ts +1 -0
  12. package/dist/analyzer/test-hook-check.js +2 -0
  13. package/dist/analyzer/tree-sitter-analyzer.d.ts +34 -0
  14. package/dist/analyzer/tree-sitter-analyzer.js +617 -0
  15. package/dist/analyzer/tree-sitter-loader.d.ts +8 -0
  16. package/dist/analyzer/tree-sitter-loader.js +97 -0
  17. package/dist/analyzer/tree-sitter-queries.d.ts +10 -0
  18. package/dist/analyzer/tree-sitter-queries.js +285 -0
  19. package/dist/conflicts/index.d.ts +1 -1
  20. package/dist/conflicts/index.js +1 -1
  21. package/dist/export/index.d.ts +1 -1
  22. package/dist/export/index.js +4 -4
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.js +5 -5
  25. package/dist/internal/codegen.d.ts +1 -1
  26. package/dist/internal/codegen.js +1 -1
  27. package/dist/internal/phases.d.ts +1 -1
  28. package/dist/internal/phases.js +1 -1
  29. package/dist/internal/plan.d.ts +1 -1
  30. package/dist/internal/plan.js +1 -1
  31. package/dist/internal/prd.d.ts +1 -1
  32. package/dist/internal/prd.js +2 -2
  33. package/dist/internal/utils.d.ts +1 -1
  34. package/dist/internal/utils.js +1 -1
  35. package/dist/storage/store-paths.js +1 -1
  36. package/dist/store-paths.d.ts +1 -1
  37. package/dist/store-paths.js +1 -1
  38. package/package.json +13 -2
@@ -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>;
@@ -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);
@@ -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";
@@ -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
+ });
@@ -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 {};
@@ -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,2 @@
1
+ export {};
2
+ // test
@@ -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 {};