@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.
Files changed (98) hide show
  1. package/dist/app/api/analysis/cycles/route.test.d.ts +2 -0
  2. package/dist/app/api/analysis/cycles/route.test.d.ts.map +1 -0
  3. package/dist/app/api/analysis/cycles/route.test.js +92 -0
  4. package/dist/app/api/analysis/metrics/route.test.d.ts +2 -0
  5. package/dist/app/api/analysis/metrics/route.test.d.ts.map +1 -0
  6. package/dist/app/api/analysis/metrics/route.test.js +82 -0
  7. package/dist/app/api/analysis/smells/route.test.d.ts +2 -0
  8. package/dist/app/api/analysis/smells/route.test.d.ts.map +1 -0
  9. package/dist/app/api/analysis/smells/route.test.js +102 -0
  10. package/dist/app/api/conflicts/route.test.d.ts +2 -0
  11. package/dist/app/api/conflicts/route.test.d.ts.map +1 -0
  12. package/dist/app/api/conflicts/route.test.js +58 -0
  13. package/dist/app/api/refactor/detect/route.test.d.ts +2 -0
  14. package/dist/app/api/refactor/detect/route.test.d.ts.map +1 -0
  15. package/dist/app/api/refactor/detect/route.test.js +61 -0
  16. package/dist/app/api/refactor/heal/route.test.d.ts +2 -0
  17. package/dist/app/api/refactor/heal/route.test.d.ts.map +1 -0
  18. package/dist/app/api/refactor/heal/route.test.js +62 -0
  19. package/dist/bin/cli.js +5 -41
  20. package/dist/conflicts.d.ts +12 -0
  21. package/dist/conflicts.d.ts.map +1 -0
  22. package/dist/conflicts.js +77 -0
  23. package/dist/conflicts.test.d.ts +2 -0
  24. package/dist/conflicts.test.d.ts.map +1 -0
  25. package/dist/conflicts.test.js +98 -0
  26. package/dist/cycles.d.ts +39 -0
  27. package/dist/cycles.d.ts.map +1 -0
  28. package/dist/cycles.js +129 -0
  29. package/dist/cycles.test.d.ts +2 -0
  30. package/dist/cycles.test.d.ts.map +1 -0
  31. package/dist/cycles.test.js +82 -0
  32. package/dist/handlers/conflicts.d.ts +28 -0
  33. package/dist/handlers/conflicts.d.ts.map +1 -0
  34. package/dist/handlers/conflicts.js +24 -0
  35. package/dist/handlers/cycles.d.ts +30 -0
  36. package/dist/handlers/cycles.d.ts.map +1 -0
  37. package/dist/handlers/cycles.js +24 -0
  38. package/dist/handlers/metrics.d.ts +35 -0
  39. package/dist/handlers/metrics.d.ts.map +1 -0
  40. package/dist/handlers/metrics.js +23 -0
  41. package/dist/handlers/refactor-detect.d.ts +15 -0
  42. package/dist/handlers/refactor-detect.d.ts.map +1 -0
  43. package/dist/handlers/refactor-detect.js +23 -0
  44. package/dist/handlers/refactor-heal.d.ts +19 -0
  45. package/dist/handlers/refactor-heal.d.ts.map +1 -0
  46. package/dist/handlers/refactor-heal.js +27 -0
  47. package/dist/handlers/smells.d.ts +27 -0
  48. package/dist/handlers/smells.d.ts.map +1 -0
  49. package/dist/handlers/smells.js +24 -0
  50. package/dist/index.d.ts +10 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +10 -0
  53. package/dist/invoke.d.ts +3 -0
  54. package/dist/invoke.d.ts.map +1 -0
  55. package/dist/invoke.js +162 -0
  56. package/dist/metrics.d.ts +33 -0
  57. package/dist/metrics.d.ts.map +1 -0
  58. package/dist/metrics.js +159 -0
  59. package/dist/metrics.test.d.ts +2 -0
  60. package/dist/metrics.test.d.ts.map +1 -0
  61. package/dist/metrics.test.js +98 -0
  62. package/dist/refactor.d.ts +84 -0
  63. package/dist/refactor.d.ts.map +1 -0
  64. package/dist/refactor.js +183 -0
  65. package/dist/refactor.test.d.ts +2 -0
  66. package/dist/refactor.test.d.ts.map +1 -0
  67. package/dist/refactor.test.js +269 -0
  68. package/dist/smells.d.ts +39 -0
  69. package/dist/smells.d.ts.map +1 -0
  70. package/dist/smells.js +186 -0
  71. package/dist/smells.test.d.ts +2 -0
  72. package/dist/smells.test.d.ts.map +1 -0
  73. package/dist/smells.test.js +120 -0
  74. package/package.json +3 -3
  75. package/src/app/api/analysis/cycles/route.test.ts +2 -2
  76. package/src/app/api/analysis/metrics/route.test.ts +2 -2
  77. package/src/app/api/analysis/smells/route.test.ts +7 -7
  78. package/src/app/api/conflicts/route.test.ts +2 -2
  79. package/src/app/api/refactor/detect/route.test.ts +2 -2
  80. package/src/app/api/refactor/heal/route.test.ts +2 -2
  81. package/src/conflicts.test.ts +2 -2
  82. package/src/conflicts.ts +2 -2
  83. package/src/cycles.test.ts +2 -2
  84. package/src/cycles.ts +1 -1
  85. package/src/handlers/conflicts.ts +1 -1
  86. package/src/handlers/cycles.ts +1 -1
  87. package/src/handlers/metrics.ts +1 -1
  88. package/src/handlers/refactor-detect.ts +1 -1
  89. package/src/handlers/refactor-heal.ts +1 -1
  90. package/src/handlers/smells.ts +1 -1
  91. package/src/invoke.ts +116 -113
  92. package/src/metrics.test.ts +3 -3
  93. package/src/metrics.ts +1 -1
  94. package/src/refactor.test.ts +4 -4
  95. package/src/refactor.ts +1 -1
  96. package/src/smells.test.ts +7 -7
  97. package/src/smells.ts +1 -1
  98. package/vitest.config.ts +0 -8
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smells.d.ts","sourceRoot":"","sources":["../../src/handlers/smells.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 { detectSmells } from "../smells";
3
+ import { blueprintGraphSchema } from "@abhinav2203/codeflow-core/schema";
4
+ /**
5
+ * POST /api/analysis/smells
6
+ *
7
+ * Body: {@link BlueprintGraph}
8
+ *
9
+ * Returns an architecture smell report including god-node, hub-node,
10
+ * orphan-node, tight-coupling, unstable-dependency, and scattered-responsibility
11
+ * detections along with an overall health score.
12
+ */
13
+ export async function POST(request) {
14
+ try {
15
+ const payload = blueprintGraphSchema.parse(await request.json());
16
+ const report = detectSmells(payload);
17
+ return NextResponse.json({ report });
18
+ }
19
+ catch (error) {
20
+ return NextResponse.json({
21
+ error: error instanceof Error ? error.message : "Failed to detect architecture smells.",
22
+ }, { status: 400 });
23
+ }
24
+ }
@@ -0,0 +1,10 @@
1
+ export { detectCycles, hasCycles } from "./cycles.js";
2
+ export type { Cycle, CycleReport } from "./cycles.js";
3
+ export { detectSmells } from "./smells.js";
4
+ export type { Smell, SmellReport } from "./smells.js";
5
+ export { computeGraphMetrics } from "./metrics.js";
6
+ export type { GraphMetrics } from "./metrics.js";
7
+ export { detectDrift, healGraph } from "./refactor.js";
8
+ export type { DriftIssue, DriftKind, HealResult, RefactorReport } from "./refactor.js";
9
+ export { detectGraphConflicts } from "./conflicts.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACtD,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAGtD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAGtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGjD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACvD,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGvF,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // cycles
2
+ export { detectCycles, hasCycles } from "./cycles.js";
3
+ // smells
4
+ export { detectSmells } from "./smells.js";
5
+ // metrics
6
+ export { computeGraphMetrics } from "./metrics.js";
7
+ // refactor
8
+ export { detectDrift, healGraph } from "./refactor.js";
9
+ // conflicts
10
+ export { detectGraphConflicts } from "./conflicts.js";
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export declare const runCLI: () => Promise<void>;
3
+ //# sourceMappingURL=invoke.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../src/invoke.ts"],"names":[],"mappings":";AAcA,eAAO,MAAM,MAAM,qBAiKlB,CAAC"}
package/dist/invoke.js ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { detectCycles, hasCycles } from "./cycles.js";
5
+ import { detectSmells } from "./smells.js";
6
+ import { computeGraphMetrics } from "./metrics.js";
7
+ import { detectDrift, healGraph } from "./refactor.js";
8
+ import { detectGraphConflicts } from "./conflicts.js";
9
+ import { blueprintGraphSchema } from "@abhinav2203/codeflow-core/schema";
10
+ // ── CLI ─────────────────────────────────────────────────────────────────────
11
+ export const runCLI = async () => {
12
+ const [command, ...args] = process.argv.slice(2);
13
+ const readBlueprint = (arg) => {
14
+ const filePath = resolve(arg);
15
+ return readFileSync(filePath, "utf-8");
16
+ };
17
+ const parseBlueprint = (content) => blueprintGraphSchema.parse(JSON.parse(content));
18
+ const printJson = (data) => {
19
+ console.log(JSON.stringify(data, null, 2));
20
+ };
21
+ const exit = (code, message) => {
22
+ if (message)
23
+ console.error(message);
24
+ process.exit(code);
25
+ };
26
+ const MISSING_ARG = (cmd) => `codeflow-analysis ${cmd}: missing required argument <blueprint-path>`;
27
+ const UNREADABLE = (path) => `codeflow-analysis: could not read file "${path}"`;
28
+ const INVALID_BLUEPRINT = (path, error) => `codeflow-analysis: invalid blueprint at "${path}": ${error instanceof Error ? error.message : error}`;
29
+ try {
30
+ switch (command) {
31
+ // ── cycles ────────────────────────────────────────────────────────────────
32
+ case "cycles": {
33
+ const [blueprintPath] = args;
34
+ if (!blueprintPath)
35
+ exit(1, MISSING_ARG("cycles"));
36
+ let graph;
37
+ try {
38
+ graph = parseBlueprint(readBlueprint(blueprintPath));
39
+ }
40
+ catch (e) {
41
+ if (e.code === "ENOENT")
42
+ exit(1, UNREADABLE(blueprintPath));
43
+ exit(1, INVALID_BLUEPRINT(blueprintPath, e));
44
+ }
45
+ const report = detectCycles(graph);
46
+ printJson({ report, hasCycles: hasCycles(graph) });
47
+ break;
48
+ }
49
+ // ── smells ───────────────────────────────────────────────────────────────
50
+ case "smells": {
51
+ const [blueprintPath] = args;
52
+ if (!blueprintPath)
53
+ exit(1, MISSING_ARG("smells"));
54
+ let graph;
55
+ try {
56
+ graph = parseBlueprint(readBlueprint(blueprintPath));
57
+ }
58
+ catch (e) {
59
+ if (e.code === "ENOENT")
60
+ exit(1, UNREADABLE(blueprintPath));
61
+ exit(1, INVALID_BLUEPRINT(blueprintPath, e));
62
+ }
63
+ const report = detectSmells(graph);
64
+ printJson({ report });
65
+ break;
66
+ }
67
+ // ── metrics ─────────────────────────────────────────────────────────────
68
+ case "metrics": {
69
+ const [blueprintPath] = args;
70
+ if (!blueprintPath)
71
+ exit(1, MISSING_ARG("metrics"));
72
+ let graph;
73
+ try {
74
+ graph = parseBlueprint(readBlueprint(blueprintPath));
75
+ }
76
+ catch (e) {
77
+ if (e.code === "ENOENT")
78
+ exit(1, UNREADABLE(blueprintPath));
79
+ exit(1, INVALID_BLUEPRINT(blueprintPath, e));
80
+ }
81
+ const metrics = computeGraphMetrics(graph);
82
+ printJson({ metrics });
83
+ break;
84
+ }
85
+ // ── refactor detect ──────────────────────────────────────────────────────
86
+ case "refactor": {
87
+ const sub = args[0];
88
+ const [blueprintPath] = args.slice(1);
89
+ if (sub === "detect") {
90
+ if (!blueprintPath)
91
+ exit(1, MISSING_ARG("refactor detect"));
92
+ let graph;
93
+ try {
94
+ graph = parseBlueprint(readBlueprint(blueprintPath));
95
+ }
96
+ catch (e) {
97
+ if (e.code === "ENOENT")
98
+ exit(1, UNREADABLE(blueprintPath));
99
+ exit(1, INVALID_BLUEPRINT(blueprintPath, e));
100
+ }
101
+ const report = detectDrift(graph);
102
+ printJson({ report });
103
+ break;
104
+ }
105
+ if (sub === "heal") {
106
+ if (!blueprintPath)
107
+ exit(1, MISSING_ARG("refactor heal"));
108
+ let graph;
109
+ try {
110
+ graph = parseBlueprint(readBlueprint(blueprintPath));
111
+ }
112
+ catch (e) {
113
+ if (e.code === "ENOENT")
114
+ exit(1, UNREADABLE(blueprintPath));
115
+ exit(1, INVALID_BLUEPRINT(blueprintPath, e));
116
+ }
117
+ const report = detectDrift(graph);
118
+ const result = healGraph(graph, report);
119
+ printJson({ report, result });
120
+ break;
121
+ }
122
+ exit(1, `codeflow-analysis refactor: unknown subcommand "${sub}". Use "detect" or "heal".`);
123
+ break;
124
+ }
125
+ // ── conflicts ────────────────────────────────────────────────────────────
126
+ case "conflicts": {
127
+ const [blueprintPath, repoPath] = args;
128
+ if (!blueprintPath)
129
+ exit(1, MISSING_ARG("conflicts"));
130
+ let graph;
131
+ try {
132
+ graph = parseBlueprint(readBlueprint(blueprintPath));
133
+ }
134
+ catch (e) {
135
+ if (e.code === "ENOENT")
136
+ exit(1, UNREADABLE(blueprintPath));
137
+ exit(1, INVALID_BLUEPRINT(blueprintPath, e));
138
+ }
139
+ const resolvedRepoPath = repoPath ?? process.cwd();
140
+ const report = await detectGraphConflicts(graph, resolvedRepoPath);
141
+ printJson({ report });
142
+ break;
143
+ }
144
+ case undefined:
145
+ exit(1, `codeflow-analysis: missing command. Usage:
146
+
147
+ codeflow-analysis cycles <blueprint-path>
148
+ codeflow-analysis smells <blueprint-path>
149
+ codeflow-analysis metrics <blueprint-path>
150
+ codeflow-analysis refactor detect <blueprint-path>
151
+ codeflow-analysis refactor heal <blueprint-path>
152
+ codeflow-analysis conflicts <blueprint-path> [repo-path]`);
153
+ default:
154
+ exit(1, `codeflow-analysis: unknown command "${command}". Use cycles, smells, metrics, refactor, or conflicts.`);
155
+ }
156
+ }
157
+ catch (error) {
158
+ exit(1, `codeflow-analysis: unexpected error: ${error instanceof Error ? error.message : error}`);
159
+ }
160
+ };
161
+ // Run when executed directly
162
+ runCLI();
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import type { BlueprintGraph } from "@abhinav2203/codeflow-core/schema";
3
+ export declare const graphMetricsSchema: z.ZodObject<{
4
+ analyzedAt: z.ZodString;
5
+ nodeCount: z.ZodNumber;
6
+ edgeCount: z.ZodNumber;
7
+ nodesByKind: z.ZodRecord<z.ZodString, z.ZodNumber>;
8
+ edgesByKind: z.ZodRecord<z.ZodString, z.ZodNumber>;
9
+ nodesByStatus: z.ZodRecord<z.ZodString, z.ZodNumber>;
10
+ density: z.ZodNumber;
11
+ avgDegree: z.ZodNumber;
12
+ maxInDegree: z.ZodNumber;
13
+ maxOutDegree: z.ZodNumber;
14
+ maxInDegreeNodeId: z.ZodOptional<z.ZodString>;
15
+ maxOutDegreeNodeId: z.ZodOptional<z.ZodString>;
16
+ avgMethodsPerNode: z.ZodNumber;
17
+ avgResponsibilitiesPerNode: z.ZodNumber;
18
+ totalMethods: z.ZodNumber;
19
+ totalResponsibilities: z.ZodNumber;
20
+ connectedComponents: z.ZodNumber;
21
+ isolatedNodes: z.ZodNumber;
22
+ leafNodes: z.ZodNumber;
23
+ }, z.core.$strip>;
24
+ export type GraphMetrics = z.infer<typeof graphMetricsSchema>;
25
+ /**
26
+ * Compute structural metrics for a blueprint graph.
27
+ *
28
+ * Metrics include: node/edge counts, degree statistics, graph density,
29
+ * connected components, isolated/leaf node counts, and contract-level
30
+ * averages (methods and responsibilities per node).
31
+ */
32
+ export declare const computeGraphMetrics: (graph: BlueprintGraph) => GraphMetrics;
33
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;iBAoB7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAgE9D;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,cAAc,KAAG,YAuF3D,CAAC"}
@@ -0,0 +1,159 @@
1
+ import { z } from "zod";
2
+ export const graphMetricsSchema = z.object({
3
+ analyzedAt: z.string(),
4
+ nodeCount: z.number(),
5
+ edgeCount: z.number(),
6
+ nodesByKind: z.record(z.string(), z.number()),
7
+ edgesByKind: z.record(z.string(), z.number()),
8
+ nodesByStatus: z.record(z.string(), z.number()),
9
+ density: z.number(),
10
+ avgDegree: z.number(),
11
+ maxInDegree: z.number(),
12
+ maxOutDegree: z.number(),
13
+ maxInDegreeNodeId: z.string().optional(),
14
+ maxOutDegreeNodeId: z.string().optional(),
15
+ avgMethodsPerNode: z.number(),
16
+ avgResponsibilitiesPerNode: z.number(),
17
+ totalMethods: z.number(),
18
+ totalResponsibilities: z.number(),
19
+ connectedComponents: z.number(),
20
+ isolatedNodes: z.number(),
21
+ leafNodes: z.number(),
22
+ });
23
+ const countBy = (items, key) => {
24
+ const counts = {};
25
+ for (const item of items) {
26
+ const k = key(item);
27
+ counts[k] = (counts[k] ?? 0) + 1;
28
+ }
29
+ return counts;
30
+ };
31
+ /** Union-Find (disjoint set) implementation for connected components. */
32
+ const computeConnectedComponents = (nodeIds, edges) => {
33
+ const parent = new Map();
34
+ const rank = new Map();
35
+ for (const id of nodeIds) {
36
+ parent.set(id, id);
37
+ rank.set(id, 0);
38
+ }
39
+ const find = (x) => {
40
+ let root = x;
41
+ while (parent.get(root) !== root) {
42
+ root = parent.get(root);
43
+ }
44
+ let current = x;
45
+ while (current !== root) {
46
+ const next = parent.get(current);
47
+ parent.set(current, root);
48
+ current = next;
49
+ }
50
+ return root;
51
+ };
52
+ const union = (a, b) => {
53
+ const ra = find(a);
54
+ const rb = find(b);
55
+ if (ra === rb)
56
+ return;
57
+ const rankA = rank.get(ra);
58
+ const rankB = rank.get(rb);
59
+ if (rankA < rankB) {
60
+ parent.set(ra, rb);
61
+ }
62
+ else if (rankA > rankB) {
63
+ parent.set(rb, ra);
64
+ }
65
+ else {
66
+ parent.set(rb, ra);
67
+ rank.set(ra, rankA + 1);
68
+ }
69
+ };
70
+ for (const edge of edges) {
71
+ if (parent.has(edge.from) && parent.has(edge.to)) {
72
+ union(edge.from, edge.to);
73
+ }
74
+ }
75
+ const roots = new Set(nodeIds.map(find));
76
+ return roots.size;
77
+ };
78
+ /**
79
+ * Compute structural metrics for a blueprint graph.
80
+ *
81
+ * Metrics include: node/edge counts, degree statistics, graph density,
82
+ * connected components, isolated/leaf node counts, and contract-level
83
+ * averages (methods and responsibilities per node).
84
+ */
85
+ export const computeGraphMetrics = (graph) => {
86
+ const { nodes, edges } = graph;
87
+ const nodeCount = nodes.length;
88
+ const edgeCount = edges.length;
89
+ const nodesByKind = countBy(nodes, (n) => n.kind);
90
+ const edgesByKind = countBy(edges, (e) => e.kind);
91
+ const nodesByStatus = countBy(nodes, (n) => n.status ?? "spec_only");
92
+ // Use unique (from,to) directed pairs for density to avoid inflated values
93
+ // from parallel edges between the same node pair.
94
+ const uniquePairCount = new Set(edges.map((e) => `${e.from}::__::${e.to}`)).size;
95
+ const density = nodeCount < 2 ? 0 : uniquePairCount / (nodeCount * (nodeCount - 1));
96
+ const inDegree = new Map();
97
+ const outDegree = new Map();
98
+ for (const node of nodes) {
99
+ inDegree.set(node.id, 0);
100
+ outDegree.set(node.id, 0);
101
+ }
102
+ for (const edge of edges) {
103
+ inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
104
+ outDegree.set(edge.from, (outDegree.get(edge.from) ?? 0) + 1);
105
+ }
106
+ let maxInDegree = 0;
107
+ let maxOutDegree = 0;
108
+ let maxInDegreeNodeId;
109
+ let maxOutDegreeNodeId;
110
+ for (const node of nodes) {
111
+ const inD = inDegree.get(node.id);
112
+ const outD = outDegree.get(node.id);
113
+ if (inD > maxInDegree) {
114
+ maxInDegree = inD;
115
+ maxInDegreeNodeId = node.id;
116
+ }
117
+ if (outD > maxOutDegree) {
118
+ maxOutDegree = outD;
119
+ maxOutDegreeNodeId = node.id;
120
+ }
121
+ }
122
+ const avgDegree = nodeCount === 0 ? 0 : (2 * edgeCount) / nodeCount;
123
+ const totalMethods = nodes.reduce((sum, n) => sum + n.contract.methods.length, 0);
124
+ const totalResponsibilities = nodes.reduce((sum, n) => sum + n.contract.responsibilities.length, 0);
125
+ const avgMethodsPerNode = nodeCount === 0 ? 0 : totalMethods / nodeCount;
126
+ const avgResponsibilitiesPerNode = nodeCount === 0 ? 0 : totalResponsibilities / nodeCount;
127
+ const nodeIds = nodes.map((n) => n.id);
128
+ const connectedComponents = nodeCount === 0 ? 0 : computeConnectedComponents(nodeIds, edges);
129
+ let isolatedNodes = 0;
130
+ let leafNodes = 0;
131
+ for (const node of nodes) {
132
+ const totalDegree = inDegree.get(node.id) + outDegree.get(node.id);
133
+ if (totalDegree === 0)
134
+ isolatedNodes++;
135
+ else if (totalDegree === 1)
136
+ leafNodes++;
137
+ }
138
+ return {
139
+ analyzedAt: new Date().toISOString(),
140
+ nodeCount,
141
+ edgeCount,
142
+ nodesByKind,
143
+ edgesByKind,
144
+ nodesByStatus,
145
+ density,
146
+ avgDegree,
147
+ maxInDegree,
148
+ maxOutDegree,
149
+ maxInDegreeNodeId,
150
+ maxOutDegreeNodeId,
151
+ avgMethodsPerNode,
152
+ avgResponsibilitiesPerNode,
153
+ totalMethods,
154
+ totalResponsibilities,
155
+ connectedComponents,
156
+ isolatedNodes,
157
+ leafNodes,
158
+ };
159
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=metrics.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.test.d.ts","sourceRoot":"","sources":["../src/metrics.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { computeGraphMetrics } from "./metrics";
3
+ import { emptyContract } from "@abhinav2203/codeflow-core/schema";
4
+ const node = (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 edge = (from, to, kind = "calls") => ({
15
+ from,
16
+ to,
17
+ kind,
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("computeGraphMetrics", () => {
31
+ it("computes correct basic metrics for a simple graph", () => {
32
+ const metrics = computeGraphMetrics(graph("Simple", [node("A", "module"), node("B", "api"), node("C", "function")], [edge("A", "B"), edge("B", "C")]));
33
+ expect(metrics.nodeCount).toBe(3);
34
+ expect(metrics.edgeCount).toBe(2);
35
+ expect(metrics.nodesByKind["module"]).toBe(1);
36
+ expect(metrics.nodesByKind["api"]).toBe(1);
37
+ expect(metrics.nodesByKind["function"]).toBe(1);
38
+ expect(metrics.connectedComponents).toBe(1);
39
+ });
40
+ it("returns all zeros for an empty graph", () => {
41
+ const metrics = computeGraphMetrics(graph("Empty", [], []));
42
+ expect(metrics.nodeCount).toBe(0);
43
+ expect(metrics.edgeCount).toBe(0);
44
+ expect(metrics.density).toBe(0);
45
+ expect(metrics.connectedComponents).toBe(0);
46
+ expect(metrics.avgDegree).toBe(0);
47
+ expect(metrics.isolatedNodes).toBe(0);
48
+ expect(metrics.leafNodes).toBe(0);
49
+ });
50
+ it("counts isolated and leaf nodes correctly", () => {
51
+ // A → B (A: out=1, B: in=1) — C is isolated
52
+ const metrics = computeGraphMetrics(graph("IsolatedLeaf", [node("A", "module"), node("B", "module"), node("C", "module")], [edge("A", "B")]));
53
+ expect(metrics.isolatedNodes).toBe(1); // C has degree 0
54
+ expect(metrics.leafNodes).toBe(2); // A has out=1, B has in=1
55
+ });
56
+ it("density stays <= 1 when parallel edges exist between the same pair", () => {
57
+ const metrics = computeGraphMetrics(graph("ParallelEdges", [node("A", "module"), node("B", "module")], [edge("A", "B"), { from: "A", to: "B", kind: "imports", required: false, confidence: 0.9 }]));
58
+ expect(metrics.density).toBeLessThanOrEqual(1);
59
+ // One unique directed pair (A→B) out of 2 possible (A→B, B→A) = 0.5
60
+ expect(metrics.density).toBeCloseTo(0.5);
61
+ });
62
+ it("identifies max in-degree and max out-degree nodes", () => {
63
+ // A → B, A → C, D → B → in(B)=2, out(A)=2
64
+ const metrics = computeGraphMetrics(graph("DegreeStats", [node("A", "module"), node("B", "module"), node("C", "module"), node("D", "module")], [edge("A", "B"), edge("A", "C"), edge("D", "B")]));
65
+ expect(metrics.maxInDegree).toBe(2);
66
+ expect(metrics.maxOutDegree).toBe(2);
67
+ expect(metrics.maxInDegreeNodeId).toBe("B");
68
+ expect(metrics.maxOutDegreeNodeId).toBe("A");
69
+ });
70
+ it("counts edges by kind correctly", () => {
71
+ const metrics = computeGraphMetrics(graph("EdgesByKind", [node("A", "module"), node("B", "module")], [edge("A", "B"), { from: "A", to: "B", kind: "imports", required: true, confidence: 1 }]));
72
+ expect(metrics.edgesByKind["calls"]).toBe(1);
73
+ expect(metrics.edgesByKind["imports"]).toBe(1);
74
+ });
75
+ it("avgMethodsPerNode is computed correctly", () => {
76
+ const metrics = computeGraphMetrics(graph("Methods", [
77
+ {
78
+ ...node("A", "class"),
79
+ contract: { ...emptyContract(), methods: [{}, {}] },
80
+ },
81
+ {
82
+ ...node("B", "class"),
83
+ contract: { ...emptyContract(), methods: [{}] },
84
+ },
85
+ ], []));
86
+ expect(metrics.totalMethods).toBe(3);
87
+ expect(metrics.avgMethodsPerNode).toBeCloseTo(1.5);
88
+ });
89
+ it("connectedComponents uses Union-Find correctly for a disconnected graph", () => {
90
+ // Two disconnected components: {A, B} and {C, D}
91
+ const metrics = computeGraphMetrics(graph("Disconnected", [node("A"), node("B"), node("C"), node("D")], [edge("A", "B"), edge("C", "D")]));
92
+ expect(metrics.connectedComponents).toBe(2);
93
+ });
94
+ it("computed at timestamp is a valid ISO string", () => {
95
+ const metrics = computeGraphMetrics(graph("Timestamp", [node("A")], []));
96
+ expect(() => new Date(metrics.analyzedAt)).not.toThrow();
97
+ });
98
+ });
@@ -0,0 +1,84 @@
1
+ import type { BlueprintEdgeKind, BlueprintGraph, FeatureMaturity, OutputProvenance } from "@abhinav2203/codeflow-core/schema";
2
+ /** The category of architectural drift that was detected. */
3
+ export type DriftKind = "broken-edge" | "missing-edge" | "signature-drift";
4
+ /**
5
+ * A single detected drift issue in the architecture graph.
6
+ *
7
+ * - `broken-edge` – An edge references a node ID that no longer exists.
8
+ * - `missing-edge` – A node's contract `calls` entry has no corresponding
9
+ * graph edge to the resolved target node.
10
+ * - `signature-drift` – The node's top-level `signature` field doesn't match
11
+ * the `signature` of its first contract method.
12
+ */
13
+ export interface DriftIssue {
14
+ kind: DriftKind;
15
+ /** ID of the existing node most closely associated with this issue. */
16
+ nodeId: string;
17
+ nodeName: string;
18
+ description: string;
19
+ /** Source node ID of the affected edge (present for edge-related issues). */
20
+ edgeFrom?: string;
21
+ /** Target node ID of the affected edge (present for edge-related issues). */
22
+ edgeTo?: string;
23
+ /**
24
+ * The node ID referenced by the edge that no longer exists in the graph
25
+ * (only set for `broken-edge` issues where the missing ID differs from `nodeId`).
26
+ */
27
+ missingNodeId?: string;
28
+ /**
29
+ * For `missing-edge` issues: the edge `kind` declared in the contract call.
30
+ * Used during healing to distinguish multiple calls between the same pair of
31
+ * nodes with different relationship kinds (e.g. `calls` vs `reads-state`).
32
+ */
33
+ edgeKind?: BlueprintEdgeKind;
34
+ }
35
+ /** Summary of all drift issues detected in a graph. */
36
+ export interface RefactorReport {
37
+ projectName: string;
38
+ detectedAt: string;
39
+ provenance: OutputProvenance;
40
+ maturity: FeatureMaturity;
41
+ scope: "graph";
42
+ issues: DriftIssue[];
43
+ /** IDs of nodes that have at least one drift issue. */
44
+ driftedNodeIds: string[];
45
+ totalIssues: number;
46
+ /** `true` when no drift was found. */
47
+ isHealthy: boolean;
48
+ }
49
+ /** Result of a heal operation that auto-fixed drift issues. */
50
+ export interface HealResult {
51
+ projectName: string;
52
+ healedAt: string;
53
+ provenance: OutputProvenance;
54
+ maturity: FeatureMaturity;
55
+ scope: "graph";
56
+ issuesFixed: number;
57
+ graph: BlueprintGraph;
58
+ summary: string[];
59
+ }
60
+ /**
61
+ * Detect architectural drift in a blueprint graph.
62
+ *
63
+ * Three kinds of drift are checked:
64
+ * 1. **Broken edges** – an edge's `from` or `to` points to a node ID that no
65
+ * longer exists in the graph.
66
+ * 2. **Missing edges** – a node's contract `calls` entry references a target
67
+ * that exists in the graph but has no corresponding edge.
68
+ * 3. **Signature drift** – the node's top-level `signature` field doesn't
69
+ * match the `signature` of its first contract method.
70
+ */
71
+ export declare const detectDrift: (graph: BlueprintGraph) => RefactorReport;
72
+ /**
73
+ * Auto-heal a blueprint graph based on a previously computed {@link RefactorReport}.
74
+ *
75
+ * Healing actions:
76
+ * - **Broken edges** are removed.
77
+ * - **Missing edges** are synthesised from the contract call definitions.
78
+ * - **Signature drift** is resolved by syncing the node's top-level
79
+ * `signature` to match its first contract method.
80
+ *
81
+ * The original graph is not mutated; a new graph object is returned.
82
+ */
83
+ export declare const healGraph: (graph: BlueprintGraph, report: RefactorReport) => HealResult;
84
+ //# sourceMappingURL=refactor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refactor.d.ts","sourceRoot":"","sources":["../src/refactor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,iBAAiB,EACjB,cAAc,EAEd,eAAe,EACf,gBAAgB,EACjB,MAAM,mCAAmC,CAAC;AAI3C,6DAA6D;AAC7D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,cAAc,GAAG,iBAAiB,CAAC;AAE3E;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,QAAQ,EAAE,eAAe,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,uDAAuD;IACvD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,QAAQ,EAAE,eAAe,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,cAAc,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAsBD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,cAAc,KAAG,cAuFnD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,cAAc,EAAE,QAAQ,cAAc,KAAG,UAqFzE,CAAC"}