@abhinav2203/codeflow-analysis 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/dist/bin/cli.js +43 -0
- package/package.json +33 -0
- package/src/app/api/analysis/cycles/route.test.ts +124 -0
- package/src/app/api/analysis/metrics/route.test.ts +122 -0
- package/src/app/api/analysis/smells/route.test.ts +134 -0
- package/src/app/api/conflicts/route.test.ts +72 -0
- package/src/app/api/refactor/detect/route.test.ts +74 -0
- package/src/app/api/refactor/heal/route.test.ts +85 -0
- package/src/conflicts.test.ts +134 -0
- package/src/conflicts.ts +106 -0
- package/src/cycles.test.ts +145 -0
- package/src/cycles.ts +161 -0
- package/src/handlers/conflicts.ts +29 -0
- package/src/handlers/cycles.ts +29 -0
- package/src/handlers/metrics.ts +28 -0
- package/src/handlers/refactor-detect.ts +28 -0
- package/src/handlers/refactor-heal.ts +32 -0
- package/src/handlers/smells.ts +29 -0
- package/src/index.ts +18 -0
- package/src/invoke.ts +176 -0
- package/src/metrics.test.ts +169 -0
- package/src/metrics.ts +184 -0
- package/src/refactor.test.ts +362 -0
- package/src/refactor.ts +290 -0
- package/src/smells.test.ts +196 -0
- package/src/smells.ts +221 -0
- package/test-fixtures/sample-repo/src/app/api/tasks/route.ts +9 -0
- package/test-fixtures/sample-repo/src/app/page.tsx +7 -0
- package/test-fixtures/sample-repo/src/lib/auth.ts +13 -0
- package/test-fixtures/sample-repo/src/services/base-service.ts +5 -0
- package/test-fixtures/sample-repo/src/services/task-service.ts +22 -0
- package/test-fixtures/sample-repo/tsconfig.json +10 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +22 -0
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry-point shim for @abhinav2203/codeflow-analysis.
|
|
4
|
+
*
|
|
5
|
+
* This file is the executable referenced in the `bin` field of package.json.
|
|
6
|
+
* It delegates to the TypeScript source via tsx so no build step is required
|
|
7
|
+
* during development. After running `npm run build`, you can replace the
|
|
8
|
+
* tsx invocation below with a direct `node` call to dist/invoke.js.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
12
|
+
import { accessSync } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const packageDir = path.resolve(__dirname, "..");
|
|
18
|
+
|
|
19
|
+
// Determine the entrypoint: compiled JS if built, otherwise TypeScript source.
|
|
20
|
+
let entrypoint;
|
|
21
|
+
try {
|
|
22
|
+
entrypoint = path.resolve(packageDir, "dist/invoke.js");
|
|
23
|
+
accessSync(entrypoint);
|
|
24
|
+
} catch {
|
|
25
|
+
entrypoint = path.resolve(packageDir, "src/invoke.ts");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Prefer locally installed tsx in the monorepo.
|
|
29
|
+
const tsxBin = path.resolve(packageDir, "node_modules/.bin/tsx");
|
|
30
|
+
|
|
31
|
+
const useNode = process.env.CLI_FORCE_NODE === "true";
|
|
32
|
+
const execPath = useNode ? process.execPath : tsxBin;
|
|
33
|
+
const execArgs = useNode ? [entrypoint, ...process.argv.slice(2)] : [entrypoint, ...process.argv.slice(2)];
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
execFileSync(execPath, execArgs, { stdio: "inherit" });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.status != null) {
|
|
39
|
+
process.exit(error.status);
|
|
40
|
+
}
|
|
41
|
+
console.error(error);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@abhinav2203/codeflow-analysis",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
9
|
+
"./cycles": { "types": "./dist/cycles.d.ts", "default": "./dist/cycles.js" },
|
|
10
|
+
"./smells": { "types": "./dist/smells.d.ts", "default": "./dist/smells.js" },
|
|
11
|
+
"./metrics": { "types": "./dist/metrics.d.ts", "default": "./dist/metrics.js" },
|
|
12
|
+
"./refactor": { "types": "./dist/refactor.d.ts", "default": "./dist/refactor.js" },
|
|
13
|
+
"./conflicts": { "types": "./dist/conflicts.d.ts", "default": "./dist/conflicts.js" }
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"codeflow-analysis": "./dist/bin/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"check": "tsc --noEmit",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"build": "tsc --outDir dist --declaration --declarationMap --noEmit false"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@abhinav2203/codeflow-core": "workspace:*",
|
|
25
|
+
"@abhinav2203/codeflow-store": "workspace:*",
|
|
26
|
+
"zod": "^3.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"typescript": "^5.7.0",
|
|
31
|
+
"vitest": "^3.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { POST } from "../../../../handlers/cycles";
|
|
4
|
+
import type { BlueprintGraph } from "@/lib/blueprint/schema";
|
|
5
|
+
import { emptyContract } from "@/lib/blueprint/schema";
|
|
6
|
+
|
|
7
|
+
const minimalNode = (id: string): BlueprintGraph["nodes"][number] => ({
|
|
8
|
+
id,
|
|
9
|
+
kind: "function",
|
|
10
|
+
name: id,
|
|
11
|
+
summary: "A node.",
|
|
12
|
+
contract: emptyContract(),
|
|
13
|
+
sourceRefs: [],
|
|
14
|
+
generatedRefs: [],
|
|
15
|
+
traceRefs: [],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const minimalEdge = (from: string, to: string): BlueprintGraph["edges"][number] => ({
|
|
19
|
+
from,
|
|
20
|
+
to,
|
|
21
|
+
kind: "calls",
|
|
22
|
+
required: true,
|
|
23
|
+
confidence: 1,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const baseGraph: BlueprintGraph = {
|
|
27
|
+
projectName: "Cycles Route Test",
|
|
28
|
+
mode: "essential",
|
|
29
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
30
|
+
warnings: [],
|
|
31
|
+
workflows: [],
|
|
32
|
+
nodes: [],
|
|
33
|
+
edges: [],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe("POST /api/analysis/cycles", () => {
|
|
37
|
+
it("returns a cycle report with no cycles for a clean DAG", async () => {
|
|
38
|
+
const graph: BlueprintGraph = {
|
|
39
|
+
...baseGraph,
|
|
40
|
+
nodes: [minimalNode("function:a"), minimalNode("function:b"), minimalNode("function:c")],
|
|
41
|
+
edges: [minimalEdge("function:a", "function:b"), minimalEdge("function:b", "function:c")],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const response = await POST(
|
|
45
|
+
new Request("http://localhost/api/analysis/cycles", {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "content-type": "application/json" },
|
|
48
|
+
body: JSON.stringify(graph),
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
const body = await response.json() as { report: { totalCycles: number; hasCycles: boolean } };
|
|
52
|
+
|
|
53
|
+
expect(response.status).toBe(200);
|
|
54
|
+
expect(body.report.totalCycles).toBe(0);
|
|
55
|
+
expect(body.report.hasCycles).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("detects a cycle between two mutually dependent nodes", async () => {
|
|
59
|
+
const graph: BlueprintGraph = {
|
|
60
|
+
...baseGraph,
|
|
61
|
+
nodes: [minimalNode("function:a"), minimalNode("function:b")],
|
|
62
|
+
edges: [minimalEdge("function:a", "function:b"), minimalEdge("function:b", "function:a")],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const response = await POST(
|
|
66
|
+
new Request("http://localhost/api/analysis/cycles", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "content-type": "application/json" },
|
|
69
|
+
body: JSON.stringify(graph),
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
const body = await response.json() as {
|
|
73
|
+
report: {
|
|
74
|
+
totalCycles: number;
|
|
75
|
+
cycles: Array<{ nodeIds: string[] }>;
|
|
76
|
+
affectedNodeIds: string[];
|
|
77
|
+
analyzedAt: string;
|
|
78
|
+
hasCycles: boolean;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
expect(response.status).toBe(200);
|
|
83
|
+
expect(body.report.totalCycles).toBe(1);
|
|
84
|
+
expect(body.report.affectedNodeIds).toContain("function:a");
|
|
85
|
+
expect(body.report.affectedNodeIds).toContain("function:b");
|
|
86
|
+
expect(body.report.hasCycles).toBe(true);
|
|
87
|
+
expect(body.report.analyzedAt).toBeTruthy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("detects a self-loop as a cycle", async () => {
|
|
91
|
+
const graph: BlueprintGraph = {
|
|
92
|
+
...baseGraph,
|
|
93
|
+
nodes: [minimalNode("function:a")],
|
|
94
|
+
edges: [{ from: "function:a", to: "function:a", kind: "calls", required: true, confidence: 1 }],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const response = await POST(
|
|
98
|
+
new Request("http://localhost/api/analysis/cycles", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: { "content-type": "application/json" },
|
|
101
|
+
body: JSON.stringify(graph),
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
const body = await response.json() as { report: { totalCycles: number; hasCycles: boolean } };
|
|
105
|
+
|
|
106
|
+
expect(response.status).toBe(200);
|
|
107
|
+
expect(body.report.totalCycles).toBe(1);
|
|
108
|
+
expect(body.report.hasCycles).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns 400 for an invalid request body", async () => {
|
|
112
|
+
const response = await POST(
|
|
113
|
+
new Request("http://localhost/api/analysis/cycles", {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "content-type": "application/json" },
|
|
116
|
+
body: JSON.stringify({ invalid: true }),
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
const body = await response.json() as { error: string };
|
|
120
|
+
|
|
121
|
+
expect(response.status).toBe(400);
|
|
122
|
+
expect(body.error).toBeTruthy();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { POST } from "../../../../handlers/metrics";
|
|
4
|
+
import type { BlueprintGraph } from "@/lib/blueprint/schema";
|
|
5
|
+
import { emptyContract } from "@/lib/blueprint/schema";
|
|
6
|
+
|
|
7
|
+
const minimalNode = (
|
|
8
|
+
id: string,
|
|
9
|
+
kind: BlueprintGraph["nodes"][number]["kind"] = "function"
|
|
10
|
+
): BlueprintGraph["nodes"][number] => ({
|
|
11
|
+
id,
|
|
12
|
+
kind,
|
|
13
|
+
name: id,
|
|
14
|
+
summary: "A node.",
|
|
15
|
+
contract: emptyContract(),
|
|
16
|
+
sourceRefs: [],
|
|
17
|
+
generatedRefs: [],
|
|
18
|
+
traceRefs: [],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const baseGraph: BlueprintGraph = {
|
|
22
|
+
projectName: "Metrics Route Test",
|
|
23
|
+
mode: "essential",
|
|
24
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
25
|
+
warnings: [],
|
|
26
|
+
workflows: [],
|
|
27
|
+
nodes: [],
|
|
28
|
+
edges: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("POST /api/analysis/metrics", () => {
|
|
32
|
+
it("returns zero metrics for an empty graph", async () => {
|
|
33
|
+
const response = await POST(
|
|
34
|
+
new Request("http://localhost/api/analysis/metrics", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
body: JSON.stringify(baseGraph),
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
const body = await response.json() as {
|
|
41
|
+
metrics: {
|
|
42
|
+
nodeCount: number;
|
|
43
|
+
edgeCount: number;
|
|
44
|
+
density: number;
|
|
45
|
+
connectedComponents: number;
|
|
46
|
+
analyzedAt: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
expect(response.status).toBe(200);
|
|
51
|
+
expect(body.metrics.nodeCount).toBe(0);
|
|
52
|
+
expect(body.metrics.edgeCount).toBe(0);
|
|
53
|
+
expect(body.metrics.density).toBe(0);
|
|
54
|
+
expect(body.metrics.connectedComponents).toBe(0);
|
|
55
|
+
expect(body.metrics.analyzedAt).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("computes correct metrics for a graph with mixed node kinds and edges", async () => {
|
|
59
|
+
const graph: BlueprintGraph = {
|
|
60
|
+
...baseGraph,
|
|
61
|
+
nodes: [
|
|
62
|
+
minimalNode("module:a", "module"),
|
|
63
|
+
minimalNode("api:b", "api"),
|
|
64
|
+
minimalNode("function:c", "function"),
|
|
65
|
+
minimalNode("function:d", "function"),
|
|
66
|
+
],
|
|
67
|
+
edges: [
|
|
68
|
+
{ from: "module:a", to: "api:b", kind: "calls", required: true, confidence: 1 },
|
|
69
|
+
{ from: "api:b", to: "function:c", kind: "calls", required: true, confidence: 1 },
|
|
70
|
+
{ from: "api:b", to: "function:d", kind: "calls", required: true, confidence: 1 },
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const response = await POST(
|
|
75
|
+
new Request("http://localhost/api/analysis/metrics", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "content-type": "application/json" },
|
|
78
|
+
body: JSON.stringify(graph),
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
const body = await response.json() as {
|
|
82
|
+
metrics: {
|
|
83
|
+
nodeCount: number;
|
|
84
|
+
edgeCount: number;
|
|
85
|
+
density: number;
|
|
86
|
+
nodesByKind: Record<string, number>;
|
|
87
|
+
edgesByKind: Record<string, number>;
|
|
88
|
+
maxInDegreeNodeId?: string;
|
|
89
|
+
connectedComponents: number;
|
|
90
|
+
isolatedNodes: number;
|
|
91
|
+
leafNodes: number;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
expect(response.status).toBe(200);
|
|
96
|
+
expect(body.metrics.nodeCount).toBe(4);
|
|
97
|
+
expect(body.metrics.edgeCount).toBe(3);
|
|
98
|
+
expect(body.metrics.density).toBeGreaterThan(0);
|
|
99
|
+
expect(body.metrics.nodesByKind["module"]).toBe(1);
|
|
100
|
+
expect(body.metrics.nodesByKind["api"]).toBe(1);
|
|
101
|
+
expect(body.metrics.nodesByKind["function"]).toBe(2);
|
|
102
|
+
expect(body.metrics.edgesByKind["calls"]).toBe(3);
|
|
103
|
+
expect(body.metrics.connectedComponents).toBe(1);
|
|
104
|
+
expect(body.metrics.isolatedNodes).toBe(0);
|
|
105
|
+
// module:a has out=1, api:b has out=2 in=1, function:c has in=1, function:d has in=1
|
|
106
|
+
expect(body.metrics.leafNodes).toBe(3);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns 400 for an invalid request body", async () => {
|
|
110
|
+
const response = await POST(
|
|
111
|
+
new Request("http://localhost/api/analysis/metrics", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "content-type": "application/json" },
|
|
114
|
+
body: JSON.stringify(42),
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
const body = await response.json() as { error: string };
|
|
118
|
+
|
|
119
|
+
expect(response.status).toBe(400);
|
|
120
|
+
expect(body.error).toBeTruthy();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { POST } from "../../../../handlers/smells";
|
|
4
|
+
import type { BlueprintGraph } from "@/lib/blueprint/schema";
|
|
5
|
+
import { emptyContract } from "@/lib/blueprint/schema";
|
|
6
|
+
|
|
7
|
+
const baseGraph: BlueprintGraph = {
|
|
8
|
+
projectName: "Smells Route Test",
|
|
9
|
+
mode: "essential",
|
|
10
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
11
|
+
warnings: [],
|
|
12
|
+
workflows: [],
|
|
13
|
+
nodes: [],
|
|
14
|
+
edges: [],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const makeMethod = (name: string) => ({
|
|
18
|
+
name,
|
|
19
|
+
summary: `Does ${name}.`,
|
|
20
|
+
inputs: [] as { name: string; type: string; description?: string }[],
|
|
21
|
+
outputs: [] as { name: string; type: string; description?: string }[],
|
|
22
|
+
sideEffects: [] as string[],
|
|
23
|
+
calls: [] as { target: string; kind?: string; description?: string }[],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("POST /api/analysis/smells", () => {
|
|
27
|
+
it("returns a clean smell report with health score near 100 for an empty graph", async () => {
|
|
28
|
+
const response = await POST(
|
|
29
|
+
new Request("http://localhost/api/analysis/smells", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "content-type": "application/json" },
|
|
32
|
+
body: JSON.stringify(baseGraph),
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
const body = await response.json() as {
|
|
36
|
+
report: { totalSmells: number; healthScore: number; analyzedAt: string };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(response.status).toBe(200);
|
|
40
|
+
expect(body.report.totalSmells).toBeGreaterThanOrEqual(0);
|
|
41
|
+
expect(body.report.analyzedAt).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("detects a god-node with too many methods and responsibilities", async () => {
|
|
45
|
+
const graph: BlueprintGraph = {
|
|
46
|
+
...baseGraph,
|
|
47
|
+
nodes: [
|
|
48
|
+
{
|
|
49
|
+
id: "module:god",
|
|
50
|
+
kind: "module",
|
|
51
|
+
name: "GodModule",
|
|
52
|
+
summary: "Does everything.",
|
|
53
|
+
contract: {
|
|
54
|
+
...emptyContract(),
|
|
55
|
+
methods: Array.from({ length: 8 }, (_, i) => makeMethod(`method${i}`)),
|
|
56
|
+
responsibilities: ["r1", "r2", "r3", "r4", "r5", "r6"],
|
|
57
|
+
},
|
|
58
|
+
sourceRefs: [],
|
|
59
|
+
generatedRefs: [],
|
|
60
|
+
traceRefs: [],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const response = await POST(
|
|
66
|
+
new Request("http://localhost/api/analysis/smells", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "content-type": "application/json" },
|
|
69
|
+
body: JSON.stringify(graph),
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
const body = await response.json() as {
|
|
73
|
+
report: {
|
|
74
|
+
totalSmells: number;
|
|
75
|
+
healthScore: number;
|
|
76
|
+
smells: Array<{ code: string; severity: string; nodeId?: string }>;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
expect(response.status).toBe(200);
|
|
81
|
+
expect(body.report.totalSmells).toBeGreaterThan(0);
|
|
82
|
+
expect(body.report.healthScore).toBeLessThan(100);
|
|
83
|
+
const godNodeSmell = body.report.smells.find((s) => s.code === "god-node");
|
|
84
|
+
expect(godNodeSmell).toBeDefined();
|
|
85
|
+
expect(godNodeSmell?.severity).toBe("critical");
|
|
86
|
+
expect(godNodeSmell?.nodeId).toBe("module:god");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("detects orphan nodes with no edges", async () => {
|
|
90
|
+
const graph: BlueprintGraph = {
|
|
91
|
+
...baseGraph,
|
|
92
|
+
nodes: [
|
|
93
|
+
{
|
|
94
|
+
id: "function:orphan",
|
|
95
|
+
kind: "function",
|
|
96
|
+
name: "orphanFn",
|
|
97
|
+
summary: "Nobody calls this.",
|
|
98
|
+
contract: emptyContract(),
|
|
99
|
+
sourceRefs: [],
|
|
100
|
+
generatedRefs: [],
|
|
101
|
+
traceRefs: [],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const response = await POST(
|
|
107
|
+
new Request("http://localhost/api/analysis/smells", {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "content-type": "application/json" },
|
|
110
|
+
body: JSON.stringify(graph),
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
const body = await response.json() as {
|
|
114
|
+
report: { smells: Array<{ code: string; nodeId?: string }> };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
expect(response.status).toBe(200);
|
|
118
|
+
expect(body.report.smells.some((s) => s.code === "orphan-node")).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns 400 for an invalid request body", async () => {
|
|
122
|
+
const response = await POST(
|
|
123
|
+
new Request("http://localhost/api/analysis/smells", {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { "content-type": "application/json" },
|
|
126
|
+
body: JSON.stringify({ not: "a graph" }),
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
const body = await response.json() as { error: string };
|
|
130
|
+
|
|
131
|
+
expect(response.status).toBe(400);
|
|
132
|
+
expect(body.error).toBeTruthy();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { POST } from "../../../handlers/conflicts";
|
|
6
|
+
import type { BlueprintGraph } from "@/lib/blueprint/schema";
|
|
7
|
+
import { emptyContract } from "@/lib/blueprint/schema";
|
|
8
|
+
|
|
9
|
+
const fixturePath = path.resolve(process.cwd(), "test-fixtures/sample-repo");
|
|
10
|
+
|
|
11
|
+
describe("POST /api/conflicts", () => {
|
|
12
|
+
it("returns drift conflicts against the repo fixture", async () => {
|
|
13
|
+
const graph: BlueprintGraph = {
|
|
14
|
+
projectName: "Conflict Route",
|
|
15
|
+
mode: "essential",
|
|
16
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
17
|
+
warnings: [],
|
|
18
|
+
workflows: [],
|
|
19
|
+
edges: [],
|
|
20
|
+
nodes: [
|
|
21
|
+
{
|
|
22
|
+
id: "function:normalize",
|
|
23
|
+
kind: "function",
|
|
24
|
+
name: "normalizeTask",
|
|
25
|
+
path: "src/services/task-service.ts",
|
|
26
|
+
summary: "Wrong summary.",
|
|
27
|
+
signature: "normalizeTask(input: string): string",
|
|
28
|
+
contract: { ...emptyContract(), summary: "Wrong summary." },
|
|
29
|
+
sourceRefs: [
|
|
30
|
+
{ kind: "repo", path: "src/services/task-service.ts", symbol: "normalizeTask" },
|
|
31
|
+
],
|
|
32
|
+
generatedRefs: [],
|
|
33
|
+
traceRefs: [],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const response = await POST(
|
|
39
|
+
new Request("http://localhost/api/conflicts", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "content-type": "application/json" },
|
|
42
|
+
body: JSON.stringify({ graph, repoPath: fixturePath }),
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
const body = await response.json() as { report: { conflicts: Array<{ kind: string }> } };
|
|
46
|
+
|
|
47
|
+
expect(response.status).toBe(200);
|
|
48
|
+
expect(body.report.conflicts.some((conflict) => conflict.kind === "signature-mismatch")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns 400 when repoPath is missing", async () => {
|
|
52
|
+
const graph: BlueprintGraph = {
|
|
53
|
+
projectName: "NoRepoPath",
|
|
54
|
+
mode: "essential",
|
|
55
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
56
|
+
warnings: [],
|
|
57
|
+
workflows: [],
|
|
58
|
+
edges: [],
|
|
59
|
+
nodes: [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const response = await POST(
|
|
63
|
+
new Request("http://localhost/api/conflicts", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "content-type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ graph }),
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(response.status).toBe(400);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { POST } from "../../../../handlers/refactor-detect";
|
|
4
|
+
import type { BlueprintGraph } from "@/lib/blueprint/schema";
|
|
5
|
+
import { emptyContract } from "@/lib/blueprint/schema";
|
|
6
|
+
|
|
7
|
+
const graph: BlueprintGraph = {
|
|
8
|
+
projectName: "Refactor Detect Route",
|
|
9
|
+
mode: "essential",
|
|
10
|
+
generatedAt: "2026-03-26T00:00:00.000Z",
|
|
11
|
+
warnings: [],
|
|
12
|
+
workflows: [],
|
|
13
|
+
nodes: [
|
|
14
|
+
{
|
|
15
|
+
id: "function:auth",
|
|
16
|
+
kind: "function",
|
|
17
|
+
name: "authenticate",
|
|
18
|
+
summary: "Authenticate a user.",
|
|
19
|
+
contract: {
|
|
20
|
+
...emptyContract(),
|
|
21
|
+
calls: [{ target: "GET /users", kind: "calls", description: undefined }],
|
|
22
|
+
},
|
|
23
|
+
sourceRefs: [],
|
|
24
|
+
generatedRefs: [],
|
|
25
|
+
traceRefs: [],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "api:users",
|
|
29
|
+
kind: "api",
|
|
30
|
+
name: "GET /users",
|
|
31
|
+
summary: "Users API.",
|
|
32
|
+
contract: emptyContract(),
|
|
33
|
+
sourceRefs: [],
|
|
34
|
+
generatedRefs: [],
|
|
35
|
+
traceRefs: [],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
edges: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("POST /api/refactor/detect", () => {
|
|
42
|
+
it("returns graph-scoped drift metadata", async () => {
|
|
43
|
+
const response = await POST(
|
|
44
|
+
new Request("http://localhost/api/refactor/detect", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "content-type": "application/json" },
|
|
47
|
+
body: JSON.stringify(graph),
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
const body = await response.json() as {
|
|
51
|
+
report: { totalIssues: number; provenance: string; maturity: string; scope: string };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
expect(response.status).toBe(200);
|
|
55
|
+
expect(body.report.totalIssues).toBeGreaterThan(0);
|
|
56
|
+
expect(body.report.provenance).toBe("deterministic");
|
|
57
|
+
expect(body.report.maturity).toBe("preview");
|
|
58
|
+
expect(body.report.scope).toBe("graph");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns 400 for an invalid request body", async () => {
|
|
62
|
+
const response = await POST(
|
|
63
|
+
new Request("http://localhost/api/refactor/detect", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "content-type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ invalid: true }),
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
const body = await response.json() as { error: string };
|
|
70
|
+
|
|
71
|
+
expect(response.status).toBe(400);
|
|
72
|
+
expect(body.error).toBeTruthy();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { POST } from "../../../../handlers/refactor-heal";
|
|
4
|
+
import type { BlueprintGraph } from "@/lib/blueprint/schema";
|
|
5
|
+
import { emptyContract } from "@/lib/blueprint/schema";
|
|
6
|
+
|
|
7
|
+
const graph: BlueprintGraph = {
|
|
8
|
+
projectName: "Refactor Heal Route",
|
|
9
|
+
mode: "essential",
|
|
10
|
+
generatedAt: "2026-03-26T00:00:00.000Z",
|
|
11
|
+
warnings: [],
|
|
12
|
+
workflows: [],
|
|
13
|
+
nodes: [
|
|
14
|
+
{
|
|
15
|
+
id: "function:auth",
|
|
16
|
+
kind: "function",
|
|
17
|
+
name: "authenticate",
|
|
18
|
+
summary: "Authenticate a user.",
|
|
19
|
+
contract: {
|
|
20
|
+
...emptyContract(),
|
|
21
|
+
calls: [{ target: "GET /users", kind: "calls", description: undefined }],
|
|
22
|
+
},
|
|
23
|
+
sourceRefs: [],
|
|
24
|
+
generatedRefs: [],
|
|
25
|
+
traceRefs: [],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "api:users",
|
|
29
|
+
kind: "api",
|
|
30
|
+
name: "GET /users",
|
|
31
|
+
summary: "Users API.",
|
|
32
|
+
contract: emptyContract(),
|
|
33
|
+
sourceRefs: [],
|
|
34
|
+
generatedRefs: [],
|
|
35
|
+
traceRefs: [],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
edges: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("POST /api/refactor/heal", () => {
|
|
42
|
+
it("heals graph drift and returns truthfulness metadata", async () => {
|
|
43
|
+
const response = await POST(
|
|
44
|
+
new Request("http://localhost/api/refactor/heal", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "content-type": "application/json" },
|
|
47
|
+
body: JSON.stringify(graph),
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
const body = await response.json() as {
|
|
51
|
+
result: {
|
|
52
|
+
issuesFixed: number;
|
|
53
|
+
provenance: string;
|
|
54
|
+
maturity: string;
|
|
55
|
+
scope: string;
|
|
56
|
+
graph: BlueprintGraph;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
expect(response.status).toBe(200);
|
|
61
|
+
expect(body.result.issuesFixed).toBeGreaterThan(0);
|
|
62
|
+
expect(body.result.provenance).toBe("deterministic");
|
|
63
|
+
expect(body.result.maturity).toBe("preview");
|
|
64
|
+
expect(body.result.scope).toBe("graph");
|
|
65
|
+
expect(
|
|
66
|
+
body.result.graph.edges.some(
|
|
67
|
+
(edge: { from: string; to: string }) => edge.from === "function:auth" && edge.to === "api:users"
|
|
68
|
+
)
|
|
69
|
+
).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns 400 for an invalid request body", async () => {
|
|
73
|
+
const response = await POST(
|
|
74
|
+
new Request("http://localhost/api/refactor/heal", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "content-type": "application/json" },
|
|
77
|
+
body: JSON.stringify({ invalid: true }),
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
const body = await response.json() as { error: string };
|
|
81
|
+
|
|
82
|
+
expect(response.status).toBe(400);
|
|
83
|
+
expect(body.error).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
});
|