@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 +31 -0
- package/dist/analyzer/build.d.ts +2 -0
- package/dist/analyzer/build.js +90 -0
- package/dist/analyzer/index.d.ts +2 -0
- package/dist/analyzer/index.js +2 -0
- package/dist/analyzer/repo.d.ts +4 -0
- package/dist/analyzer/repo.js +451 -0
- package/dist/conflicts/index.d.ts +2 -0
- package/dist/conflicts/index.js +63 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.js +398 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/internal/codegen.d.ts +5 -0
- package/dist/internal/codegen.js +224 -0
- package/dist/internal/phases.d.ts +17 -0
- package/dist/internal/phases.js +124 -0
- package/dist/internal/plan.d.ts +2 -0
- package/dist/internal/plan.js +67 -0
- package/dist/internal/prd.d.ts +9 -0
- package/dist/internal/prd.js +220 -0
- package/dist/internal/utils.d.ts +13 -0
- package/dist/internal/utils.js +103 -0
- package/dist/schema/index.d.ts +4448 -0
- package/dist/schema/index.js +720 -0
- package/dist/storage/store-paths.d.ts +10 -0
- package/dist/storage/store-paths.js +27 -0
- package/dist/store-paths.d.ts +1 -0
- package/dist/store-paths.js +1 -0
- package/package.json +52 -0
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,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,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,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
|
+
};
|