@abhinav2203/codeflow-analysis 0.1.0 → 0.1.1
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/app/api/analysis/cycles/route.test.d.ts +2 -0
- package/dist/app/api/analysis/cycles/route.test.d.ts.map +1 -0
- package/dist/app/api/analysis/cycles/route.test.js +92 -0
- package/dist/app/api/analysis/metrics/route.test.d.ts +2 -0
- package/dist/app/api/analysis/metrics/route.test.d.ts.map +1 -0
- package/dist/app/api/analysis/metrics/route.test.js +82 -0
- package/dist/app/api/analysis/smells/route.test.d.ts +2 -0
- package/dist/app/api/analysis/smells/route.test.d.ts.map +1 -0
- package/dist/app/api/analysis/smells/route.test.js +102 -0
- package/dist/app/api/conflicts/route.test.d.ts +2 -0
- package/dist/app/api/conflicts/route.test.d.ts.map +1 -0
- package/dist/app/api/conflicts/route.test.js +58 -0
- package/dist/app/api/refactor/detect/route.test.d.ts +2 -0
- package/dist/app/api/refactor/detect/route.test.d.ts.map +1 -0
- package/dist/app/api/refactor/detect/route.test.js +61 -0
- package/dist/app/api/refactor/heal/route.test.d.ts +2 -0
- package/dist/app/api/refactor/heal/route.test.d.ts.map +1 -0
- package/dist/app/api/refactor/heal/route.test.js +62 -0
- package/dist/bin/cli.js +5 -41
- package/dist/conflicts.d.ts +12 -0
- package/dist/conflicts.d.ts.map +1 -0
- package/dist/conflicts.js +77 -0
- package/dist/conflicts.test.d.ts +2 -0
- package/dist/conflicts.test.d.ts.map +1 -0
- package/dist/conflicts.test.js +98 -0
- package/dist/cycles.d.ts +39 -0
- package/dist/cycles.d.ts.map +1 -0
- package/dist/cycles.js +129 -0
- package/dist/cycles.test.d.ts +2 -0
- package/dist/cycles.test.d.ts.map +1 -0
- package/dist/cycles.test.js +82 -0
- package/dist/handlers/conflicts.d.ts +28 -0
- package/dist/handlers/conflicts.d.ts.map +1 -0
- package/dist/handlers/conflicts.js +24 -0
- package/dist/handlers/cycles.d.ts +30 -0
- package/dist/handlers/cycles.d.ts.map +1 -0
- package/dist/handlers/cycles.js +24 -0
- package/dist/handlers/metrics.d.ts +35 -0
- package/dist/handlers/metrics.d.ts.map +1 -0
- package/dist/handlers/metrics.js +23 -0
- package/dist/handlers/refactor-detect.d.ts +15 -0
- package/dist/handlers/refactor-detect.d.ts.map +1 -0
- package/dist/handlers/refactor-detect.js +23 -0
- package/dist/handlers/refactor-heal.d.ts +19 -0
- package/dist/handlers/refactor-heal.d.ts.map +1 -0
- package/dist/handlers/refactor-heal.js +27 -0
- package/dist/handlers/smells.d.ts +27 -0
- package/dist/handlers/smells.d.ts.map +1 -0
- package/dist/handlers/smells.js +24 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/invoke.d.ts +3 -0
- package/dist/invoke.d.ts.map +1 -0
- package/dist/invoke.js +162 -0
- package/dist/metrics.d.ts +33 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +159 -0
- package/dist/metrics.test.d.ts +2 -0
- package/dist/metrics.test.d.ts.map +1 -0
- package/dist/metrics.test.js +98 -0
- package/dist/refactor.d.ts +84 -0
- package/dist/refactor.d.ts.map +1 -0
- package/dist/refactor.js +183 -0
- package/dist/refactor.test.d.ts +2 -0
- package/dist/refactor.test.d.ts.map +1 -0
- package/dist/refactor.test.js +269 -0
- package/dist/smells.d.ts +39 -0
- package/dist/smells.d.ts.map +1 -0
- package/dist/smells.js +186 -0
- package/dist/smells.test.d.ts +2 -0
- package/dist/smells.test.d.ts.map +1 -0
- package/dist/smells.test.js +120 -0
- package/package.json +3 -3
- package/src/app/api/analysis/cycles/route.test.ts +2 -2
- package/src/app/api/analysis/metrics/route.test.ts +2 -2
- package/src/app/api/analysis/smells/route.test.ts +7 -7
- package/src/app/api/conflicts/route.test.ts +2 -2
- package/src/app/api/refactor/detect/route.test.ts +2 -2
- package/src/app/api/refactor/heal/route.test.ts +2 -2
- package/src/conflicts.test.ts +2 -2
- package/src/conflicts.ts +2 -2
- package/src/cycles.test.ts +2 -2
- package/src/cycles.ts +1 -1
- package/src/handlers/conflicts.ts +1 -1
- package/src/handlers/cycles.ts +1 -1
- package/src/handlers/metrics.ts +1 -1
- package/src/handlers/refactor-detect.ts +1 -1
- package/src/handlers/refactor-heal.ts +1 -1
- package/src/handlers/smells.ts +1 -1
- package/src/invoke.ts +116 -113
- package/src/metrics.test.ts +3 -3
- package/src/metrics.ts +1 -1
- package/src/refactor.test.ts +4 -4
- package/src/refactor.ts +1 -1
- package/src/smells.test.ts +7 -7
- package/src/smells.ts +1 -1
- package/vitest.config.ts +0 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.test.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/analysis/cycles/route.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { POST } from "../../../../handlers/cycles";
|
|
3
|
+
import { emptyContract } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
const minimalNode = (id) => ({
|
|
5
|
+
id,
|
|
6
|
+
kind: "function",
|
|
7
|
+
name: id,
|
|
8
|
+
summary: "A node.",
|
|
9
|
+
contract: emptyContract(),
|
|
10
|
+
sourceRefs: [],
|
|
11
|
+
generatedRefs: [],
|
|
12
|
+
traceRefs: [],
|
|
13
|
+
});
|
|
14
|
+
const minimalEdge = (from, to) => ({
|
|
15
|
+
from,
|
|
16
|
+
to,
|
|
17
|
+
kind: "calls",
|
|
18
|
+
required: true,
|
|
19
|
+
confidence: 1,
|
|
20
|
+
});
|
|
21
|
+
const baseGraph = {
|
|
22
|
+
projectName: "Cycles Route Test",
|
|
23
|
+
mode: "essential",
|
|
24
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
25
|
+
warnings: [],
|
|
26
|
+
workflows: [],
|
|
27
|
+
nodes: [],
|
|
28
|
+
edges: [],
|
|
29
|
+
};
|
|
30
|
+
describe("POST /api/analysis/cycles", () => {
|
|
31
|
+
it("returns a cycle report with no cycles for a clean DAG", async () => {
|
|
32
|
+
const graph = {
|
|
33
|
+
...baseGraph,
|
|
34
|
+
nodes: [minimalNode("function:a"), minimalNode("function:b"), minimalNode("function:c")],
|
|
35
|
+
edges: [minimalEdge("function:a", "function:b"), minimalEdge("function:b", "function:c")],
|
|
36
|
+
};
|
|
37
|
+
const response = await POST(new Request("http://localhost/api/analysis/cycles", {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
body: JSON.stringify(graph),
|
|
41
|
+
}));
|
|
42
|
+
const body = await response.json();
|
|
43
|
+
expect(response.status).toBe(200);
|
|
44
|
+
expect(body.report.totalCycles).toBe(0);
|
|
45
|
+
expect(body.report.hasCycles).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it("detects a cycle between two mutually dependent nodes", async () => {
|
|
48
|
+
const graph = {
|
|
49
|
+
...baseGraph,
|
|
50
|
+
nodes: [minimalNode("function:a"), minimalNode("function:b")],
|
|
51
|
+
edges: [minimalEdge("function:a", "function:b"), minimalEdge("function:b", "function:a")],
|
|
52
|
+
};
|
|
53
|
+
const response = await POST(new Request("http://localhost/api/analysis/cycles", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "content-type": "application/json" },
|
|
56
|
+
body: JSON.stringify(graph),
|
|
57
|
+
}));
|
|
58
|
+
const body = await response.json();
|
|
59
|
+
expect(response.status).toBe(200);
|
|
60
|
+
expect(body.report.totalCycles).toBe(1);
|
|
61
|
+
expect(body.report.affectedNodeIds).toContain("function:a");
|
|
62
|
+
expect(body.report.affectedNodeIds).toContain("function:b");
|
|
63
|
+
expect(body.report.hasCycles).toBe(true);
|
|
64
|
+
expect(body.report.analyzedAt).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
it("detects a self-loop as a cycle", async () => {
|
|
67
|
+
const graph = {
|
|
68
|
+
...baseGraph,
|
|
69
|
+
nodes: [minimalNode("function:a")],
|
|
70
|
+
edges: [{ from: "function:a", to: "function:a", kind: "calls", required: true, confidence: 1 }],
|
|
71
|
+
};
|
|
72
|
+
const response = await POST(new Request("http://localhost/api/analysis/cycles", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "content-type": "application/json" },
|
|
75
|
+
body: JSON.stringify(graph),
|
|
76
|
+
}));
|
|
77
|
+
const body = await response.json();
|
|
78
|
+
expect(response.status).toBe(200);
|
|
79
|
+
expect(body.report.totalCycles).toBe(1);
|
|
80
|
+
expect(body.report.hasCycles).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it("returns 400 for an invalid request body", async () => {
|
|
83
|
+
const response = await POST(new Request("http://localhost/api/analysis/cycles", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "content-type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ invalid: true }),
|
|
87
|
+
}));
|
|
88
|
+
const body = await response.json();
|
|
89
|
+
expect(response.status).toBe(400);
|
|
90
|
+
expect(body.error).toBeTruthy();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.test.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/analysis/metrics/route.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { POST } from "../../../../handlers/metrics";
|
|
3
|
+
import { emptyContract } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
const minimalNode = (id, kind = "function") => ({
|
|
5
|
+
id,
|
|
6
|
+
kind,
|
|
7
|
+
name: id,
|
|
8
|
+
summary: "A node.",
|
|
9
|
+
contract: emptyContract(),
|
|
10
|
+
sourceRefs: [],
|
|
11
|
+
generatedRefs: [],
|
|
12
|
+
traceRefs: [],
|
|
13
|
+
});
|
|
14
|
+
const baseGraph = {
|
|
15
|
+
projectName: "Metrics Route Test",
|
|
16
|
+
mode: "essential",
|
|
17
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
18
|
+
warnings: [],
|
|
19
|
+
workflows: [],
|
|
20
|
+
nodes: [],
|
|
21
|
+
edges: [],
|
|
22
|
+
};
|
|
23
|
+
describe("POST /api/analysis/metrics", () => {
|
|
24
|
+
it("returns zero metrics for an empty graph", async () => {
|
|
25
|
+
const response = await POST(new Request("http://localhost/api/analysis/metrics", {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "content-type": "application/json" },
|
|
28
|
+
body: JSON.stringify(baseGraph),
|
|
29
|
+
}));
|
|
30
|
+
const body = await response.json();
|
|
31
|
+
expect(response.status).toBe(200);
|
|
32
|
+
expect(body.metrics.nodeCount).toBe(0);
|
|
33
|
+
expect(body.metrics.edgeCount).toBe(0);
|
|
34
|
+
expect(body.metrics.density).toBe(0);
|
|
35
|
+
expect(body.metrics.connectedComponents).toBe(0);
|
|
36
|
+
expect(body.metrics.analyzedAt).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
it("computes correct metrics for a graph with mixed node kinds and edges", async () => {
|
|
39
|
+
const graph = {
|
|
40
|
+
...baseGraph,
|
|
41
|
+
nodes: [
|
|
42
|
+
minimalNode("module:a", "module"),
|
|
43
|
+
minimalNode("api:b", "api"),
|
|
44
|
+
minimalNode("function:c", "function"),
|
|
45
|
+
minimalNode("function:d", "function"),
|
|
46
|
+
],
|
|
47
|
+
edges: [
|
|
48
|
+
{ from: "module:a", to: "api:b", kind: "calls", required: true, confidence: 1 },
|
|
49
|
+
{ from: "api:b", to: "function:c", kind: "calls", required: true, confidence: 1 },
|
|
50
|
+
{ from: "api:b", to: "function:d", kind: "calls", required: true, confidence: 1 },
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
const response = await POST(new Request("http://localhost/api/analysis/metrics", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "content-type": "application/json" },
|
|
56
|
+
body: JSON.stringify(graph),
|
|
57
|
+
}));
|
|
58
|
+
const body = await response.json();
|
|
59
|
+
expect(response.status).toBe(200);
|
|
60
|
+
expect(body.metrics.nodeCount).toBe(4);
|
|
61
|
+
expect(body.metrics.edgeCount).toBe(3);
|
|
62
|
+
expect(body.metrics.density).toBeGreaterThan(0);
|
|
63
|
+
expect(body.metrics.nodesByKind["module"]).toBe(1);
|
|
64
|
+
expect(body.metrics.nodesByKind["api"]).toBe(1);
|
|
65
|
+
expect(body.metrics.nodesByKind["function"]).toBe(2);
|
|
66
|
+
expect(body.metrics.edgesByKind["calls"]).toBe(3);
|
|
67
|
+
expect(body.metrics.connectedComponents).toBe(1);
|
|
68
|
+
expect(body.metrics.isolatedNodes).toBe(0);
|
|
69
|
+
// module:a has out=1, api:b has out=2 in=1, function:c has in=1, function:d has in=1
|
|
70
|
+
expect(body.metrics.leafNodes).toBe(3);
|
|
71
|
+
});
|
|
72
|
+
it("returns 400 for an invalid request body", async () => {
|
|
73
|
+
const response = await POST(new Request("http://localhost/api/analysis/metrics", {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "content-type": "application/json" },
|
|
76
|
+
body: JSON.stringify(42),
|
|
77
|
+
}));
|
|
78
|
+
const body = await response.json();
|
|
79
|
+
expect(response.status).toBe(400);
|
|
80
|
+
expect(body.error).toBeTruthy();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.test.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/analysis/smells/route.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { POST } from "../../../../handlers/smells";
|
|
3
|
+
import { emptyContract } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
const baseGraph = {
|
|
5
|
+
projectName: "Smells Route Test",
|
|
6
|
+
mode: "essential",
|
|
7
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
8
|
+
warnings: [],
|
|
9
|
+
workflows: [],
|
|
10
|
+
nodes: [],
|
|
11
|
+
edges: [],
|
|
12
|
+
};
|
|
13
|
+
const makeMethod = (name) => ({
|
|
14
|
+
name,
|
|
15
|
+
summary: `Does ${name}.`,
|
|
16
|
+
inputs: [],
|
|
17
|
+
outputs: [],
|
|
18
|
+
sideEffects: [],
|
|
19
|
+
calls: [],
|
|
20
|
+
});
|
|
21
|
+
describe("POST /api/analysis/smells", () => {
|
|
22
|
+
it("returns a clean smell report with health score near 100 for an empty graph", async () => {
|
|
23
|
+
const response = await POST(new Request("http://localhost/api/analysis/smells", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "content-type": "application/json" },
|
|
26
|
+
body: JSON.stringify(baseGraph),
|
|
27
|
+
}));
|
|
28
|
+
const body = await response.json();
|
|
29
|
+
expect(response.status).toBe(200);
|
|
30
|
+
expect(body.report.totalSmells).toBeGreaterThanOrEqual(0);
|
|
31
|
+
expect(body.report.analyzedAt).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
it("detects a god-node with too many methods and responsibilities", async () => {
|
|
34
|
+
const graph = {
|
|
35
|
+
...baseGraph,
|
|
36
|
+
nodes: [
|
|
37
|
+
{
|
|
38
|
+
id: "module:god",
|
|
39
|
+
kind: "module",
|
|
40
|
+
name: "GodModule",
|
|
41
|
+
summary: "Does everything.",
|
|
42
|
+
contract: {
|
|
43
|
+
...emptyContract(),
|
|
44
|
+
methods: Array.from({ length: 8 }, (_, i) => makeMethod(`method${i}`)),
|
|
45
|
+
responsibilities: ["r1", "r2", "r3", "r4", "r5", "r6"],
|
|
46
|
+
},
|
|
47
|
+
sourceRefs: [],
|
|
48
|
+
generatedRefs: [],
|
|
49
|
+
traceRefs: [],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
const response = await POST(new Request("http://localhost/api/analysis/smells", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "content-type": "application/json" },
|
|
56
|
+
body: JSON.stringify(graph),
|
|
57
|
+
}));
|
|
58
|
+
const body = await response.json();
|
|
59
|
+
expect(response.status).toBe(200);
|
|
60
|
+
expect(body.report.totalSmells).toBeGreaterThan(0);
|
|
61
|
+
expect(body.report.healthScore).toBeLessThan(100);
|
|
62
|
+
const godNodeSmell = body.report.smells.find((s) => s.code === "god-node");
|
|
63
|
+
expect(godNodeSmell).toBeDefined();
|
|
64
|
+
expect(godNodeSmell?.severity).toBe("critical");
|
|
65
|
+
expect(godNodeSmell?.nodeId).toBe("module:god");
|
|
66
|
+
});
|
|
67
|
+
it("detects orphan nodes with no edges", async () => {
|
|
68
|
+
const graph = {
|
|
69
|
+
...baseGraph,
|
|
70
|
+
nodes: [
|
|
71
|
+
{
|
|
72
|
+
id: "function:orphan",
|
|
73
|
+
kind: "function",
|
|
74
|
+
name: "orphanFn",
|
|
75
|
+
summary: "Nobody calls this.",
|
|
76
|
+
contract: emptyContract(),
|
|
77
|
+
sourceRefs: [],
|
|
78
|
+
generatedRefs: [],
|
|
79
|
+
traceRefs: [],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
const response = await POST(new Request("http://localhost/api/analysis/smells", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "content-type": "application/json" },
|
|
86
|
+
body: JSON.stringify(graph),
|
|
87
|
+
}));
|
|
88
|
+
const body = await response.json();
|
|
89
|
+
expect(response.status).toBe(200);
|
|
90
|
+
expect(body.report.smells.some((s) => s.code === "orphan-node")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it("returns 400 for an invalid request body", async () => {
|
|
93
|
+
const response = await POST(new Request("http://localhost/api/analysis/smells", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "content-type": "application/json" },
|
|
96
|
+
body: JSON.stringify({ not: "a graph" }),
|
|
97
|
+
}));
|
|
98
|
+
const body = await response.json();
|
|
99
|
+
expect(response.status).toBe(400);
|
|
100
|
+
expect(body.error).toBeTruthy();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.test.d.ts","sourceRoot":"","sources":["../../../../src/app/api/conflicts/route.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { POST } from "../../../handlers/conflicts";
|
|
4
|
+
import { emptyContract } from "@abhinav2203/codeflow-core/schema";
|
|
5
|
+
const fixturePath = path.resolve(process.cwd(), "test-fixtures/sample-repo");
|
|
6
|
+
describe("POST /api/conflicts", () => {
|
|
7
|
+
it("returns drift conflicts against the repo fixture", async () => {
|
|
8
|
+
const graph = {
|
|
9
|
+
projectName: "Conflict Route",
|
|
10
|
+
mode: "essential",
|
|
11
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
12
|
+
warnings: [],
|
|
13
|
+
workflows: [],
|
|
14
|
+
edges: [],
|
|
15
|
+
nodes: [
|
|
16
|
+
{
|
|
17
|
+
id: "function:normalize",
|
|
18
|
+
kind: "function",
|
|
19
|
+
name: "normalizeTask",
|
|
20
|
+
path: "src/services/task-service.ts",
|
|
21
|
+
summary: "Wrong summary.",
|
|
22
|
+
signature: "normalizeTask(input: string): string",
|
|
23
|
+
contract: { ...emptyContract(), summary: "Wrong summary." },
|
|
24
|
+
sourceRefs: [
|
|
25
|
+
{ kind: "repo", path: "src/services/task-service.ts", symbol: "normalizeTask" },
|
|
26
|
+
],
|
|
27
|
+
generatedRefs: [],
|
|
28
|
+
traceRefs: [],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
const response = await POST(new Request("http://localhost/api/conflicts", {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "content-type": "application/json" },
|
|
35
|
+
body: JSON.stringify({ graph, repoPath: fixturePath }),
|
|
36
|
+
}));
|
|
37
|
+
const body = await response.json();
|
|
38
|
+
expect(response.status).toBe(200);
|
|
39
|
+
expect(body.report.conflicts.some((conflict) => conflict.kind === "signature-mismatch")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it("returns 400 when repoPath is missing", async () => {
|
|
42
|
+
const graph = {
|
|
43
|
+
projectName: "NoRepoPath",
|
|
44
|
+
mode: "essential",
|
|
45
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
46
|
+
warnings: [],
|
|
47
|
+
workflows: [],
|
|
48
|
+
edges: [],
|
|
49
|
+
nodes: [],
|
|
50
|
+
};
|
|
51
|
+
const response = await POST(new Request("http://localhost/api/conflicts", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
body: JSON.stringify({ graph }),
|
|
55
|
+
}));
|
|
56
|
+
expect(response.status).toBe(400);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.test.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/refactor/detect/route.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { POST } from "../../../../handlers/refactor-detect";
|
|
3
|
+
import { emptyContract } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
const graph = {
|
|
5
|
+
projectName: "Refactor Detect Route",
|
|
6
|
+
mode: "essential",
|
|
7
|
+
generatedAt: "2026-03-26T00:00:00.000Z",
|
|
8
|
+
warnings: [],
|
|
9
|
+
workflows: [],
|
|
10
|
+
nodes: [
|
|
11
|
+
{
|
|
12
|
+
id: "function:auth",
|
|
13
|
+
kind: "function",
|
|
14
|
+
name: "authenticate",
|
|
15
|
+
summary: "Authenticate a user.",
|
|
16
|
+
contract: {
|
|
17
|
+
...emptyContract(),
|
|
18
|
+
calls: [{ target: "GET /users", kind: "calls", description: undefined }],
|
|
19
|
+
},
|
|
20
|
+
sourceRefs: [],
|
|
21
|
+
generatedRefs: [],
|
|
22
|
+
traceRefs: [],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "api:users",
|
|
26
|
+
kind: "api",
|
|
27
|
+
name: "GET /users",
|
|
28
|
+
summary: "Users API.",
|
|
29
|
+
contract: emptyContract(),
|
|
30
|
+
sourceRefs: [],
|
|
31
|
+
generatedRefs: [],
|
|
32
|
+
traceRefs: [],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
edges: [],
|
|
36
|
+
};
|
|
37
|
+
describe("POST /api/refactor/detect", () => {
|
|
38
|
+
it("returns graph-scoped drift metadata", async () => {
|
|
39
|
+
const response = await POST(new Request("http://localhost/api/refactor/detect", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "content-type": "application/json" },
|
|
42
|
+
body: JSON.stringify(graph),
|
|
43
|
+
}));
|
|
44
|
+
const body = await response.json();
|
|
45
|
+
expect(response.status).toBe(200);
|
|
46
|
+
expect(body.report.totalIssues).toBeGreaterThan(0);
|
|
47
|
+
expect(body.report.provenance).toBe("deterministic");
|
|
48
|
+
expect(body.report.maturity).toBe("preview");
|
|
49
|
+
expect(body.report.scope).toBe("graph");
|
|
50
|
+
});
|
|
51
|
+
it("returns 400 for an invalid request body", async () => {
|
|
52
|
+
const response = await POST(new Request("http://localhost/api/refactor/detect", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "content-type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ invalid: true }),
|
|
56
|
+
}));
|
|
57
|
+
const body = await response.json();
|
|
58
|
+
expect(response.status).toBe(400);
|
|
59
|
+
expect(body.error).toBeTruthy();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.test.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/refactor/heal/route.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { POST } from "../../../../handlers/refactor-heal";
|
|
3
|
+
import { emptyContract } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
const graph = {
|
|
5
|
+
projectName: "Refactor Heal Route",
|
|
6
|
+
mode: "essential",
|
|
7
|
+
generatedAt: "2026-03-26T00:00:00.000Z",
|
|
8
|
+
warnings: [],
|
|
9
|
+
workflows: [],
|
|
10
|
+
nodes: [
|
|
11
|
+
{
|
|
12
|
+
id: "function:auth",
|
|
13
|
+
kind: "function",
|
|
14
|
+
name: "authenticate",
|
|
15
|
+
summary: "Authenticate a user.",
|
|
16
|
+
contract: {
|
|
17
|
+
...emptyContract(),
|
|
18
|
+
calls: [{ target: "GET /users", kind: "calls", description: undefined }],
|
|
19
|
+
},
|
|
20
|
+
sourceRefs: [],
|
|
21
|
+
generatedRefs: [],
|
|
22
|
+
traceRefs: [],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "api:users",
|
|
26
|
+
kind: "api",
|
|
27
|
+
name: "GET /users",
|
|
28
|
+
summary: "Users API.",
|
|
29
|
+
contract: emptyContract(),
|
|
30
|
+
sourceRefs: [],
|
|
31
|
+
generatedRefs: [],
|
|
32
|
+
traceRefs: [],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
edges: [],
|
|
36
|
+
};
|
|
37
|
+
describe("POST /api/refactor/heal", () => {
|
|
38
|
+
it("heals graph drift and returns truthfulness metadata", async () => {
|
|
39
|
+
const response = await POST(new Request("http://localhost/api/refactor/heal", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "content-type": "application/json" },
|
|
42
|
+
body: JSON.stringify(graph),
|
|
43
|
+
}));
|
|
44
|
+
const body = await response.json();
|
|
45
|
+
expect(response.status).toBe(200);
|
|
46
|
+
expect(body.result.issuesFixed).toBeGreaterThan(0);
|
|
47
|
+
expect(body.result.provenance).toBe("deterministic");
|
|
48
|
+
expect(body.result.maturity).toBe("preview");
|
|
49
|
+
expect(body.result.scope).toBe("graph");
|
|
50
|
+
expect(body.result.graph.edges.some((edge) => edge.from === "function:auth" && edge.to === "api:users")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it("returns 400 for an invalid request body", async () => {
|
|
53
|
+
const response = await POST(new Request("http://localhost/api/refactor/heal", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "content-type": "application/json" },
|
|
56
|
+
body: JSON.stringify({ invalid: true }),
|
|
57
|
+
}));
|
|
58
|
+
const body = await response.json();
|
|
59
|
+
expect(response.status).toBe(400);
|
|
60
|
+
expect(body.error).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
});
|
package/dist/bin/cli.js
CHANGED
|
@@ -1,43 +1,7 @@
|
|
|
1
|
-
|
|
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
|
-
*/
|
|
1
|
+
// Auto-generated bin entry — delegates to the invoke CLI
|
|
2
|
+
import { runCLI } from "../invoke.js";
|
|
10
3
|
|
|
11
|
-
|
|
12
|
-
|
|
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);
|
|
4
|
+
runCLI().catch((e) => {
|
|
5
|
+
console.error(e);
|
|
42
6
|
process.exit(1);
|
|
43
|
-
}
|
|
7
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BlueprintGraph, ConflictReport } from "@abhinav2203/codeflow-core/schema";
|
|
2
|
+
/**
|
|
3
|
+
* Detect structural conflicts between a blueprint graph and a live TypeScript repository.
|
|
4
|
+
*
|
|
5
|
+
* Conflicts detected:
|
|
6
|
+
* - `missing-in-repo` – blueprint node has no corresponding symbol in the repo snapshot.
|
|
7
|
+
* - `missing-in-blueprint` – repo has a symbol not represented in the blueprint.
|
|
8
|
+
* - `signature-mismatch` – blueprint node `signature` differs from the repo-derived signature.
|
|
9
|
+
* - `summary-mismatch` – blueprint node `summary` differs from the repo-derived summary.
|
|
10
|
+
*/
|
|
11
|
+
export declare const detectGraphConflicts: (graph: BlueprintGraph, repoPath: string) => Promise<ConflictReport>;
|
|
12
|
+
//# sourceMappingURL=conflicts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflicts.d.ts","sourceRoot":"","sources":["../src/conflicts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,cAAc,EAGd,cAAc,EACf,MAAM,mCAAmC,CAAC;AAK3C;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,cAAc,EACrB,UAAU,MAAM,KACf,OAAO,CAAC,cAAc,CAgFxB,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { analyzeTypeScriptRepo } from "@abhinav2203/codeflow-core/analyzer";
|
|
3
|
+
const repoKeyForNode = (node) => `${node.kind}:${node.path ?? ""}:${node.name}`;
|
|
4
|
+
/**
|
|
5
|
+
* Detect structural conflicts between a blueprint graph and a live TypeScript repository.
|
|
6
|
+
*
|
|
7
|
+
* Conflicts detected:
|
|
8
|
+
* - `missing-in-repo` – blueprint node has no corresponding symbol in the repo snapshot.
|
|
9
|
+
* - `missing-in-blueprint` – repo has a symbol not represented in the blueprint.
|
|
10
|
+
* - `signature-mismatch` – blueprint node `signature` differs from the repo-derived signature.
|
|
11
|
+
* - `summary-mismatch` – blueprint node `summary` differs from the repo-derived summary.
|
|
12
|
+
*/
|
|
13
|
+
export const detectGraphConflicts = async (graph, repoPath) => {
|
|
14
|
+
const repoGraph = await analyzeTypeScriptRepo(path.resolve(repoPath));
|
|
15
|
+
const conflicts = [];
|
|
16
|
+
// Only consider code-bearing nodes (not modules, which are structural containers).
|
|
17
|
+
const repoNodes = repoGraph.nodes.filter((node) => node.kind !== "module");
|
|
18
|
+
const blueprintRepoNodes = graph.nodes.filter((node) => node.sourceRefs.some((ref) => ref.kind === "repo"));
|
|
19
|
+
const repoMap = new Map(repoNodes.map((node) => [repoKeyForNode(node), node]));
|
|
20
|
+
const blueprintMap = new Map(blueprintRepoNodes.map((node) => [repoKeyForNode(node), node]));
|
|
21
|
+
// Check each blueprint node against the repo snapshot.
|
|
22
|
+
for (const blueprintNode of blueprintRepoNodes) {
|
|
23
|
+
const repoNode = repoMap.get(repoKeyForNode(blueprintNode));
|
|
24
|
+
if (!repoNode) {
|
|
25
|
+
conflicts.push({
|
|
26
|
+
kind: "missing-in-repo",
|
|
27
|
+
nodeId: blueprintNode.id,
|
|
28
|
+
path: blueprintNode.path,
|
|
29
|
+
blueprintValue: blueprintNode.name,
|
|
30
|
+
message: `${blueprintNode.name} is in the blueprint but not in the repo snapshot.`,
|
|
31
|
+
suggestedAction: "Remove the node from the blueprint or recreate it in the repo.",
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if ((blueprintNode.signature ?? "") !== (repoNode.signature ?? "")) {
|
|
36
|
+
conflicts.push({
|
|
37
|
+
kind: "signature-mismatch",
|
|
38
|
+
nodeId: blueprintNode.id,
|
|
39
|
+
path: blueprintNode.path,
|
|
40
|
+
blueprintValue: blueprintNode.signature,
|
|
41
|
+
repoValue: repoNode.signature,
|
|
42
|
+
message: `${blueprintNode.name} has a different signature in the repo.`,
|
|
43
|
+
suggestedAction: "Refresh the blueprint contract from the repo or update the implementation.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (blueprintNode.summary &&
|
|
47
|
+
repoNode.summary &&
|
|
48
|
+
blueprintNode.summary !== repoNode.summary) {
|
|
49
|
+
conflicts.push({
|
|
50
|
+
kind: "summary-mismatch",
|
|
51
|
+
nodeId: blueprintNode.id,
|
|
52
|
+
path: blueprintNode.path,
|
|
53
|
+
blueprintValue: blueprintNode.summary,
|
|
54
|
+
repoValue: repoNode.summary,
|
|
55
|
+
message: `${blueprintNode.name} summary diverges from the repo-derived description.`,
|
|
56
|
+
suggestedAction: "Review the contract summary and align it with current behavior.",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Detect repo symbols missing from the blueprint.
|
|
61
|
+
for (const repoNode of repoNodes) {
|
|
62
|
+
if (!blueprintMap.has(repoKeyForNode(repoNode))) {
|
|
63
|
+
conflicts.push({
|
|
64
|
+
kind: "missing-in-blueprint",
|
|
65
|
+
path: repoNode.path,
|
|
66
|
+
repoValue: repoNode.name,
|
|
67
|
+
message: `${repoNode.name} exists in the repo but is not represented in the blueprint.`,
|
|
68
|
+
suggestedAction: "Add the node to the blueprint or mark it intentionally out of scope.",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
checkedAt: new Date().toISOString(),
|
|
74
|
+
repoPath: path.resolve(repoPath),
|
|
75
|
+
conflicts,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflicts.test.d.ts","sourceRoot":"","sources":["../src/conflicts.test.ts"],"names":[],"mappings":""}
|