@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,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"}
@@ -0,0 +1,183 @@
1
+ // ── Internal helpers ──────────────────────────────────────────────────────────
2
+ const buildNodeIndex = (graph) => new Map(graph.nodes.map((n) => [n.id, n]));
3
+ /**
4
+ * Resolve a contract call `target` (which may be a node ID or node name) to
5
+ * the matching blueprint node.
6
+ */
7
+ const resolveCallTarget = (graph, target) => {
8
+ const byId = graph.nodes.find((n) => n.id === target);
9
+ if (byId)
10
+ return byId;
11
+ return graph.nodes.find((n) => n.name === target);
12
+ };
13
+ // ── Public API ────────────────────────────────────────────────────────────────
14
+ /**
15
+ * Detect architectural drift in a blueprint graph.
16
+ *
17
+ * Three kinds of drift are checked:
18
+ * 1. **Broken edges** – an edge's `from` or `to` points to a node ID that no
19
+ * longer exists in the graph.
20
+ * 2. **Missing edges** – a node's contract `calls` entry references a target
21
+ * that exists in the graph but has no corresponding edge.
22
+ * 3. **Signature drift** – the node's top-level `signature` field doesn't
23
+ * match the `signature` of its first contract method.
24
+ */
25
+ export const detectDrift = (graph) => {
26
+ const issues = [];
27
+ const index = buildNodeIndex(graph);
28
+ // ── 1. Broken edges ────────────────────────────────────────────────────────
29
+ for (const edge of graph.edges) {
30
+ if (!index.has(edge.from)) {
31
+ const existingNode = index.get(edge.to);
32
+ issues.push({
33
+ kind: "broken-edge",
34
+ nodeId: existingNode?.id ?? edge.to,
35
+ nodeName: existingNode?.name ?? edge.to,
36
+ description: `Edge "${edge.from}" → "${edge.to}" references a non-existent source node.`,
37
+ edgeFrom: edge.from,
38
+ edgeTo: edge.to,
39
+ missingNodeId: edge.from,
40
+ });
41
+ }
42
+ if (!index.has(edge.to)) {
43
+ const existingNode = index.get(edge.from);
44
+ issues.push({
45
+ kind: "broken-edge",
46
+ nodeId: existingNode?.id ?? edge.from,
47
+ nodeName: existingNode?.name ?? edge.from,
48
+ description: `Edge "${edge.from}" → "${edge.to}" references a non-existent target node.`,
49
+ edgeFrom: edge.from,
50
+ edgeTo: edge.to,
51
+ missingNodeId: edge.to,
52
+ });
53
+ }
54
+ }
55
+ // ── 2. Missing edges + signature drift ────────────────────────────────────
56
+ for (const node of graph.nodes) {
57
+ // Signature drift: top-level signature doesn't match the first method's.
58
+ const firstMethod = node.contract.methods?.[0];
59
+ if (node.signature &&
60
+ firstMethod?.signature &&
61
+ node.signature !== firstMethod.signature) {
62
+ issues.push({
63
+ kind: "signature-drift",
64
+ nodeId: node.id,
65
+ nodeName: node.name,
66
+ description: `Node "${node.name}" signature "${node.signature}" does not match contract method "${firstMethod.signature}".`,
67
+ });
68
+ }
69
+ // Missing edges: contract calls with no corresponding graph edge.
70
+ for (const call of node.contract.calls ?? []) {
71
+ const targetNode = resolveCallTarget(graph, call.target);
72
+ if (!targetNode)
73
+ continue; // target not in graph – not our responsibility here
74
+ const edgeKind = call.kind ?? "calls";
75
+ const edgeExists = graph.edges.some((e) => e.from === node.id && e.to === targetNode.id && e.kind === edgeKind);
76
+ if (!edgeExists) {
77
+ issues.push({
78
+ kind: "missing-edge",
79
+ nodeId: node.id,
80
+ nodeName: node.name,
81
+ description: `Node "${node.name}" declares a "${edgeKind}" call to "${call.target}" in its contract but no graph edge exists.`,
82
+ edgeFrom: node.id,
83
+ edgeTo: targetNode.id,
84
+ edgeKind,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ const driftedNodeIds = [...new Set(issues.map((i) => i.nodeId))];
90
+ return {
91
+ projectName: graph.projectName,
92
+ detectedAt: new Date().toISOString(),
93
+ provenance: "deterministic",
94
+ maturity: "preview",
95
+ scope: "graph",
96
+ issues,
97
+ driftedNodeIds,
98
+ totalIssues: issues.length,
99
+ isHealthy: issues.length === 0,
100
+ };
101
+ };
102
+ /**
103
+ * Auto-heal a blueprint graph based on a previously computed {@link RefactorReport}.
104
+ *
105
+ * Healing actions:
106
+ * - **Broken edges** are removed.
107
+ * - **Missing edges** are synthesised from the contract call definitions.
108
+ * - **Signature drift** is resolved by syncing the node's top-level
109
+ * `signature` to match its first contract method.
110
+ *
111
+ * The original graph is not mutated; a new graph object is returned.
112
+ */
113
+ export const healGraph = (graph, report) => {
114
+ const index = buildNodeIndex(graph);
115
+ const summary = [];
116
+ let issuesFixed = 0;
117
+ // ── Remove broken edges ─────────────────────────────────────────────────────
118
+ const healedEdges = graph.edges.filter((edge) => {
119
+ if (!index.has(edge.from) || !index.has(edge.to)) {
120
+ summary.push(`Removed broken edge: ${edge.from} → ${edge.to}`);
121
+ issuesFixed++;
122
+ return false;
123
+ }
124
+ return true;
125
+ });
126
+ // ── Synthesise missing edges ────────────────────────────────────────────────
127
+ const newEdges = [];
128
+ const missingEdgeIssues = report.issues.filter((i) => i.kind === "missing-edge");
129
+ for (const issue of missingEdgeIssues) {
130
+ if (!issue.edgeFrom || !issue.edgeTo)
131
+ continue;
132
+ const issueEdgeKind = issue.edgeKind ?? "calls";
133
+ const alreadyAdded = newEdges.some((e) => e.from === issue.edgeFrom && e.to === issue.edgeTo && e.kind === issueEdgeKind);
134
+ if (alreadyAdded)
135
+ continue;
136
+ // Find the original contract call to preserve kind/label. Match on both
137
+ // target node ID and kind so that multiple calls between the same pair with
138
+ // different kinds each resolve to their own contract entry.
139
+ const fromNode = index.get(issue.edgeFrom);
140
+ const call = fromNode?.contract.calls?.find((c) => {
141
+ const target = resolveCallTarget(graph, c.target);
142
+ return target?.id === issue.edgeTo && (c.kind ?? "calls") === issueEdgeKind;
143
+ });
144
+ newEdges.push({
145
+ from: issue.edgeFrom,
146
+ to: issue.edgeTo,
147
+ kind: issueEdgeKind,
148
+ required: false,
149
+ confidence: 0.8,
150
+ label: call?.description,
151
+ });
152
+ const fromName = fromNode?.name ?? issue.edgeFrom;
153
+ const toName = index.get(issue.edgeTo)?.name ?? issue.edgeTo;
154
+ summary.push(`Added missing edge: ${fromName} → ${toName}`);
155
+ issuesFixed++;
156
+ }
157
+ // ── Fix signature drift ────────────────────────────────────────────────────
158
+ const healedNodes = graph.nodes.map((node) => {
159
+ const hasDrift = report.issues.some((i) => i.kind === "signature-drift" && i.nodeId === node.id);
160
+ if (!hasDrift)
161
+ return node;
162
+ const firstMethod = node.contract.methods?.[0];
163
+ if (!firstMethod?.signature)
164
+ return node;
165
+ summary.push(`Synced signature for "${node.name}": "${node.signature}" → "${firstMethod.signature}"`);
166
+ issuesFixed++;
167
+ return { ...node, signature: firstMethod.signature };
168
+ });
169
+ return {
170
+ projectName: graph.projectName,
171
+ healedAt: new Date().toISOString(),
172
+ provenance: "deterministic",
173
+ maturity: "preview",
174
+ scope: "graph",
175
+ issuesFixed,
176
+ graph: {
177
+ ...graph,
178
+ nodes: healedNodes,
179
+ edges: [...healedEdges, ...newEdges],
180
+ },
181
+ summary,
182
+ };
183
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=refactor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refactor.test.d.ts","sourceRoot":"","sources":["../src/refactor.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,269 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { detectDrift, healGraph } from "./refactor";
3
+ import { emptyContract } from "@abhinav2203/codeflow-core/schema";
4
+ // ── Fixtures ───────────────────────────────────────────────────────────────
5
+ const makeNode = (id, overrides = {}) => {
6
+ const calls = overrides.contractCalls?.map((c) => ({
7
+ target: c.target,
8
+ kind: c.kind,
9
+ description: undefined,
10
+ })) ?? [];
11
+ const methods = overrides.firstMethodSignature
12
+ ? [
13
+ {
14
+ name: id,
15
+ signature: overrides.firstMethodSignature,
16
+ summary: "Method.",
17
+ inputs: [],
18
+ outputs: [],
19
+ sideEffects: [],
20
+ calls: [],
21
+ },
22
+ ]
23
+ : [];
24
+ return {
25
+ id,
26
+ kind: "function",
27
+ name: id,
28
+ summary: `${id} summary.`,
29
+ signature: overrides.signature,
30
+ contract: {
31
+ ...emptyContract(),
32
+ ...(calls.length > 0 ? { calls } : {}),
33
+ ...(methods.length > 0 ? { methods } : {}),
34
+ },
35
+ sourceRefs: [],
36
+ generatedRefs: [],
37
+ traceRefs: [],
38
+ };
39
+ };
40
+ const edge = (from, to, kind = "calls") => ({
41
+ from,
42
+ to,
43
+ kind,
44
+ required: false,
45
+ confidence: 1,
46
+ });
47
+ const makeGraph = (overrides = {}) => ({
48
+ projectName: "TestApp",
49
+ mode: "essential",
50
+ phase: "spec",
51
+ generatedAt: "2026-03-14T00:00:00.000Z",
52
+ warnings: [],
53
+ workflows: [],
54
+ edges: [],
55
+ nodes: [
56
+ makeNode("function:auth", {
57
+ contractCalls: [],
58
+ }),
59
+ makeNode("api:users", { contractCalls: [] }),
60
+ makeNode("function:checkout", { contractCalls: [] }),
61
+ ],
62
+ ...overrides,
63
+ });
64
+ // ── detectDrift ─────────────────────────────────────────────────────────────
65
+ describe("detectDrift", () => {
66
+ it("reports a healthy graph with no issues", () => {
67
+ const report = detectDrift(makeGraph());
68
+ expect(report.isHealthy).toBe(true);
69
+ expect(report.issues).toHaveLength(0);
70
+ expect(report.totalIssues).toBe(0);
71
+ expect(report.driftedNodeIds).toHaveLength(0);
72
+ expect(report.provenance).toBe("deterministic");
73
+ expect(report.maturity).toBe("preview");
74
+ expect(report.scope).toBe("graph");
75
+ });
76
+ it("detects a broken edge whose source node does not exist", () => {
77
+ const graph = makeGraph({
78
+ edges: [edge("node:ghost", "function:auth")],
79
+ });
80
+ const report = detectDrift(graph);
81
+ expect(report.isHealthy).toBe(false);
82
+ const brokenIssues = report.issues.filter((i) => i.kind === "broken-edge");
83
+ expect(brokenIssues.length).toBeGreaterThanOrEqual(1);
84
+ // Anchored on the existing endpoint.
85
+ expect(brokenIssues[0].nodeId).toBe("function:auth");
86
+ expect(brokenIssues[0].missingNodeId).toBe("node:ghost");
87
+ // driftedNodeIds only contains real node IDs.
88
+ expect(report.driftedNodeIds).toContain("function:auth");
89
+ expect(report.driftedNodeIds).not.toContain("node:ghost");
90
+ });
91
+ it("detects a broken edge whose target node does not exist", () => {
92
+ const graph = makeGraph({
93
+ edges: [edge("function:auth", "node:deleted")],
94
+ });
95
+ const report = detectDrift(graph);
96
+ const brokenIssues = report.issues.filter((i) => i.kind === "broken-edge");
97
+ expect(brokenIssues.length).toBeGreaterThanOrEqual(1);
98
+ expect(brokenIssues[0].nodeId).toBe("function:auth");
99
+ expect(brokenIssues[0].missingNodeId).toBe("node:deleted");
100
+ expect(report.driftedNodeIds).toContain("function:auth");
101
+ expect(report.driftedNodeIds).not.toContain("node:deleted");
102
+ });
103
+ it("detects a missing edge when a contract call has no graph edge", () => {
104
+ const graph = makeGraph({
105
+ nodes: makeGraph().nodes.map((n) => n.id === "function:auth"
106
+ ? makeNode("function:auth", { contractCalls: [{ target: "api:users", kind: "calls" }] })
107
+ : n),
108
+ });
109
+ const report = detectDrift(graph);
110
+ const missingIssues = report.issues.filter((i) => i.kind === "missing-edge");
111
+ expect(missingIssues.length).toBeGreaterThanOrEqual(1);
112
+ expect(missingIssues[0].edgeFrom).toBe("function:auth");
113
+ expect(missingIssues[0].edgeTo).toBe("api:users");
114
+ });
115
+ it("does NOT report a missing-edge when the edge already exists", () => {
116
+ const graph = makeGraph({
117
+ nodes: makeGraph().nodes.map((n) => n.id === "function:auth"
118
+ ? makeNode("function:auth", { contractCalls: [{ target: "api:users", kind: "calls" }] })
119
+ : n),
120
+ edges: [edge("function:auth", "api:users", "calls")],
121
+ });
122
+ const report = detectDrift(graph);
123
+ expect(report.issues.filter((i) => i.kind === "missing-edge")).toHaveLength(0);
124
+ });
125
+ it("detects signature drift when node signature does not match first method", () => {
126
+ const graph = makeGraph({
127
+ nodes: makeGraph().nodes.map((n) => n.id === "function:auth"
128
+ ? makeNode("function:auth", {
129
+ signature: "authenticate(token: string): void",
130
+ firstMethodSignature: "authenticate(token: string, opts?: Options): string",
131
+ })
132
+ : n),
133
+ });
134
+ const report = detectDrift(graph);
135
+ const driftIssues = report.issues.filter((i) => i.kind === "signature-drift");
136
+ expect(driftIssues.length).toBeGreaterThanOrEqual(1);
137
+ expect(driftIssues[0].nodeId).toBe("function:auth");
138
+ });
139
+ it("does NOT report signature drift when signatures match", () => {
140
+ const sig = "authenticate(token: string): string";
141
+ const graph = makeGraph({
142
+ nodes: makeGraph().nodes.map((n) => n.id === "function:auth"
143
+ ? makeNode("function:auth", {
144
+ signature: sig,
145
+ firstMethodSignature: sig,
146
+ })
147
+ : n),
148
+ });
149
+ const report = detectDrift(graph);
150
+ expect(report.issues.filter((i) => i.kind === "signature-drift")).toHaveLength(0);
151
+ });
152
+ it("populates driftedNodeIds with unique node IDs", () => {
153
+ const graph = makeGraph({
154
+ edges: [edge("node:ghost", "function:auth")],
155
+ });
156
+ const report = detectDrift(graph);
157
+ expect(report.driftedNodeIds).toContain("function:auth");
158
+ expect(report.driftedNodeIds).not.toContain("node:ghost");
159
+ expect(report.driftedNodeIds.filter((id) => id === "function:auth")).toHaveLength(1);
160
+ });
161
+ it("includes projectName and detectedAt in the report", () => {
162
+ const report = detectDrift(makeGraph());
163
+ expect(report.projectName).toBe("TestApp");
164
+ expect(report.detectedAt).toBeTruthy();
165
+ });
166
+ });
167
+ // ── healGraph ────────────────────────────────────────────────────────────────
168
+ describe("healGraph", () => {
169
+ it("returns unchanged graph when the report is healthy", () => {
170
+ const graph = makeGraph();
171
+ const report = detectDrift(graph);
172
+ const result = healGraph(graph, report);
173
+ expect(result.issuesFixed).toBe(0);
174
+ expect(result.graph.edges).toHaveLength(0);
175
+ expect(result.graph.nodes).toHaveLength(graph.nodes.length);
176
+ });
177
+ it("removes broken edges", () => {
178
+ const graph = makeGraph({
179
+ edges: [edge("node:ghost", "function:auth"), edge("function:auth", "api:users")],
180
+ });
181
+ const report = detectDrift(graph);
182
+ const result = healGraph(graph, report);
183
+ expect(result.graph.edges.some((e) => e.from === "node:ghost")).toBe(false);
184
+ expect(result.graph.edges.some((e) => e.from === "function:auth" && e.to === "api:users")).toBe(true);
185
+ expect(result.issuesFixed).toBeGreaterThanOrEqual(1);
186
+ expect(result.summary.some((s) => s.includes("Removed broken edge"))).toBe(true);
187
+ });
188
+ it("adds missing edges from contract calls", () => {
189
+ const graph = makeGraph({
190
+ nodes: makeGraph().nodes.map((n) => n.id === "function:auth"
191
+ ? makeNode("function:auth", { contractCalls: [{ target: "api:users", kind: "calls" }] })
192
+ : n),
193
+ });
194
+ const report = detectDrift(graph);
195
+ const result = healGraph(graph, report);
196
+ expect(result.graph.edges.some((e) => e.from === "function:auth" && e.to === "api:users" && e.kind === "calls")).toBe(true);
197
+ expect(result.issuesFixed).toBeGreaterThanOrEqual(1);
198
+ expect(result.summary.some((s) => s.includes("Added missing edge"))).toBe(true);
199
+ });
200
+ it("does not duplicate edges when healing the same missing edge twice", () => {
201
+ const graph = makeGraph({
202
+ nodes: makeGraph().nodes.map((n) => n.id === "function:auth"
203
+ ? makeNode("function:auth", { contractCalls: [{ target: "api:users", kind: "calls" }] })
204
+ : n),
205
+ });
206
+ const report = detectDrift(graph);
207
+ const result = healGraph(graph, report);
208
+ const edgesFromAuth = result.graph.edges.filter((e) => e.from === "function:auth" && e.to === "api:users");
209
+ expect(edgesFromAuth).toHaveLength(1);
210
+ });
211
+ it("syncs signature drift to the first contract method signature", () => {
212
+ const correctedSig = "authenticate(token: string, opts?: Options): string";
213
+ const graph = makeGraph({
214
+ nodes: makeGraph().nodes.map((n) => n.id === "function:auth"
215
+ ? makeNode("function:auth", {
216
+ signature: "authenticate(token: string): void",
217
+ firstMethodSignature: correctedSig,
218
+ })
219
+ : n),
220
+ });
221
+ const report = detectDrift(graph);
222
+ const result = healGraph(graph, report);
223
+ const authNode = result.graph.nodes.find((n) => n.id === "function:auth");
224
+ expect(authNode?.signature).toBe(correctedSig);
225
+ expect(result.issuesFixed).toBeGreaterThanOrEqual(1);
226
+ expect(result.summary.some((s) => s.includes("Synced signature"))).toBe(true);
227
+ });
228
+ it("does not mutate the original graph", () => {
229
+ const graph = makeGraph({
230
+ edges: [edge("node:ghost", "function:auth")],
231
+ });
232
+ const originalEdgeCount = graph.edges.length;
233
+ const report = detectDrift(graph);
234
+ healGraph(graph, report);
235
+ expect(graph.edges).toHaveLength(originalEdgeCount);
236
+ });
237
+ it("includes provenance and maturity in the result", () => {
238
+ const graph = makeGraph();
239
+ const report = detectDrift(graph);
240
+ const result = healGraph(graph, report);
241
+ expect(result.projectName).toBe("TestApp");
242
+ expect(result.healedAt).toBeTruthy();
243
+ expect(result.provenance).toBe("deterministic");
244
+ expect(result.maturity).toBe("preview");
245
+ expect(result.scope).toBe("graph");
246
+ });
247
+ it("synthesises one edge per distinct (from, to, kind) when multiple calls have different kinds", () => {
248
+ const graph = makeGraph({
249
+ nodes: makeGraph().nodes.map((n) => n.id === "function:auth"
250
+ ? makeNode("function:auth", {
251
+ contractCalls: [
252
+ { target: "api:users", kind: "calls" },
253
+ { target: "api:users", kind: "reads-state" },
254
+ ],
255
+ })
256
+ : n),
257
+ });
258
+ const report = detectDrift(graph);
259
+ const missingIssues = report.issues.filter((i) => i.kind === "missing-edge");
260
+ // Two distinct kinds → two distinct missing-edge issues.
261
+ expect(missingIssues).toHaveLength(2);
262
+ const result = healGraph(graph, report);
263
+ const callsEdges = result.graph.edges.filter((e) => e.from === "function:auth" && e.to === "api:users" && e.kind === "calls");
264
+ const readsEdges = result.graph.edges.filter((e) => e.from === "function:auth" && e.to === "api:users" && e.kind === "reads-state");
265
+ expect(callsEdges).toHaveLength(1);
266
+ expect(readsEdges).toHaveLength(1);
267
+ expect(result.issuesFixed).toBe(2);
268
+ });
269
+ });
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ import type { BlueprintGraph } from "@abhinav2203/codeflow-core/schema";
3
+ export declare const smellSchema: z.ZodObject<{
4
+ code: z.ZodString;
5
+ severity: z.ZodEnum<["info", "warning", "critical"]>;
6
+ nodeId: z.ZodOptional<z.ZodString>;
7
+ message: z.ZodString;
8
+ suggestion: z.ZodString;
9
+ }, "strip", z.ZodTypeAny, {
10
+ message: string;
11
+ code: string;
12
+ severity: "warning" | "info" | "critical";
13
+ suggestion: string;
14
+ nodeId?: string | undefined;
15
+ }, {
16
+ message: string;
17
+ code: string;
18
+ severity: "warning" | "info" | "critical";
19
+ suggestion: string;
20
+ nodeId?: string | undefined;
21
+ }>;
22
+ export type Smell = z.infer<typeof smellSchema>;
23
+ export declare const smellReportSchema: z.ZodObject<{
24
+ analyzedAt: z.ZodString;
25
+ totalSmells: z.ZodNumber;
26
+ smells: z.ZodArray<z.ZodObject<{
27
+ code: z.ZodString;
28
+ severity: z.ZodEnum<["info", "warning", "critical"]>;
29
+ nodeId: z.ZodOptional<z.ZodString>;
30
+ message: z.ZodString;
31
+ suggestion: z.ZodString;
32
+ }, "strip", z.ZodTypeAny, {
33
+ message: string;
34
+ code: string;
35
+ severity: "warning" | "info" | "critical";
36
+ suggestion: string;
37
+ nodeId?: string | undefined;
38
+ }, {
39
+ message: string;
40
+ code: string;
41
+ severity: "warning" | "info" | "critical";
42
+ suggestion: string;
43
+ nodeId?: string | undefined;
44
+ }>, "many">;
45
+ healthScore: z.ZodNumber;
46
+ }, "strip", z.ZodTypeAny, {
47
+ analyzedAt: string;
48
+ totalSmells: number;
49
+ smells: {
50
+ message: string;
51
+ code: string;
52
+ severity: "warning" | "info" | "critical";
53
+ suggestion: string;
54
+ nodeId?: string | undefined;
55
+ }[];
56
+ healthScore: number;
57
+ }, {
58
+ analyzedAt: string;
59
+ totalSmells: number;
60
+ smells: {
61
+ message: string;
62
+ code: string;
63
+ severity: "warning" | "info" | "critical";
64
+ suggestion: string;
65
+ nodeId?: string | undefined;
66
+ }[];
67
+ healthScore: number;
68
+ }>;
69
+ export type SmellReport = z.infer<typeof smellReportSchema>;
70
+ /**
71
+ * Detect all architecture smells in a blueprint graph.
72
+ *
73
+ * Smell categories: god-node, hub-node, orphan-node, tight-coupling,
74
+ * unstable-dependency, scattered-responsibility.
75
+ */
76
+ export declare const detectSmells: (graph: BlueprintGraph) => SmellReport;
77
+ //# sourceMappingURL=smells.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smells.d.ts","sourceRoot":"","sources":["../src/smells.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;EAMtB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAK5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAmL5D;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,cAAc,KAAG,WAgBpD,CAAC"}