@abhinav2203/codeflow-analysis 0.1.0 → 0.1.2

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 +109 -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 +73 -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 +77 -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":"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
+ message: string;
17
+ code: string;
18
+ severity: "warning" | "info" | "critical";
19
+ suggestion: string;
20
+ nodeId?: string | undefined;
21
+ }[];
22
+ healthScore: number;
23
+ };
24
+ }> | NextResponse<{
25
+ error: string;
26
+ }>>;
27
+ //# sourceMappingURL=smells.d.ts.map
@@ -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,73 @@
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
+ }, "strip", z.ZodTypeAny, {
24
+ analyzedAt: string;
25
+ nodeCount: number;
26
+ edgeCount: number;
27
+ nodesByKind: Record<string, number>;
28
+ edgesByKind: Record<string, number>;
29
+ nodesByStatus: Record<string, number>;
30
+ density: number;
31
+ avgDegree: number;
32
+ maxInDegree: number;
33
+ maxOutDegree: number;
34
+ avgMethodsPerNode: number;
35
+ avgResponsibilitiesPerNode: number;
36
+ totalMethods: number;
37
+ totalResponsibilities: number;
38
+ connectedComponents: number;
39
+ isolatedNodes: number;
40
+ leafNodes: number;
41
+ maxInDegreeNodeId?: string | undefined;
42
+ maxOutDegreeNodeId?: string | undefined;
43
+ }, {
44
+ analyzedAt: string;
45
+ nodeCount: number;
46
+ edgeCount: number;
47
+ nodesByKind: Record<string, number>;
48
+ edgesByKind: Record<string, number>;
49
+ nodesByStatus: Record<string, number>;
50
+ density: number;
51
+ avgDegree: number;
52
+ maxInDegree: number;
53
+ maxOutDegree: number;
54
+ avgMethodsPerNode: number;
55
+ avgResponsibilitiesPerNode: number;
56
+ totalMethods: number;
57
+ totalResponsibilities: number;
58
+ connectedComponents: number;
59
+ isolatedNodes: number;
60
+ leafNodes: number;
61
+ maxInDegreeNodeId?: string | undefined;
62
+ maxOutDegreeNodeId?: string | undefined;
63
+ }>;
64
+ export type GraphMetrics = z.infer<typeof graphMetricsSchema>;
65
+ /**
66
+ * Compute structural metrics for a blueprint graph.
67
+ *
68
+ * Metrics include: node/edge counts, degree statistics, graph density,
69
+ * connected components, isolated/leaf node counts, and contract-level
70
+ * averages (methods and responsibilities per node).
71
+ */
72
+ export declare const computeGraphMetrics: (graph: BlueprintGraph) => GraphMetrics;
73
+ //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoB7B,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
+ });