@abhinav2203/codeflow-core 0.1.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 ADDED
@@ -0,0 +1,31 @@
1
+ # @abhinav2203/codeflow-core
2
+
3
+ Framework-agnostic CodeFlow analysis core.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @abhinav2203/codeflow-core
9
+ ```
10
+
11
+ ## Exports
12
+
13
+ ```ts
14
+ import { buildBlueprintGraph, analyzeTypeScriptRepo } from "@abhinav2203/codeflow-core/analyzer";
15
+ import { exportBlueprintArtifacts } from "@abhinav2203/codeflow-core/export";
16
+ import { detectGraphConflicts } from "@abhinav2203/codeflow-core/conflicts";
17
+ import type { BlueprintGraph } from "@abhinav2203/codeflow-core/schema";
18
+ ```
19
+
20
+ ## Scope
21
+
22
+ This package contains the reusable, Node-oriented blueprint logic extracted from CodeFlow:
23
+
24
+ - repository analysis
25
+ - blueprint graph building
26
+ - schema types and validation
27
+ - artifact export helpers
28
+ - blueprint conflict detection
29
+ - store path helpers
30
+
31
+ It does not include Next.js routes, UI components, browser stores, or provider-specific integrations.
@@ -0,0 +1,2 @@
1
+ import type { BlueprintGraph, BuildBlueprintRequest } from "../schema/index";
2
+ export declare const buildBlueprintGraph: (request: BuildBlueprintRequest) => Promise<BlueprintGraph>;
@@ -0,0 +1,90 @@
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";
7
+ const mergeNodes = (nodes) => {
8
+ const map = new Map();
9
+ for (const node of nodes) {
10
+ const dedupeKey = `${node.kind}:${node.path ?? node.name}`;
11
+ const existing = map.get(dedupeKey);
12
+ if (!existing) {
13
+ map.set(dedupeKey, node);
14
+ continue;
15
+ }
16
+ map.set(dedupeKey, {
17
+ ...existing,
18
+ summary: existing.summary || node.summary,
19
+ path: existing.path ?? node.path,
20
+ signature: existing.signature ?? node.signature,
21
+ ownerId: existing.ownerId ?? node.ownerId,
22
+ contract: mergeContracts(existing.contract, node.contract),
23
+ sourceRefs: mergeSourceRefs(existing.sourceRefs, node.sourceRefs),
24
+ generatedRefs: [...new Set([...existing.generatedRefs, ...node.generatedRefs])],
25
+ traceRefs: [...new Set([...existing.traceRefs, ...node.traceRefs])]
26
+ });
27
+ }
28
+ return [...map.values()];
29
+ };
30
+ const createImplicitWorkflowEdges = (nodes, workflows) => {
31
+ const edges = workflows.flatMap((workflow) => workflow.steps.flatMap((step, index) => {
32
+ const current = nodes.find((node) => node.name === step);
33
+ const next = nodes.find((node) => node.name === workflow.steps[index + 1]);
34
+ if (!current || !next) {
35
+ return [];
36
+ }
37
+ return [
38
+ {
39
+ from: current.id,
40
+ to: next.id,
41
+ kind: "calls",
42
+ label: workflow.name,
43
+ required: true,
44
+ confidence: 0.6
45
+ }
46
+ ];
47
+ }));
48
+ return dedupeEdges(edges);
49
+ };
50
+ const emptyGraphPart = () => ({
51
+ nodes: [],
52
+ edges: [],
53
+ workflows: [],
54
+ warnings: []
55
+ });
56
+ export const buildBlueprintGraph = async (request) => {
57
+ const graphParts = [];
58
+ if (request.prdText?.trim()) {
59
+ graphParts.push(parsePrd(request.prdText));
60
+ }
61
+ if (request.repoPath?.trim()) {
62
+ graphParts.push(await analyzeTypeScriptRepo(path.resolve(request.repoPath)));
63
+ }
64
+ const combined = graphParts.reduce((accumulator, part) => ({
65
+ nodes: [...accumulator.nodes, ...part.nodes],
66
+ edges: [...accumulator.edges, ...part.edges],
67
+ workflows: [...accumulator.workflows, ...part.workflows],
68
+ warnings: [...accumulator.warnings, ...part.warnings]
69
+ }), emptyGraphPart());
70
+ const nodes = mergeNodes(combined.nodes.map((node) => createNode({
71
+ ...node,
72
+ contract: mergeContracts(emptyContract(), node.contract)
73
+ })));
74
+ const workflowEdges = createImplicitWorkflowEdges(nodes, combined.workflows);
75
+ const graph = {
76
+ projectName: request.projectName,
77
+ mode: request.mode,
78
+ phase: "spec",
79
+ generatedAt: new Date().toISOString(),
80
+ nodes,
81
+ edges: dedupeEdges([...combined.edges, ...workflowEdges]).filter((edge) => nodes.some((node) => node.id === edge.from) &&
82
+ nodes.some((node) => node.id === edge.to)),
83
+ workflows: combined.workflows,
84
+ warnings: combined.warnings
85
+ };
86
+ if (graph.nodes.length === 0) {
87
+ graph.warnings.push("No blueprint nodes were produced. Provide PRD content and/or a TypeScript repo.");
88
+ }
89
+ return withSpecDrafts(graph);
90
+ };
@@ -0,0 +1,2 @@
1
+ export { buildBlueprintGraph } from "./build";
2
+ export { analyzeTypeScriptRepo } from "./repo";
@@ -0,0 +1,2 @@
1
+ export { buildBlueprintGraph } from "./build";
2
+ export { analyzeTypeScriptRepo } from "./repo";
@@ -0,0 +1,4 @@
1
+ import type { BlueprintGraph } from "../schema/index";
2
+ type RepoGraphPart = Omit<BlueprintGraph, "projectName" | "mode" | "generatedAt">;
3
+ export declare const analyzeTypeScriptRepo: (repoPath: string) => Promise<RepoGraphPart>;
4
+ export {};
@@ -0,0 +1,451 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
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";
6
+ const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
7
+ const EXCLUDED_SEGMENTS = ["/node_modules/", "/.next/", "/dist/", "/artifacts/", "/coverage/"];
8
+ const hasJsDocSummary = (node) => {
9
+ const maybeJsDocNode = node;
10
+ return maybeJsDocNode.getJsDocs
11
+ ? maybeJsDocNode
12
+ .getJsDocs()
13
+ .map((doc) => doc.getDescription().trim())
14
+ .filter(Boolean)
15
+ .join(" ")
16
+ : "";
17
+ };
18
+ const buildSummary = (name, fallback) => fallback || `Blueprint node for ${name}.`;
19
+ const getCallableSignatureNode = (declaration) => {
20
+ if (Node.isVariableDeclaration(declaration)) {
21
+ return (declaration.getInitializerIfKind(SyntaxKind.ArrowFunction) ??
22
+ declaration.getInitializerIfKind(SyntaxKind.FunctionExpression));
23
+ }
24
+ return declaration;
25
+ };
26
+ const createContractFromCallable = (summary, declaration) => {
27
+ const signatureNode = getCallableSignatureNode(declaration);
28
+ if (!signatureNode || !("getParameters" in signatureNode) || !("getReturnType" in signatureNode)) {
29
+ return mergeContracts(emptyContract(), {
30
+ ...emptyContract(),
31
+ summary,
32
+ responsibilities: [summary]
33
+ });
34
+ }
35
+ const callableNode = signatureNode;
36
+ const parameters = callableNode.getParameters().map((parameter) => ({
37
+ name: parameter.getName(),
38
+ type: parameter.getType().getText()
39
+ }));
40
+ const returnType = callableNode.getReturnType().getText(signatureNode);
41
+ return mergeContracts(emptyContract(), {
42
+ ...emptyContract(),
43
+ summary,
44
+ responsibilities: [summary],
45
+ inputs: parameters,
46
+ outputs: [{ name: "result", type: returnType }]
47
+ });
48
+ };
49
+ const createMethodSpecFromDeclaration = (summary, declaration, name, signature) => {
50
+ const callableContract = createContractFromCallable(summary, declaration);
51
+ return {
52
+ name,
53
+ signature,
54
+ summary,
55
+ inputs: callableContract.inputs,
56
+ outputs: callableContract.outputs,
57
+ sideEffects: callableContract.sideEffects,
58
+ calls: callableContract.calls
59
+ };
60
+ };
61
+ const createClassContract = (summary, classDeclaration) => mergeContracts(emptyContract(), {
62
+ ...emptyContract(),
63
+ summary,
64
+ responsibilities: [summary],
65
+ attributes: classDeclaration.getProperties().map((property) => ({
66
+ name: property.getName(),
67
+ type: property.getType().getText(property),
68
+ description: hasJsDocSummary(property) || undefined
69
+ })),
70
+ methods: classDeclaration.getMethods().map((method) => createMethodSpecFromDeclaration(buildSummary(`${classDeclaration.getName()}.${method.getName()}`, hasJsDocSummary(method)), method, method.getName(), `${method.getName()}(${method
71
+ .getParameters()
72
+ .map((parameter) => `${parameter.getName()}: ${parameter.getType().getText(parameter)}`)
73
+ .join(", ")}): ${method.getReturnType().getText(method)}`))
74
+ });
75
+ const toRelativePath = (repoPath, filePath) => toPosixPath(path.relative(repoPath, filePath));
76
+ const isIncludedFile = (repoPath, filePath) => {
77
+ const normalized = toPosixPath(filePath);
78
+ return normalized.startsWith(toPosixPath(repoPath)) && !EXCLUDED_SEGMENTS.some((segment) => normalized.includes(segment));
79
+ };
80
+ const buildRoutePath = (relativePath) => {
81
+ const normalized = relativePath.replace(/^src\//, "");
82
+ const match = normalized.match(/app\/api\/(.+)\/route\.(ts|tsx|js|jsx)$/);
83
+ if (!match) {
84
+ return normalized;
85
+ }
86
+ return `/api/${match[1].replace(/\/index$/, "")}`;
87
+ };
88
+ const buildScreenName = (relativePath) => {
89
+ const normalized = relativePath.replace(/^src\//, "");
90
+ const match = normalized.match(/app\/(.+)\/page\.(ts|tsx|js|jsx)$/);
91
+ if (!match) {
92
+ return "Home Screen";
93
+ }
94
+ const routePath = `/${match[1].replace(/\/index$/, "")}`;
95
+ return routePath === "/" ? "Home Screen" : `${routePath} Screen`;
96
+ };
97
+ const createSymbolKey = (relativePath, symbolName, ownerName) => `${relativePath}::${ownerName ? `${ownerName}.` : ""}${symbolName}`;
98
+ const getAliasedSymbol = (node) => {
99
+ const symbol = node.getSymbol();
100
+ return symbol?.getAliasedSymbol() ?? symbol;
101
+ };
102
+ const getDeclarationKey = (repoPath, declaration) => {
103
+ const sourceFile = declaration.getSourceFile();
104
+ const relativePath = toRelativePath(repoPath, sourceFile.getFilePath());
105
+ if (Node.isMethodDeclaration(declaration)) {
106
+ const classDeclaration = declaration.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
107
+ const className = classDeclaration?.getName();
108
+ if (!className) {
109
+ return null;
110
+ }
111
+ return createSymbolKey(relativePath, declaration.getName(), className);
112
+ }
113
+ if (Node.isFunctionDeclaration(declaration) ||
114
+ Node.isVariableDeclaration(declaration) ||
115
+ Node.isClassDeclaration(declaration) ||
116
+ Node.isFunctionExpression(declaration) ||
117
+ Node.isArrowFunction(declaration)) {
118
+ const name = "getName" in declaration && declaration.getName
119
+ ? declaration.getName()
120
+ : declaration.getFirstAncestorByKind(SyntaxKind.VariableDeclaration)?.getName();
121
+ if (!name) {
122
+ return null;
123
+ }
124
+ return createSymbolKey(relativePath, name);
125
+ }
126
+ return null;
127
+ };
128
+ const addModuleNode = (nodes, relativePath) => {
129
+ const id = createNodeId("module", relativePath, relativePath);
130
+ if (nodes.has(id)) {
131
+ return id;
132
+ }
133
+ nodes.set(id, createNode({
134
+ id,
135
+ kind: "module",
136
+ name: relativePath,
137
+ path: relativePath,
138
+ summary: `Source module ${relativePath}.`,
139
+ contract: mergeContracts(emptyContract(), {
140
+ ...emptyContract(),
141
+ summary: `Source module ${relativePath}.`,
142
+ responsibilities: [`Owns the source file ${relativePath}.`]
143
+ }),
144
+ sourceRefs: [
145
+ {
146
+ kind: "repo",
147
+ path: relativePath
148
+ }
149
+ ]
150
+ }));
151
+ return id;
152
+ };
153
+ const collectVariableFunctions = (sourceFile) => sourceFile.getVariableDeclarations().filter((declaration) => {
154
+ return Boolean(getCallableSignatureNode(declaration));
155
+ });
156
+ export const analyzeTypeScriptRepo = async (repoPath) => {
157
+ const repoStats = await fs.stat(repoPath).catch(() => null);
158
+ if (!repoStats?.isDirectory()) {
159
+ throw new Error(`Repo path does not exist or is not a directory: ${repoPath}`);
160
+ }
161
+ const tsconfigPath = path.join(repoPath, "tsconfig.json");
162
+ const hasTsconfig = await fs
163
+ .stat(tsconfigPath)
164
+ .then((stats) => stats.isFile())
165
+ .catch(() => false);
166
+ const project = hasTsconfig
167
+ ? new Project({
168
+ tsConfigFilePath: tsconfigPath,
169
+ skipAddingFilesFromTsConfig: false
170
+ })
171
+ : new Project({
172
+ compilerOptions: {
173
+ allowJs: true,
174
+ jsx: 4
175
+ }
176
+ });
177
+ if (!hasTsconfig) {
178
+ project.addSourceFilesAtPaths([
179
+ path.join(repoPath, "**/*.ts"),
180
+ path.join(repoPath, "**/*.tsx"),
181
+ path.join(repoPath, "**/*.js"),
182
+ path.join(repoPath, "**/*.jsx")
183
+ ]);
184
+ }
185
+ const sourceFiles = project
186
+ .getSourceFiles()
187
+ .filter((sourceFile) => isIncludedFile(repoPath, sourceFile.getFilePath()));
188
+ const warnings = [];
189
+ if (sourceFiles.length === 0) {
190
+ warnings.push(`No TypeScript or JavaScript source files found under ${repoPath}.`);
191
+ }
192
+ const nodes = new Map();
193
+ const edges = [];
194
+ const symbolToNodeId = new Map();
195
+ const callableEntries = [];
196
+ for (const sourceFile of sourceFiles) {
197
+ const relativePath = toRelativePath(repoPath, sourceFile.getFilePath());
198
+ const moduleId = addModuleNode(nodes, relativePath);
199
+ for (const importDeclaration of sourceFile.getImportDeclarations()) {
200
+ const target = importDeclaration.getModuleSpecifierSourceFile();
201
+ if (!target || !isIncludedFile(repoPath, target.getFilePath())) {
202
+ continue;
203
+ }
204
+ edges.push({
205
+ from: moduleId,
206
+ to: addModuleNode(nodes, toRelativePath(repoPath, target.getFilePath())),
207
+ kind: "imports",
208
+ label: importDeclaration.getModuleSpecifierValue(),
209
+ required: true,
210
+ confidence: 1
211
+ });
212
+ }
213
+ const classes = sourceFile.getClasses().filter((declaration) => declaration.getName());
214
+ for (const classDeclaration of classes) {
215
+ const className = classDeclaration.getNameOrThrow();
216
+ const classId = createNodeId("class", `${relativePath}:${className}`, `${relativePath}:${className}`);
217
+ const summary = buildSummary(className, hasJsDocSummary(classDeclaration));
218
+ nodes.set(classId, createNode({
219
+ id: classId,
220
+ kind: "class",
221
+ name: className,
222
+ path: relativePath,
223
+ ownerId: moduleId,
224
+ summary,
225
+ signature: `class ${className}`,
226
+ contract: createClassContract(summary, classDeclaration),
227
+ sourceRefs: [
228
+ {
229
+ kind: "repo",
230
+ path: relativePath,
231
+ symbol: className
232
+ }
233
+ ]
234
+ }));
235
+ symbolToNodeId.set(createSymbolKey(relativePath, className), classId);
236
+ const heritage = classDeclaration.getExtends();
237
+ if (heritage) {
238
+ const parentSymbol = getAliasedSymbol(heritage.getExpression());
239
+ const parentDeclaration = parentSymbol?.getDeclarations()[0];
240
+ const declarationKey = parentDeclaration ? getDeclarationKey(repoPath, parentDeclaration) : null;
241
+ const parentId = declarationKey ? symbolToNodeId.get(declarationKey) : undefined;
242
+ if (parentId) {
243
+ edges.push({
244
+ from: classId,
245
+ to: parentId,
246
+ kind: "inherits",
247
+ required: true,
248
+ confidence: 0.95
249
+ });
250
+ }
251
+ }
252
+ for (const method of classDeclaration.getMethods()) {
253
+ const summary = buildSummary(`${className}.${method.getName()}`, hasJsDocSummary(method));
254
+ const nodeId = createNodeId("function", `${relativePath}:${className}.${method.getName()}`, `${relativePath}:${className}.${method.getName()}`);
255
+ const methodSignature = `${method.getName()}(${method
256
+ .getParameters()
257
+ .map((parameter) => `${parameter.getName()}: ${parameter.getType().getText(parameter)}`)
258
+ .join(", ")}): ${method.getReturnType().getText(method)}`;
259
+ nodes.set(nodeId, createNode({
260
+ id: nodeId,
261
+ kind: "function",
262
+ name: `${className}.${method.getName()}`,
263
+ path: relativePath,
264
+ ownerId: classId,
265
+ summary,
266
+ signature: methodSignature,
267
+ contract: createContractFromCallable(summary, method),
268
+ sourceRefs: [
269
+ {
270
+ kind: "repo",
271
+ path: relativePath,
272
+ symbol: `${className}.${method.getName()}`
273
+ }
274
+ ]
275
+ }));
276
+ symbolToNodeId.set(createSymbolKey(relativePath, method.getName(), className), nodeId);
277
+ callableEntries.push({
278
+ nodeId,
279
+ declaration: method,
280
+ relativePath,
281
+ symbolName: method.getName(),
282
+ ownerName: className
283
+ });
284
+ }
285
+ }
286
+ for (const functionDeclaration of sourceFile.getFunctions().filter((declaration) => declaration.getName())) {
287
+ const functionName = functionDeclaration.getNameOrThrow();
288
+ const summary = buildSummary(functionName, hasJsDocSummary(functionDeclaration));
289
+ const isApiRoute = sourceFile.getBaseNameWithoutExtension() === "route" && HTTP_METHODS.has(functionName);
290
+ const isPageFile = sourceFile.getBaseNameWithoutExtension() === "page" && functionName.toLowerCase().includes("page");
291
+ const kind = isApiRoute ? "api" : isPageFile ? "ui-screen" : "function";
292
+ const nodeName = isApiRoute
293
+ ? `${functionName} ${buildRoutePath(relativePath)}`
294
+ : isPageFile
295
+ ? buildScreenName(relativePath)
296
+ : functionName;
297
+ const nodeId = createNodeId(kind, `${relativePath}:${nodeName}`, `${relativePath}:${nodeName}`);
298
+ const signature = `${functionName}(${functionDeclaration
299
+ .getParameters()
300
+ .map((parameter) => `${parameter.getName()}: ${parameter.getType().getText(parameter)}`)
301
+ .join(", ")}): ${functionDeclaration.getReturnType().getText(functionDeclaration)}`;
302
+ nodes.set(nodeId, createNode({
303
+ id: nodeId,
304
+ kind,
305
+ name: nodeName,
306
+ path: relativePath,
307
+ ownerId: kind === "function" ? moduleId : undefined,
308
+ summary,
309
+ signature,
310
+ contract: createContractFromCallable(summary, functionDeclaration),
311
+ sourceRefs: [
312
+ {
313
+ kind: "repo",
314
+ path: relativePath,
315
+ symbol: functionName
316
+ }
317
+ ]
318
+ }));
319
+ symbolToNodeId.set(createSymbolKey(relativePath, functionName), nodeId);
320
+ callableEntries.push({
321
+ nodeId,
322
+ declaration: functionDeclaration,
323
+ relativePath,
324
+ symbolName: functionName
325
+ });
326
+ }
327
+ for (const variableDeclaration of collectVariableFunctions(sourceFile)) {
328
+ const symbolName = variableDeclaration.getName();
329
+ const summary = buildSummary(symbolName, hasJsDocSummary(variableDeclaration.getVariableStatement() ?? variableDeclaration));
330
+ const nodeId = createNodeId("function", `${relativePath}:${symbolName}`, `${relativePath}:${symbolName}`);
331
+ const initializer = variableDeclaration.getInitializer();
332
+ if (!initializer || (!Node.isArrowFunction(initializer) && !Node.isFunctionExpression(initializer))) {
333
+ continue;
334
+ }
335
+ const signature = `${symbolName}(${initializer
336
+ .getParameters()
337
+ .map((parameter) => `${parameter.getName()}: ${parameter.getType().getText(parameter)}`)
338
+ .join(", ")}): ${initializer.getReturnType().getText(initializer)}`;
339
+ nodes.set(nodeId, createNode({
340
+ id: nodeId,
341
+ kind: "function",
342
+ name: symbolName,
343
+ path: relativePath,
344
+ ownerId: moduleId,
345
+ summary,
346
+ signature,
347
+ contract: createContractFromCallable(summary, variableDeclaration),
348
+ sourceRefs: [
349
+ {
350
+ kind: "repo",
351
+ path: relativePath,
352
+ symbol: symbolName
353
+ }
354
+ ]
355
+ }));
356
+ symbolToNodeId.set(createSymbolKey(relativePath, symbolName), nodeId);
357
+ callableEntries.push({
358
+ nodeId,
359
+ declaration: variableDeclaration,
360
+ relativePath,
361
+ symbolName
362
+ });
363
+ }
364
+ }
365
+ const callableNodeIdsByClassId = new Map();
366
+ for (const node of nodes.values()) {
367
+ if (node.kind === "function" && node.ownerId) {
368
+ const owned = callableNodeIdsByClassId.get(node.ownerId) ?? [];
369
+ owned.push(node.id);
370
+ callableNodeIdsByClassId.set(node.ownerId, owned);
371
+ }
372
+ }
373
+ for (const entry of callableEntries) {
374
+ const scope = getCallableSignatureNode(entry.declaration);
375
+ if (!scope) {
376
+ continue;
377
+ }
378
+ for (const callExpression of scope.getDescendantsOfKind(SyntaxKind.CallExpression)) {
379
+ const expression = callExpression.getExpression();
380
+ const targetSymbol = getAliasedSymbol(expression);
381
+ const targetDeclaration = targetSymbol?.getDeclarations()[0];
382
+ const targetKey = targetDeclaration ? getDeclarationKey(repoPath, targetDeclaration) : null;
383
+ const targetNodeId = targetKey ? symbolToNodeId.get(targetKey) : undefined;
384
+ if (!targetNodeId || targetNodeId === entry.nodeId) {
385
+ continue;
386
+ }
387
+ const targetNode = nodes.get(targetNodeId);
388
+ edges.push({
389
+ from: entry.nodeId,
390
+ to: targetNodeId,
391
+ kind: "calls",
392
+ label: expression.getText(),
393
+ required: true,
394
+ confidence: 0.9
395
+ });
396
+ const caller = nodes.get(entry.nodeId);
397
+ if (caller && targetNode) {
398
+ nodes.set(entry.nodeId, {
399
+ ...caller,
400
+ contract: mergeContracts(caller.contract, {
401
+ ...emptyContract(),
402
+ calls: [
403
+ {
404
+ target: targetNode.name,
405
+ kind: "calls",
406
+ description: expression.getText()
407
+ }
408
+ ],
409
+ dependencies: [targetNode.name]
410
+ })
411
+ });
412
+ }
413
+ }
414
+ }
415
+ for (const node of nodes.values()) {
416
+ if (node.kind !== "class") {
417
+ continue;
418
+ }
419
+ const ownedMethodIds = callableNodeIdsByClassId.get(node.id) ?? [];
420
+ if (!ownedMethodIds.length || !node.contract.methods.length) {
421
+ continue;
422
+ }
423
+ const methods = node.contract.methods.map((methodSpec) => {
424
+ const ownedMethodNode = ownedMethodIds
425
+ .map((methodId) => nodes.get(methodId))
426
+ .find((methodNode) => methodNode?.name.split(".").pop() === methodSpec.name);
427
+ if (!ownedMethodNode) {
428
+ return methodSpec;
429
+ }
430
+ return {
431
+ ...methodSpec,
432
+ sideEffects: [...new Set([...methodSpec.sideEffects, ...ownedMethodNode.contract.sideEffects])],
433
+ calls: mergeDesignCalls(methodSpec.calls, ownedMethodNode.contract.calls)
434
+ };
435
+ });
436
+ nodes.set(node.id, {
437
+ ...node,
438
+ contract: {
439
+ ...node.contract,
440
+ methods,
441
+ dependencies: [...new Set([...node.contract.dependencies, ...methods.flatMap((method) => method.calls.map((call) => call.target))])]
442
+ }
443
+ });
444
+ }
445
+ return {
446
+ nodes: [...nodes.values()],
447
+ edges: dedupeEdges(edges),
448
+ workflows: [],
449
+ warnings
450
+ };
451
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlueprintGraph, ConflictReport } from "../schema/index";
2
+ export declare const detectGraphConflicts: (graph: BlueprintGraph, repoPath: string) => Promise<ConflictReport>;
@@ -0,0 +1,63 @@
1
+ import path from "node:path";
2
+ import { analyzeTypeScriptRepo } from "../analyzer/repo";
3
+ const repoKeyForNode = (node) => `${node.kind}:${node.path ?? ""}:${node.name}`;
4
+ export const detectGraphConflicts = async (graph, repoPath) => {
5
+ const repoGraph = await analyzeTypeScriptRepo(path.resolve(repoPath));
6
+ const conflicts = [];
7
+ const repoNodes = repoGraph.nodes.filter((node) => node.kind !== "module");
8
+ const blueprintRepoNodes = graph.nodes.filter((node) => node.sourceRefs.some((ref) => ref.kind === "repo"));
9
+ const repoMap = new Map(repoNodes.map((node) => [repoKeyForNode(node), node]));
10
+ const blueprintMap = new Map(blueprintRepoNodes.map((node) => [repoKeyForNode(node), node]));
11
+ for (const blueprintNode of blueprintRepoNodes) {
12
+ const repoNode = repoMap.get(repoKeyForNode(blueprintNode));
13
+ if (!repoNode) {
14
+ conflicts.push({
15
+ kind: "missing-in-repo",
16
+ nodeId: blueprintNode.id,
17
+ path: blueprintNode.path,
18
+ blueprintValue: blueprintNode.name,
19
+ message: `${blueprintNode.name} is in the blueprint but not in the repo snapshot.`,
20
+ suggestedAction: "Remove the node from the blueprint or recreate it in the repo."
21
+ });
22
+ continue;
23
+ }
24
+ if ((blueprintNode.signature ?? "") !== (repoNode.signature ?? "")) {
25
+ conflicts.push({
26
+ kind: "signature-mismatch",
27
+ nodeId: blueprintNode.id,
28
+ path: blueprintNode.path,
29
+ blueprintValue: blueprintNode.signature,
30
+ repoValue: repoNode.signature,
31
+ message: `${blueprintNode.name} has a different signature in the repo.`,
32
+ suggestedAction: "Refresh the blueprint contract from the repo or update the implementation."
33
+ });
34
+ }
35
+ if (blueprintNode.summary && repoNode.summary && blueprintNode.summary !== repoNode.summary) {
36
+ conflicts.push({
37
+ kind: "summary-mismatch",
38
+ nodeId: blueprintNode.id,
39
+ path: blueprintNode.path,
40
+ blueprintValue: blueprintNode.summary,
41
+ repoValue: repoNode.summary,
42
+ message: `${blueprintNode.name} summary diverges from the repo-derived description.`,
43
+ suggestedAction: "Review the contract summary and align it with current behavior."
44
+ });
45
+ }
46
+ }
47
+ for (const repoNode of repoNodes) {
48
+ if (!blueprintMap.has(repoKeyForNode(repoNode))) {
49
+ conflicts.push({
50
+ kind: "missing-in-blueprint",
51
+ path: repoNode.path,
52
+ repoValue: repoNode.name,
53
+ message: `${repoNode.name} exists in the repo but is not represented in the blueprint.`,
54
+ suggestedAction: "Add the node to the blueprint or mark it intentionally out of scope."
55
+ });
56
+ }
57
+ }
58
+ return {
59
+ checkedAt: new Date().toISOString(),
60
+ repoPath: path.resolve(repoPath),
61
+ conflicts
62
+ };
63
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlueprintGraph, ExecutionReport, ExportResult } from "../schema/index";
2
+ export declare const exportBlueprintArtifacts: (graph: BlueprintGraph, outputDir?: string, executionReport?: ExecutionReport, codeDrafts?: Record<string, string>) => Promise<ExportResult>;