@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,98 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { detectGraphConflicts } from "./conflicts";
|
|
4
|
+
import { emptyContract } from "@abhinav2203/codeflow-core/schema";
|
|
5
|
+
const fixturePath = path.resolve(process.cwd(), "test-fixtures/sample-repo");
|
|
6
|
+
const node = (id, overrides = {}) => ({
|
|
7
|
+
id,
|
|
8
|
+
kind: overrides.kind ?? "function",
|
|
9
|
+
name: overrides.name ?? id,
|
|
10
|
+
path: overrides.path,
|
|
11
|
+
summary: overrides.summary ?? id,
|
|
12
|
+
signature: overrides.signature,
|
|
13
|
+
contract: { ...emptyContract(), summary: overrides.summary ?? id },
|
|
14
|
+
sourceRefs: overrides.sourceRefsPath
|
|
15
|
+
? [{ kind: "repo", path: overrides.sourceRefsPath, symbol: overrides.name ?? id }]
|
|
16
|
+
: [],
|
|
17
|
+
generatedRefs: [],
|
|
18
|
+
traceRefs: [],
|
|
19
|
+
});
|
|
20
|
+
describe("detectGraphConflicts", () => {
|
|
21
|
+
it("finds a signature-mismatch when blueprint signature diverges from repo", async () => {
|
|
22
|
+
const graph = {
|
|
23
|
+
projectName: "Conflicts",
|
|
24
|
+
mode: "essential",
|
|
25
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
26
|
+
warnings: [],
|
|
27
|
+
workflows: [],
|
|
28
|
+
edges: [],
|
|
29
|
+
nodes: [
|
|
30
|
+
node("function:normalize", {
|
|
31
|
+
kind: "function",
|
|
32
|
+
path: "src/services/task-service.ts",
|
|
33
|
+
name: "normalizeTask",
|
|
34
|
+
summary: "Wrong summary.",
|
|
35
|
+
signature: "normalizeTask(input: string): string",
|
|
36
|
+
sourceRefsPath: "src/services/task-service.ts",
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
const report = await detectGraphConflicts(graph, fixturePath);
|
|
41
|
+
expect(report.conflicts.some((c) => c.kind === "signature-mismatch")).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it("finds missing-in-blueprint when repo has a symbol not in the blueprint", async () => {
|
|
44
|
+
const graph = {
|
|
45
|
+
projectName: "MissingInBlueprint",
|
|
46
|
+
mode: "essential",
|
|
47
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
48
|
+
warnings: [],
|
|
49
|
+
workflows: [],
|
|
50
|
+
edges: [],
|
|
51
|
+
nodes: [], // empty — all repo symbols should be reported missing
|
|
52
|
+
};
|
|
53
|
+
const report = await detectGraphConflicts(graph, fixturePath);
|
|
54
|
+
expect(report.conflicts.some((c) => c.kind === "missing-in-blueprint")).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it("returns empty conflicts for an empty graph and empty repo", async () => {
|
|
57
|
+
// Using a path that exists but has no matching symbols
|
|
58
|
+
const report = await detectGraphConflicts({
|
|
59
|
+
projectName: "Empty",
|
|
60
|
+
mode: "essential",
|
|
61
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
62
|
+
warnings: [],
|
|
63
|
+
workflows: [],
|
|
64
|
+
edges: [],
|
|
65
|
+
nodes: [],
|
|
66
|
+
}, fixturePath);
|
|
67
|
+
// sample-repo has symbols, so missing-in-blueprint will fire
|
|
68
|
+
// but there should be no signature-mismatch
|
|
69
|
+
expect(report.conflicts.every((c) => c.kind === "missing-in-blueprint")).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it("returns a valid checkedAt timestamp", async () => {
|
|
72
|
+
const report = await detectGraphConflicts({
|
|
73
|
+
projectName: "Timestamp",
|
|
74
|
+
mode: "essential",
|
|
75
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
76
|
+
warnings: [],
|
|
77
|
+
workflows: [],
|
|
78
|
+
edges: [],
|
|
79
|
+
nodes: [],
|
|
80
|
+
}, fixturePath);
|
|
81
|
+
expect(() => new Date(report.checkedAt)).not.toThrow();
|
|
82
|
+
expect(report.repoPath).toBe(path.resolve(fixturePath));
|
|
83
|
+
});
|
|
84
|
+
it("includes suggestedAction on every conflict", async () => {
|
|
85
|
+
const report = await detectGraphConflicts({
|
|
86
|
+
projectName: "Suggestions",
|
|
87
|
+
mode: "essential",
|
|
88
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
89
|
+
warnings: [],
|
|
90
|
+
workflows: [],
|
|
91
|
+
edges: [],
|
|
92
|
+
nodes: [],
|
|
93
|
+
}, fixturePath);
|
|
94
|
+
for (const conflict of report.conflicts) {
|
|
95
|
+
expect(conflict.suggestedAction.length).toBeGreaterThan(0);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
package/dist/cycles.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { BlueprintGraph } from "@abhinav2203/codeflow-core/schema";
|
|
3
|
+
export declare const cycleSchema: z.ZodObject<{
|
|
4
|
+
nodeIds: z.ZodArray<z.ZodString>;
|
|
5
|
+
edges: z.ZodArray<z.ZodObject<{
|
|
6
|
+
from: z.ZodString;
|
|
7
|
+
to: z.ZodString;
|
|
8
|
+
kind: z.ZodString;
|
|
9
|
+
}, z.core.$strip>>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type Cycle = z.infer<typeof cycleSchema>;
|
|
12
|
+
export declare const cycleReportSchema: z.ZodObject<{
|
|
13
|
+
analyzedAt: z.ZodString;
|
|
14
|
+
totalCycles: z.ZodNumber;
|
|
15
|
+
maxCycleLength: z.ZodNumber;
|
|
16
|
+
cycles: z.ZodArray<z.ZodObject<{
|
|
17
|
+
nodeIds: z.ZodArray<z.ZodString>;
|
|
18
|
+
edges: z.ZodArray<z.ZodObject<{
|
|
19
|
+
from: z.ZodString;
|
|
20
|
+
to: z.ZodString;
|
|
21
|
+
kind: z.ZodString;
|
|
22
|
+
}, z.core.$strip>>;
|
|
23
|
+
}, z.core.$strip>>;
|
|
24
|
+
affectedNodeIds: z.ZodArray<z.ZodString>;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
export type CycleReport = z.infer<typeof cycleReportSchema>;
|
|
27
|
+
/**
|
|
28
|
+
* Detect all directed cycles in a blueprint graph using Tarjan's strongly-connected
|
|
29
|
+
* components algorithm (iterative, stack-safe).
|
|
30
|
+
*
|
|
31
|
+
* Self-loop edges (from === to) are detected separately and treated as single-node cycles.
|
|
32
|
+
*/
|
|
33
|
+
export declare const detectCycles: (graph: BlueprintGraph) => CycleReport;
|
|
34
|
+
/**
|
|
35
|
+
* Returns true if the graph contains at least one directed cycle.
|
|
36
|
+
* Faster than detectCycles — stops early on the first cycle found.
|
|
37
|
+
*/
|
|
38
|
+
export declare const hasCycles: (graph: BlueprintGraph) => boolean;
|
|
39
|
+
//# sourceMappingURL=cycles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cycles.d.ts","sourceRoot":"","sources":["../src/cycles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,eAAO,MAAM,WAAW;;;;;;;iBAStB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;iBAM5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAmE5D;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,cAAc,KAAG,WA2CpD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,cAAc,KAAG,OAejD,CAAC"}
|
package/dist/cycles.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const cycleSchema = z.object({
|
|
3
|
+
nodeIds: z.array(z.string()),
|
|
4
|
+
edges: z.array(z.object({
|
|
5
|
+
from: z.string(),
|
|
6
|
+
to: z.string(),
|
|
7
|
+
kind: z.string(),
|
|
8
|
+
})),
|
|
9
|
+
});
|
|
10
|
+
export const cycleReportSchema = z.object({
|
|
11
|
+
analyzedAt: z.string(),
|
|
12
|
+
totalCycles: z.number(),
|
|
13
|
+
maxCycleLength: z.number(),
|
|
14
|
+
cycles: z.array(cycleSchema),
|
|
15
|
+
affectedNodeIds: z.array(z.string()),
|
|
16
|
+
});
|
|
17
|
+
const tarjanIterative = (nodeIds, adjacency) => {
|
|
18
|
+
const indices = new Map();
|
|
19
|
+
const lowlinks = new Map();
|
|
20
|
+
const onStack = new Set();
|
|
21
|
+
const stack = [];
|
|
22
|
+
const sccs = [];
|
|
23
|
+
let index = 0;
|
|
24
|
+
for (const root of nodeIds) {
|
|
25
|
+
if (indices.has(root))
|
|
26
|
+
continue;
|
|
27
|
+
const callStack = [{ node: root, neighborIndex: 0, neighbors: adjacency.get(root) ?? [] }];
|
|
28
|
+
indices.set(root, index);
|
|
29
|
+
lowlinks.set(root, index);
|
|
30
|
+
index++;
|
|
31
|
+
stack.push(root);
|
|
32
|
+
onStack.add(root);
|
|
33
|
+
while (callStack.length > 0) {
|
|
34
|
+
const frame = callStack[callStack.length - 1];
|
|
35
|
+
if (frame.neighborIndex < frame.neighbors.length) {
|
|
36
|
+
const neighbor = frame.neighbors[frame.neighborIndex];
|
|
37
|
+
frame.neighborIndex++;
|
|
38
|
+
if (!indices.has(neighbor)) {
|
|
39
|
+
indices.set(neighbor, index);
|
|
40
|
+
lowlinks.set(neighbor, index);
|
|
41
|
+
index++;
|
|
42
|
+
stack.push(neighbor);
|
|
43
|
+
onStack.add(neighbor);
|
|
44
|
+
callStack.push({ node: neighbor, neighborIndex: 0, neighbors: adjacency.get(neighbor) ?? [] });
|
|
45
|
+
}
|
|
46
|
+
else if (onStack.has(neighbor)) {
|
|
47
|
+
lowlinks.set(frame.node, Math.min(lowlinks.get(frame.node), lowlinks.get(neighbor)));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (lowlinks.get(frame.node) === indices.get(frame.node)) {
|
|
52
|
+
const scc = [];
|
|
53
|
+
let w;
|
|
54
|
+
do {
|
|
55
|
+
w = stack.pop();
|
|
56
|
+
onStack.delete(w);
|
|
57
|
+
scc.push(w);
|
|
58
|
+
} while (w !== frame.node);
|
|
59
|
+
sccs.push(scc);
|
|
60
|
+
}
|
|
61
|
+
callStack.pop();
|
|
62
|
+
if (callStack.length > 0) {
|
|
63
|
+
const parent = callStack[callStack.length - 1];
|
|
64
|
+
lowlinks.set(parent.node, Math.min(lowlinks.get(parent.node), lowlinks.get(frame.node)));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return sccs;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Detect all directed cycles in a blueprint graph using Tarjan's strongly-connected
|
|
73
|
+
* components algorithm (iterative, stack-safe).
|
|
74
|
+
*
|
|
75
|
+
* Self-loop edges (from === to) are detected separately and treated as single-node cycles.
|
|
76
|
+
*/
|
|
77
|
+
export const detectCycles = (graph) => {
|
|
78
|
+
const nodeIds = graph.nodes.map((n) => n.id);
|
|
79
|
+
const adjacency = new Map();
|
|
80
|
+
for (const id of nodeIds) {
|
|
81
|
+
adjacency.set(id, []);
|
|
82
|
+
}
|
|
83
|
+
for (const edge of graph.edges) {
|
|
84
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
85
|
+
}
|
|
86
|
+
const sccs = tarjanIterative(nodeIds, adjacency);
|
|
87
|
+
// A self-loop (from === to) is a genuine cycle but Tarjan's SCC returns it as
|
|
88
|
+
// a size-1 SCC. Detect them separately and treat them as single-node cycles.
|
|
89
|
+
const selfLoopNodeIds = new Set(graph.edges.filter((e) => e.from === e.to).map((e) => e.from));
|
|
90
|
+
const selfLoopSccs = [...selfLoopNodeIds].map((id) => [id]);
|
|
91
|
+
const sccSet = [
|
|
92
|
+
...sccs.filter((scc) => scc.length >= 2),
|
|
93
|
+
...selfLoopSccs
|
|
94
|
+
];
|
|
95
|
+
const cycles = sccSet.map((scc) => {
|
|
96
|
+
const memberSet = new Set(scc);
|
|
97
|
+
const edges = graph.edges
|
|
98
|
+
.filter((e) => memberSet.has(e.from) && memberSet.has(e.to))
|
|
99
|
+
.map((e) => ({ from: e.from, to: e.to, kind: e.kind }));
|
|
100
|
+
return { nodeIds: scc, edges };
|
|
101
|
+
});
|
|
102
|
+
const affectedNodeIds = [...new Set(cycles.flatMap((c) => c.nodeIds))];
|
|
103
|
+
const maxCycleLength = cycles.reduce((max, c) => Math.max(max, c.nodeIds.length), 0);
|
|
104
|
+
return {
|
|
105
|
+
analyzedAt: new Date().toISOString(),
|
|
106
|
+
totalCycles: cycles.length,
|
|
107
|
+
maxCycleLength,
|
|
108
|
+
cycles,
|
|
109
|
+
affectedNodeIds,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Returns true if the graph contains at least one directed cycle.
|
|
114
|
+
* Faster than detectCycles — stops early on the first cycle found.
|
|
115
|
+
*/
|
|
116
|
+
export const hasCycles = (graph) => {
|
|
117
|
+
if (graph.edges.some((e) => e.from === e.to))
|
|
118
|
+
return true;
|
|
119
|
+
const nodeIds = graph.nodes.map((n) => n.id);
|
|
120
|
+
const adjacency = new Map();
|
|
121
|
+
for (const id of nodeIds) {
|
|
122
|
+
adjacency.set(id, []);
|
|
123
|
+
}
|
|
124
|
+
for (const edge of graph.edges) {
|
|
125
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
126
|
+
}
|
|
127
|
+
const sccs = tarjanIterative(nodeIds, adjacency);
|
|
128
|
+
return sccs.some((scc) => scc.length >= 2);
|
|
129
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cycles.test.d.ts","sourceRoot":"","sources":["../src/cycles.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { detectCycles, hasCycles } from "./cycles";
|
|
3
|
+
import { emptyContract } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
const node = (id) => ({
|
|
5
|
+
id,
|
|
6
|
+
kind: "module",
|
|
7
|
+
name: id,
|
|
8
|
+
summary: id,
|
|
9
|
+
contract: emptyContract(),
|
|
10
|
+
sourceRefs: [],
|
|
11
|
+
generatedRefs: [],
|
|
12
|
+
traceRefs: [],
|
|
13
|
+
});
|
|
14
|
+
const edge = (from, to) => ({
|
|
15
|
+
from,
|
|
16
|
+
to,
|
|
17
|
+
kind: "calls",
|
|
18
|
+
required: true,
|
|
19
|
+
confidence: 1,
|
|
20
|
+
});
|
|
21
|
+
const graph = (projectName, nodes, edges) => ({
|
|
22
|
+
projectName,
|
|
23
|
+
mode: "essential",
|
|
24
|
+
generatedAt: "2026-03-14T00:00:00.000Z",
|
|
25
|
+
warnings: [],
|
|
26
|
+
workflows: [],
|
|
27
|
+
nodes,
|
|
28
|
+
edges,
|
|
29
|
+
});
|
|
30
|
+
describe("detectCycles", () => {
|
|
31
|
+
it("returns no cycles for a DAG", () => {
|
|
32
|
+
const report = detectCycles(graph("DAG", [node("A"), node("B"), node("C")], [edge("A", "B"), edge("B", "C")]));
|
|
33
|
+
expect(report.totalCycles).toBe(0);
|
|
34
|
+
expect(report.affectedNodeIds).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
it("detects a simple two-node cycle", () => {
|
|
37
|
+
const report = detectCycles(graph("TwoNodeCycle", [node("A"), node("B")], [edge("A", "B"), edge("B", "A")]));
|
|
38
|
+
expect(report.totalCycles).toBe(1);
|
|
39
|
+
expect(report.affectedNodeIds).toContain("A");
|
|
40
|
+
expect(report.affectedNodeIds).toContain("B");
|
|
41
|
+
});
|
|
42
|
+
it("detects multiple independent cycles", () => {
|
|
43
|
+
const report = detectCycles(graph("MultiCycle", [node("A"), node("B"), node("C"), node("D")], [edge("A", "B"), edge("B", "A"), edge("C", "D"), edge("D", "C")]));
|
|
44
|
+
expect(report.totalCycles).toBe(2);
|
|
45
|
+
});
|
|
46
|
+
it("handles empty graph", () => {
|
|
47
|
+
const report = detectCycles(graph("Empty", [], []));
|
|
48
|
+
expect(report.totalCycles).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
it("detects a self-loop edge as a cycle", () => {
|
|
51
|
+
const report = detectCycles(graph("SelfLoop", [node("A")], [{ from: "A", to: "A", kind: "calls", required: true, confidence: 1 }]));
|
|
52
|
+
expect(report.totalCycles).toBe(1);
|
|
53
|
+
expect(report.affectedNodeIds).toContain("A");
|
|
54
|
+
});
|
|
55
|
+
it("returns maxCycleLength correctly", () => {
|
|
56
|
+
const report = detectCycles(graph("ThreeCycle", [node("A"), node("B"), node("C")], [edge("A", "B"), edge("B", "C"), edge("C", "A")]));
|
|
57
|
+
expect(report.totalCycles).toBe(1);
|
|
58
|
+
expect(report.maxCycleLength).toBe(3);
|
|
59
|
+
});
|
|
60
|
+
it("cycles array contains edges belonging to the SCC", () => {
|
|
61
|
+
const report = detectCycles(graph("EdgeCycle", [node("X"), node("Y")], [edge("X", "Y"), edge("Y", "X")]));
|
|
62
|
+
const cycle = report.cycles[0];
|
|
63
|
+
expect(cycle.nodeIds).toContain("X");
|
|
64
|
+
expect(cycle.nodeIds).toContain("Y");
|
|
65
|
+
expect(cycle.edges).toHaveLength(2);
|
|
66
|
+
expect(cycle.edges.map((e) => `${e.from}→${e.to}`)).toEqual(expect.arrayContaining(["X→Y", "Y→X"]));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("hasCycles", () => {
|
|
70
|
+
it("returns false for a DAG", () => {
|
|
71
|
+
expect(hasCycles(graph("DAG", [node("A"), node("B")], [edge("A", "B")]))).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it("returns true when a two-node cycle exists", () => {
|
|
74
|
+
expect(hasCycles(graph("Cyclic", [node("A"), node("B")], [edge("A", "B"), edge("B", "A")]))).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it("returns true for a self-loop", () => {
|
|
77
|
+
expect(hasCycles(graph("SelfLoop", [node("A")], [{ from: "A", to: "A", kind: "calls", required: true, confidence: 1 }]))).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
it("returns false for empty graph", () => {
|
|
80
|
+
expect(hasCycles(graph("Empty", [], []))).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* POST /api/conflicts
|
|
4
|
+
*
|
|
5
|
+
* Body: { graph: BlueprintGraph, repoPath: string }
|
|
6
|
+
*
|
|
7
|
+
* Compares a blueprint graph against a live TypeScript repository,
|
|
8
|
+
* detecting signature mismatches, summary mismatches, missing-in-repo
|
|
9
|
+
* nodes, and missing-in-blueprint symbols.
|
|
10
|
+
*/
|
|
11
|
+
export declare function POST(request: Request): Promise<NextResponse<{
|
|
12
|
+
report: {
|
|
13
|
+
checkedAt: string;
|
|
14
|
+
repoPath: string;
|
|
15
|
+
conflicts: {
|
|
16
|
+
kind: "missing-in-repo" | "missing-in-blueprint" | "signature-mismatch" | "summary-mismatch";
|
|
17
|
+
message: string;
|
|
18
|
+
suggestedAction: string;
|
|
19
|
+
nodeId?: string | undefined;
|
|
20
|
+
path?: string | undefined;
|
|
21
|
+
blueprintValue?: string | undefined;
|
|
22
|
+
repoValue?: string | undefined;
|
|
23
|
+
}[];
|
|
24
|
+
};
|
|
25
|
+
}> | NextResponse<{
|
|
26
|
+
error: string;
|
|
27
|
+
}>>;
|
|
28
|
+
//# sourceMappingURL=conflicts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflicts.d.ts","sourceRoot":"","sources":["../../src/handlers/conflicts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C;;;;;;;;GAQG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO;;;;;;;;;;;;;;;;IAc1C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { detectGraphConflicts } from "../conflicts";
|
|
3
|
+
import { conflictCheckRequestSchema } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/conflicts
|
|
6
|
+
*
|
|
7
|
+
* Body: { graph: BlueprintGraph, repoPath: string }
|
|
8
|
+
*
|
|
9
|
+
* Compares a blueprint graph against a live TypeScript repository,
|
|
10
|
+
* detecting signature mismatches, summary mismatches, missing-in-repo
|
|
11
|
+
* nodes, and missing-in-blueprint symbols.
|
|
12
|
+
*/
|
|
13
|
+
export async function POST(request) {
|
|
14
|
+
try {
|
|
15
|
+
const payload = conflictCheckRequestSchema.parse(await request.json());
|
|
16
|
+
const report = await detectGraphConflicts(payload.graph, payload.repoPath);
|
|
17
|
+
return NextResponse.json({ report });
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return NextResponse.json({
|
|
21
|
+
error: error instanceof Error ? error.message : "Failed to analyze graph conflicts.",
|
|
22
|
+
}, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* POST /api/analysis/cycles
|
|
4
|
+
*
|
|
5
|
+
* Body: {@link BlueprintGraph}
|
|
6
|
+
*
|
|
7
|
+
* Returns a cycle detection report for the submitted blueprint graph.
|
|
8
|
+
* Includes total cycle count, affected node IDs, per-cycle edge details,
|
|
9
|
+
* and a convenience `hasCycles` boolean.
|
|
10
|
+
*/
|
|
11
|
+
export declare function POST(request: Request): Promise<NextResponse<{
|
|
12
|
+
report: {
|
|
13
|
+
hasCycles: boolean;
|
|
14
|
+
analyzedAt: string;
|
|
15
|
+
totalCycles: number;
|
|
16
|
+
maxCycleLength: number;
|
|
17
|
+
cycles: {
|
|
18
|
+
nodeIds: string[];
|
|
19
|
+
edges: {
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
}[];
|
|
24
|
+
}[];
|
|
25
|
+
affectedNodeIds: string[];
|
|
26
|
+
};
|
|
27
|
+
}> | NextResponse<{
|
|
28
|
+
error: string;
|
|
29
|
+
}>>;
|
|
30
|
+
//# sourceMappingURL=cycles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cycles.d.ts","sourceRoot":"","sources":["../../src/handlers/cycles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C;;;;;;;;GAQG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO;;;;;;;;;;;;;;;;;;IAc1C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { detectCycles, hasCycles } from "../cycles";
|
|
3
|
+
import { blueprintGraphSchema } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/analysis/cycles
|
|
6
|
+
*
|
|
7
|
+
* Body: {@link BlueprintGraph}
|
|
8
|
+
*
|
|
9
|
+
* Returns a cycle detection report for the submitted blueprint graph.
|
|
10
|
+
* Includes total cycle count, affected node IDs, per-cycle edge details,
|
|
11
|
+
* and a convenience `hasCycles` boolean.
|
|
12
|
+
*/
|
|
13
|
+
export async function POST(request) {
|
|
14
|
+
try {
|
|
15
|
+
const payload = blueprintGraphSchema.parse(await request.json());
|
|
16
|
+
const report = detectCycles(payload);
|
|
17
|
+
return NextResponse.json({ report: { ...report, hasCycles: hasCycles(payload) } });
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return NextResponse.json({
|
|
21
|
+
error: error instanceof Error ? error.message : "Failed to detect dependency cycles.",
|
|
22
|
+
}, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* POST /api/analysis/metrics
|
|
4
|
+
*
|
|
5
|
+
* Body: {@link BlueprintGraph}
|
|
6
|
+
*
|
|
7
|
+
* Returns structural graph metrics: node/edge counts, degree statistics,
|
|
8
|
+
* density, connected components, and contract-level averages.
|
|
9
|
+
*/
|
|
10
|
+
export declare function POST(request: Request): Promise<NextResponse<{
|
|
11
|
+
metrics: {
|
|
12
|
+
analyzedAt: string;
|
|
13
|
+
nodeCount: number;
|
|
14
|
+
edgeCount: number;
|
|
15
|
+
nodesByKind: Record<string, number>;
|
|
16
|
+
edgesByKind: Record<string, number>;
|
|
17
|
+
nodesByStatus: Record<string, number>;
|
|
18
|
+
density: number;
|
|
19
|
+
avgDegree: number;
|
|
20
|
+
maxInDegree: number;
|
|
21
|
+
maxOutDegree: number;
|
|
22
|
+
avgMethodsPerNode: number;
|
|
23
|
+
avgResponsibilitiesPerNode: number;
|
|
24
|
+
totalMethods: number;
|
|
25
|
+
totalResponsibilities: number;
|
|
26
|
+
connectedComponents: number;
|
|
27
|
+
isolatedNodes: number;
|
|
28
|
+
leafNodes: number;
|
|
29
|
+
maxInDegreeNodeId?: string | undefined;
|
|
30
|
+
maxOutDegreeNodeId?: string | undefined;
|
|
31
|
+
};
|
|
32
|
+
}> | NextResponse<{
|
|
33
|
+
error: string;
|
|
34
|
+
}>>;
|
|
35
|
+
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/handlers/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C;;;;;;;GAOG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;IAc1C"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { computeGraphMetrics } from "../metrics";
|
|
3
|
+
import { blueprintGraphSchema } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/analysis/metrics
|
|
6
|
+
*
|
|
7
|
+
* Body: {@link BlueprintGraph}
|
|
8
|
+
*
|
|
9
|
+
* Returns structural graph metrics: node/edge counts, degree statistics,
|
|
10
|
+
* density, connected components, and contract-level averages.
|
|
11
|
+
*/
|
|
12
|
+
export async function POST(request) {
|
|
13
|
+
try {
|
|
14
|
+
const payload = blueprintGraphSchema.parse(await request.json());
|
|
15
|
+
const metrics = computeGraphMetrics(payload);
|
|
16
|
+
return NextResponse.json({ metrics });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return NextResponse.json({
|
|
20
|
+
error: error instanceof Error ? error.message : "Failed to compute graph metrics.",
|
|
21
|
+
}, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* POST /api/refactor/detect
|
|
4
|
+
*
|
|
5
|
+
* Body: {@link BlueprintGraph}
|
|
6
|
+
*
|
|
7
|
+
* Returns a {@link RefactorReport} describing all detected drift issues:
|
|
8
|
+
* broken edges, missing edges, and signature drift.
|
|
9
|
+
*/
|
|
10
|
+
export declare function POST(request: Request): Promise<NextResponse<{
|
|
11
|
+
report: import("..").RefactorReport;
|
|
12
|
+
}> | NextResponse<{
|
|
13
|
+
error: string;
|
|
14
|
+
}>>;
|
|
15
|
+
//# sourceMappingURL=refactor-detect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refactor-detect.d.ts","sourceRoot":"","sources":["../../src/handlers/refactor-detect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C;;;;;;;GAOG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO;;;;IAc1C"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { detectDrift } from "../refactor";
|
|
3
|
+
import { blueprintGraphSchema } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/refactor/detect
|
|
6
|
+
*
|
|
7
|
+
* Body: {@link BlueprintGraph}
|
|
8
|
+
*
|
|
9
|
+
* Returns a {@link RefactorReport} describing all detected drift issues:
|
|
10
|
+
* broken edges, missing edges, and signature drift.
|
|
11
|
+
*/
|
|
12
|
+
export async function POST(request) {
|
|
13
|
+
try {
|
|
14
|
+
const graph = blueprintGraphSchema.parse(await request.json());
|
|
15
|
+
const report = detectDrift(graph);
|
|
16
|
+
return NextResponse.json({ report });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return NextResponse.json({
|
|
20
|
+
error: error instanceof Error ? error.message : "Failed to detect architectural drift.",
|
|
21
|
+
}, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* POST /api/refactor/heal
|
|
4
|
+
*
|
|
5
|
+
* Body: {@link BlueprintGraph}
|
|
6
|
+
*
|
|
7
|
+
* Detects all drift issues, then auto-heals the graph:
|
|
8
|
+
* removes broken edges, synthesises missing edges from contract calls,
|
|
9
|
+
* and syncs node signatures to match their first contract method.
|
|
10
|
+
*
|
|
11
|
+
* Returns both the detection report and the healed graph.
|
|
12
|
+
*/
|
|
13
|
+
export declare function POST(request: Request): Promise<NextResponse<{
|
|
14
|
+
report: import("..").RefactorReport;
|
|
15
|
+
result: import("..").HealResult;
|
|
16
|
+
}> | NextResponse<{
|
|
17
|
+
error: string;
|
|
18
|
+
}>>;
|
|
19
|
+
//# sourceMappingURL=refactor-heal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refactor-heal.d.ts","sourceRoot":"","sources":["../../src/handlers/refactor-heal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C;;;;;;;;;;GAUG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO;;;;;IAe1C"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { detectDrift, healGraph } from "../refactor";
|
|
3
|
+
import { blueprintGraphSchema } from "@abhinav2203/codeflow-core/schema";
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/refactor/heal
|
|
6
|
+
*
|
|
7
|
+
* Body: {@link BlueprintGraph}
|
|
8
|
+
*
|
|
9
|
+
* Detects all drift issues, then auto-heals the graph:
|
|
10
|
+
* removes broken edges, synthesises missing edges from contract calls,
|
|
11
|
+
* and syncs node signatures to match their first contract method.
|
|
12
|
+
*
|
|
13
|
+
* Returns both the detection report and the healed graph.
|
|
14
|
+
*/
|
|
15
|
+
export async function POST(request) {
|
|
16
|
+
try {
|
|
17
|
+
const graph = blueprintGraphSchema.parse(await request.json());
|
|
18
|
+
const report = detectDrift(graph);
|
|
19
|
+
const result = healGraph(graph, report);
|
|
20
|
+
return NextResponse.json({ report, result });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return NextResponse.json({
|
|
24
|
+
error: error instanceof Error ? error.message : "Failed to heal architectural drift.",
|
|
25
|
+
}, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* POST /api/analysis/smells
|
|
4
|
+
*
|
|
5
|
+
* Body: {@link BlueprintGraph}
|
|
6
|
+
*
|
|
7
|
+
* Returns an architecture smell report including god-node, hub-node,
|
|
8
|
+
* orphan-node, tight-coupling, unstable-dependency, and scattered-responsibility
|
|
9
|
+
* detections along with an overall health score.
|
|
10
|
+
*/
|
|
11
|
+
export declare function POST(request: Request): Promise<NextResponse<{
|
|
12
|
+
report: {
|
|
13
|
+
analyzedAt: string;
|
|
14
|
+
totalSmells: number;
|
|
15
|
+
smells: {
|
|
16
|
+
code: string;
|
|
17
|
+
severity: "warning" | "info" | "critical";
|
|
18
|
+
message: string;
|
|
19
|
+
suggestion: string;
|
|
20
|
+
nodeId?: string | undefined;
|
|
21
|
+
}[];
|
|
22
|
+
healthScore: number;
|
|
23
|
+
};
|
|
24
|
+
}> | NextResponse<{
|
|
25
|
+
error: string;
|
|
26
|
+
}>>;
|
|
27
|
+
//# sourceMappingURL=smells.d.ts.map
|