microservice-kg 0.1.0
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/LICENSE +21 -0
- package/README.md +161 -0
- package/package.json +44 -0
- package/src/analyzer.mjs +1137 -0
- package/src/cli.mjs +70 -0
- package/src/export-obsidian.mjs +226 -0
- package/src/graph-query.mjs +292 -0
- package/src/mcp-server.mjs +260 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { analyzeWorkspace, writeGraphArtifacts } from "./analyzer.mjs";
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`Usage:
|
|
9
|
+
microservice-kg analyze <input-directory> [--output <output-directory>]
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
microservice-kg analyze /Users/me/workspace/services
|
|
13
|
+
microservice-kg analyze /Users/me/workspace/services --output /tmp/service-kg
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const [, , command, maybeInput, ...rest] = argv;
|
|
19
|
+
if (!command || command === "--help" || command === "-h") {
|
|
20
|
+
return { help: true };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const options = {
|
|
24
|
+
command,
|
|
25
|
+
inputDir: maybeInput,
|
|
26
|
+
outputDir: null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
30
|
+
const token = rest[index];
|
|
31
|
+
if (token === "--output" || token === "-o") {
|
|
32
|
+
options.outputDir = rest[index + 1];
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return options;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const args = parseArgs(process.argv);
|
|
42
|
+
if (args.help || !args.command) {
|
|
43
|
+
printUsage();
|
|
44
|
+
process.exit(args.help ? 0 : 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (args.command !== "analyze" || !args.inputDir) {
|
|
48
|
+
printUsage();
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const inputDir = path.resolve(args.inputDir);
|
|
53
|
+
const outputDir = args.outputDir
|
|
54
|
+
? path.resolve(args.outputDir)
|
|
55
|
+
: path.join(inputDir, ".microservice-kg");
|
|
56
|
+
|
|
57
|
+
const graph = await analyzeWorkspace(inputDir);
|
|
58
|
+
await writeGraphArtifacts(graph, outputDir);
|
|
59
|
+
|
|
60
|
+
console.log(`Analyzed workspace: ${inputDir}`);
|
|
61
|
+
console.log(`Discovered services: ${graph.serviceCount}`);
|
|
62
|
+
console.log(`Discovered service edges: ${graph.serviceEdges.length}`);
|
|
63
|
+
console.log(`Wrote graph to: ${path.join(outputDir, "service-graph.json")}`);
|
|
64
|
+
console.log(`Wrote summary to: ${path.join(outputDir, "summary.md")}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch((error) => {
|
|
68
|
+
console.error(error?.stack || error?.message || String(error));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const [, , inputGraphPath, outputVaultPath] = process.argv;
|
|
8
|
+
if (!inputGraphPath || !outputVaultPath) {
|
|
9
|
+
console.error("Usage: node export-obsidian.mjs <service-graph.json> <output-vault>");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const graph = JSON.parse(await fs.readFile(path.resolve(inputGraphPath), "utf8"));
|
|
14
|
+
const vaultPath = path.resolve(outputVaultPath);
|
|
15
|
+
const servicesDir = path.join(vaultPath, "services");
|
|
16
|
+
const obsidianDir = path.join(vaultPath, ".obsidian");
|
|
17
|
+
|
|
18
|
+
await fs.rm(vaultPath, { recursive: true, force: true });
|
|
19
|
+
await fs.mkdir(servicesDir, { recursive: true });
|
|
20
|
+
await fs.mkdir(obsidianDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
const serviceMap = new Map();
|
|
23
|
+
for (const service of graph.services) {
|
|
24
|
+
if (!serviceMap.has(service.id)) {
|
|
25
|
+
serviceMap.set(service.id, {
|
|
26
|
+
id: service.id,
|
|
27
|
+
roots: new Set(),
|
|
28
|
+
endpointCount: 0,
|
|
29
|
+
clientCount: 0,
|
|
30
|
+
methodInteractionCount: 0,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const entry = serviceMap.get(service.id);
|
|
34
|
+
entry.roots.add(service.relativeRootDir || service.rootDir || service.id);
|
|
35
|
+
entry.endpointCount += service.endpoints?.length || 0;
|
|
36
|
+
entry.clientCount += service.clients?.length || 0;
|
|
37
|
+
entry.methodInteractionCount += service.methodInteractions?.length || 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const outgoingMap = new Map();
|
|
41
|
+
const incomingMap = new Map();
|
|
42
|
+
for (const edge of graph.serviceEdges) {
|
|
43
|
+
if (!outgoingMap.has(edge.sourceServiceId)) {
|
|
44
|
+
outgoingMap.set(edge.sourceServiceId, []);
|
|
45
|
+
}
|
|
46
|
+
outgoingMap.get(edge.sourceServiceId).push(edge);
|
|
47
|
+
|
|
48
|
+
if (!incomingMap.has(edge.targetServiceId)) {
|
|
49
|
+
incomingMap.set(edge.targetServiceId, []);
|
|
50
|
+
}
|
|
51
|
+
incomingMap.get(edge.targetServiceId).push(edge);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const notePathByServiceId = new Map();
|
|
55
|
+
for (const serviceId of Array.from(serviceMap.keys()).sort((a, b) => a.localeCompare(b))) {
|
|
56
|
+
notePathByServiceId.set(serviceId, `services/${serviceId}.md`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const serviceIds = Array.from(serviceMap.keys()).sort((a, b) => a.localeCompare(b));
|
|
60
|
+
for (const serviceId of serviceIds) {
|
|
61
|
+
const entry = serviceMap.get(serviceId);
|
|
62
|
+
const outgoing = (outgoingMap.get(serviceId) || []).sort((a, b) => a.targetServiceId.localeCompare(b.targetServiceId));
|
|
63
|
+
const incoming = (incomingMap.get(serviceId) || []).sort((a, b) => a.sourceServiceId.localeCompare(b.sourceServiceId));
|
|
64
|
+
const lines = [
|
|
65
|
+
`# ${serviceId}`,
|
|
66
|
+
"",
|
|
67
|
+
"## Service",
|
|
68
|
+
"",
|
|
69
|
+
`- Logical service id: \`${serviceId}\``,
|
|
70
|
+
`- Roots: ${Array.from(entry.roots).sort((a, b) => a.localeCompare(b)).map((root) => `\`${root}\``).join(", ")}`,
|
|
71
|
+
`- Endpoints discovered: ${entry.endpointCount}`,
|
|
72
|
+
`- Clients discovered: ${entry.clientCount}`,
|
|
73
|
+
`- Method interactions discovered: ${entry.methodInteractionCount}`,
|
|
74
|
+
"",
|
|
75
|
+
"## Outgoing",
|
|
76
|
+
"",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
if (outgoing.length === 0) {
|
|
80
|
+
lines.push("- None", "");
|
|
81
|
+
} else {
|
|
82
|
+
for (const edge of outgoing) {
|
|
83
|
+
const targetLink = linkToService(edge.targetServiceId, notePathByServiceId);
|
|
84
|
+
lines.push(`- ${targetLink}`);
|
|
85
|
+
for (const call of edge.calls.slice(0, 8)) {
|
|
86
|
+
lines.push(` - \`${call.httpMethod}\` \`${call.path}\``);
|
|
87
|
+
lines.push(` - client: \`${call.sourceClassName}.${call.sourceMethodName}\``);
|
|
88
|
+
if (call.provider) {
|
|
89
|
+
lines.push(` - provider: \`${call.provider.targetClassName}.${call.provider.targetMethodName}\``);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
lines.push("");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
lines.push("## Incoming", "");
|
|
97
|
+
if (incoming.length === 0) {
|
|
98
|
+
lines.push("- None", "");
|
|
99
|
+
} else {
|
|
100
|
+
for (const edge of incoming) {
|
|
101
|
+
lines.push(`- ${linkToService(edge.sourceServiceId, notePathByServiceId)}`);
|
|
102
|
+
}
|
|
103
|
+
lines.push("");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
lines.push("## Notes", "", "- Graph view shows logical service-to-service links from these markdown references.");
|
|
107
|
+
|
|
108
|
+
await fs.writeFile(
|
|
109
|
+
path.join(vaultPath, notePathByServiceId.get(serviceId)),
|
|
110
|
+
`${lines.join("\n")}\n`,
|
|
111
|
+
"utf8",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const indexLines = [
|
|
116
|
+
"# Microservice KG",
|
|
117
|
+
"",
|
|
118
|
+
`- Generated at: ${graph.generatedAt}`,
|
|
119
|
+
`- Input directory: \`${graph.inputDir}\``,
|
|
120
|
+
`- Logical services: ${serviceIds.length}`,
|
|
121
|
+
`- Service edges: ${graph.serviceEdges.length}`,
|
|
122
|
+
"",
|
|
123
|
+
"## Services",
|
|
124
|
+
"",
|
|
125
|
+
...serviceIds.map((serviceId) => `- ${linkToService(serviceId, notePathByServiceId)}`),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
await fs.writeFile(path.join(vaultPath, "INDEX.md"), `${indexLines.join("\n")}\n`, "utf8");
|
|
129
|
+
|
|
130
|
+
const corePlugins = [
|
|
131
|
+
"file-explorer",
|
|
132
|
+
"graph",
|
|
133
|
+
"markdown-importer",
|
|
134
|
+
"outline",
|
|
135
|
+
"search",
|
|
136
|
+
"switcher",
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
await fs.writeFile(
|
|
140
|
+
path.join(obsidianDir, "core-plugins.json"),
|
|
141
|
+
`${JSON.stringify(corePlugins, null, 2)}\n`,
|
|
142
|
+
"utf8",
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await fs.writeFile(
|
|
146
|
+
path.join(obsidianDir, "graph.json"),
|
|
147
|
+
`${JSON.stringify({
|
|
148
|
+
"collapse-filter": false,
|
|
149
|
+
search: "",
|
|
150
|
+
showTags: false,
|
|
151
|
+
showAttachments: false,
|
|
152
|
+
showExistingOnly: true,
|
|
153
|
+
localJumps: false,
|
|
154
|
+
colorGroups: [],
|
|
155
|
+
}, null, 2)}\n`,
|
|
156
|
+
"utf8",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await fs.writeFile(
|
|
160
|
+
path.join(obsidianDir, "workspace.json"),
|
|
161
|
+
`${JSON.stringify({
|
|
162
|
+
main: {
|
|
163
|
+
id: "main",
|
|
164
|
+
type: "split",
|
|
165
|
+
children: [
|
|
166
|
+
{
|
|
167
|
+
id: "graph-leaf",
|
|
168
|
+
type: "leaf",
|
|
169
|
+
state: {
|
|
170
|
+
type: "graph",
|
|
171
|
+
state: {},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
direction: "vertical",
|
|
176
|
+
},
|
|
177
|
+
left: {
|
|
178
|
+
id: "left",
|
|
179
|
+
type: "split",
|
|
180
|
+
children: [
|
|
181
|
+
{
|
|
182
|
+
id: "left-tabs",
|
|
183
|
+
type: "tabs",
|
|
184
|
+
children: [
|
|
185
|
+
{
|
|
186
|
+
id: "file-explorer",
|
|
187
|
+
type: "leaf",
|
|
188
|
+
state: {
|
|
189
|
+
type: "file-explorer",
|
|
190
|
+
state: {},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
direction: "horizontal",
|
|
197
|
+
width: 300,
|
|
198
|
+
},
|
|
199
|
+
right: {
|
|
200
|
+
id: "right",
|
|
201
|
+
type: "split",
|
|
202
|
+
children: [],
|
|
203
|
+
direction: "horizontal",
|
|
204
|
+
width: 300,
|
|
205
|
+
},
|
|
206
|
+
active: "graph-leaf",
|
|
207
|
+
lastOpenFiles: ["INDEX.md"],
|
|
208
|
+
}, null, 2)}\n`,
|
|
209
|
+
"utf8",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
console.log(`Created Obsidian vault at ${vaultPath}`);
|
|
213
|
+
console.log(`Logical services: ${serviceIds.length}`);
|
|
214
|
+
console.log(`Service edges: ${graph.serviceEdges.length}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function linkToService(serviceId, notePathByServiceId) {
|
|
218
|
+
const notePath = notePathByServiceId.get(serviceId);
|
|
219
|
+
const basename = path.basename(notePath, ".md");
|
|
220
|
+
return `[[${basename}]]`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
main().catch((error) => {
|
|
224
|
+
console.error(error?.stack || error?.message || String(error));
|
|
225
|
+
process.exit(1);
|
|
226
|
+
});
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { analyzeWorkspace, writeGraphArtifacts } from "./analyzer.mjs";
|
|
4
|
+
|
|
5
|
+
export class GraphStore {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.defaultGraphPath = options.defaultGraphPath || null;
|
|
8
|
+
this.graph = null;
|
|
9
|
+
this.graphPath = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async loadGraph(graphPath = this.defaultGraphPath) {
|
|
13
|
+
if (!graphPath) {
|
|
14
|
+
throw new Error("No graph path configured");
|
|
15
|
+
}
|
|
16
|
+
const resolvedPath = path.resolve(graphPath);
|
|
17
|
+
const graph = JSON.parse(await fs.readFile(resolvedPath, "utf8"));
|
|
18
|
+
this.graph = graph;
|
|
19
|
+
this.graphPath = resolvedPath;
|
|
20
|
+
return graph;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async ensureGraph() {
|
|
24
|
+
if (this.graph) {
|
|
25
|
+
return this.graph;
|
|
26
|
+
}
|
|
27
|
+
return this.loadGraph();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async analyzeAndLoad({ inputDir, outputDir }) {
|
|
31
|
+
const resolvedInput = path.resolve(inputDir);
|
|
32
|
+
const resolvedOutput = outputDir
|
|
33
|
+
? path.resolve(outputDir)
|
|
34
|
+
: path.join(resolvedInput, ".microservice-kg");
|
|
35
|
+
const graph = await analyzeWorkspace(resolvedInput);
|
|
36
|
+
await writeGraphArtifacts(graph, resolvedOutput);
|
|
37
|
+
const graphPath = path.join(resolvedOutput, "service-graph.json");
|
|
38
|
+
this.graph = graph;
|
|
39
|
+
this.graphPath = graphPath;
|
|
40
|
+
return {
|
|
41
|
+
inputDir: resolvedInput,
|
|
42
|
+
outputDir: resolvedOutput,
|
|
43
|
+
graphPath,
|
|
44
|
+
serviceCount: graph.serviceCount,
|
|
45
|
+
edgeCount: graph.serviceEdges.length,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function listServices(store, { includeStats = true } = {}) {
|
|
51
|
+
const graph = await store.ensureGraph();
|
|
52
|
+
const grouped = groupServices(graph);
|
|
53
|
+
const results = Object.values(grouped)
|
|
54
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
55
|
+
.map((service) => {
|
|
56
|
+
const base = {
|
|
57
|
+
id: service.id,
|
|
58
|
+
name: service.id,
|
|
59
|
+
roots: service.roots.sort((a, b) => a.localeCompare(b)),
|
|
60
|
+
};
|
|
61
|
+
if (!includeStats) {
|
|
62
|
+
return base;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
...base,
|
|
66
|
+
endpointCount: service.endpointCount,
|
|
67
|
+
clientCount: service.clientCount,
|
|
68
|
+
methodInteractionCount: service.methodInteractionCount,
|
|
69
|
+
outgoingServices: uniqueSorted(
|
|
70
|
+
graph.serviceEdges
|
|
71
|
+
.filter((edge) => edge.sourceServiceId === service.id)
|
|
72
|
+
.map((edge) => edge.targetServiceId),
|
|
73
|
+
),
|
|
74
|
+
incomingServices: uniqueSorted(
|
|
75
|
+
graph.serviceEdges
|
|
76
|
+
.filter((edge) => edge.targetServiceId === service.id)
|
|
77
|
+
.map((edge) => edge.sourceServiceId),
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
graphPath: store.graphPath,
|
|
84
|
+
serviceCount: results.length,
|
|
85
|
+
services: results,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function getServiceContext(store, { serviceId }) {
|
|
90
|
+
const graph = await store.ensureGraph();
|
|
91
|
+
const grouped = groupServices(graph);
|
|
92
|
+
const service = grouped[serviceId];
|
|
93
|
+
if (!service) {
|
|
94
|
+
throw new Error(`Unknown service: ${serviceId}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const outgoing = graph.serviceEdges
|
|
98
|
+
.filter((edge) => edge.sourceServiceId === serviceId)
|
|
99
|
+
.map(summarizeEdge);
|
|
100
|
+
const incoming = graph.serviceEdges
|
|
101
|
+
.filter((edge) => edge.targetServiceId === serviceId)
|
|
102
|
+
.map(summarizeEdge);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
service: {
|
|
106
|
+
id: service.id,
|
|
107
|
+
roots: service.roots.sort((a, b) => a.localeCompare(b)),
|
|
108
|
+
endpointCount: service.endpointCount,
|
|
109
|
+
clientCount: service.clientCount,
|
|
110
|
+
methodInteractionCount: service.methodInteractionCount,
|
|
111
|
+
},
|
|
112
|
+
outgoing,
|
|
113
|
+
incoming,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function getEdgeDetails(store, { sourceServiceId, targetServiceId }) {
|
|
118
|
+
const graph = await store.ensureGraph();
|
|
119
|
+
const edge = graph.serviceEdges.find(
|
|
120
|
+
(candidate) =>
|
|
121
|
+
candidate.sourceServiceId === sourceServiceId
|
|
122
|
+
&& candidate.targetServiceId === targetServiceId,
|
|
123
|
+
);
|
|
124
|
+
if (!edge) {
|
|
125
|
+
throw new Error(`Unknown edge: ${sourceServiceId} -> ${targetServiceId}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
id: edge.id,
|
|
130
|
+
sourceServiceId: edge.sourceServiceId,
|
|
131
|
+
targetServiceId: edge.targetServiceId,
|
|
132
|
+
protocol: edge.protocol,
|
|
133
|
+
reasons: edge.reasons,
|
|
134
|
+
calls: edge.calls,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function getDependencyPath(
|
|
139
|
+
store,
|
|
140
|
+
{ sourceServiceId, targetServiceId, maxDepth, direction = "downstream" },
|
|
141
|
+
) {
|
|
142
|
+
const graph = await store.ensureGraph();
|
|
143
|
+
const effectiveMaxDepth = normalizeMaxDepth(maxDepth, graph);
|
|
144
|
+
const adjacency = buildAdjacency(graph, direction);
|
|
145
|
+
const queue = [{ node: sourceServiceId, path: [sourceServiceId] }];
|
|
146
|
+
const visited = new Set([sourceServiceId]);
|
|
147
|
+
|
|
148
|
+
while (queue.length > 0) {
|
|
149
|
+
const current = queue.shift();
|
|
150
|
+
if (current.node === targetServiceId) {
|
|
151
|
+
return {
|
|
152
|
+
found: true,
|
|
153
|
+
direction,
|
|
154
|
+
path: current.path,
|
|
155
|
+
depth: current.path.length - 1,
|
|
156
|
+
maxDepth: effectiveMaxDepth,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (current.path.length - 1 >= effectiveMaxDepth) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const neighbor of adjacency.get(current.node) || []) {
|
|
165
|
+
if (visited.has(neighbor)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
visited.add(neighbor);
|
|
169
|
+
queue.push({
|
|
170
|
+
node: neighbor,
|
|
171
|
+
path: [...current.path, neighbor],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
found: false,
|
|
178
|
+
direction,
|
|
179
|
+
path: [],
|
|
180
|
+
depth: null,
|
|
181
|
+
maxDepth: effectiveMaxDepth,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function getServiceImpact(
|
|
186
|
+
store,
|
|
187
|
+
{ serviceId, direction = "downstream", maxDepth },
|
|
188
|
+
) {
|
|
189
|
+
const graph = await store.ensureGraph();
|
|
190
|
+
const effectiveMaxDepth = normalizeMaxDepth(maxDepth, graph);
|
|
191
|
+
const adjacency = buildAdjacency(graph, direction);
|
|
192
|
+
const queue = [{ node: serviceId, depth: 0 }];
|
|
193
|
+
const visited = new Set([serviceId]);
|
|
194
|
+
const results = [];
|
|
195
|
+
|
|
196
|
+
while (queue.length > 0) {
|
|
197
|
+
const current = queue.shift();
|
|
198
|
+
if (current.depth >= effectiveMaxDepth) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const neighbor of adjacency.get(current.node) || []) {
|
|
203
|
+
if (visited.has(neighbor)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
visited.add(neighbor);
|
|
207
|
+
const nextDepth = current.depth + 1;
|
|
208
|
+
results.push({
|
|
209
|
+
serviceId: neighbor,
|
|
210
|
+
depth: nextDepth,
|
|
211
|
+
});
|
|
212
|
+
queue.push({
|
|
213
|
+
node: neighbor,
|
|
214
|
+
depth: nextDepth,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
serviceId,
|
|
221
|
+
direction,
|
|
222
|
+
maxDepth: effectiveMaxDepth,
|
|
223
|
+
impactedServices: results.sort(
|
|
224
|
+
(left, right) => left.depth - right.depth || left.serviceId.localeCompare(right.serviceId),
|
|
225
|
+
),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function summarizeEdge(edge) {
|
|
230
|
+
return {
|
|
231
|
+
id: edge.id,
|
|
232
|
+
sourceServiceId: edge.sourceServiceId,
|
|
233
|
+
targetServiceId: edge.targetServiceId,
|
|
234
|
+
protocol: edge.protocol,
|
|
235
|
+
clientClasses: uniqueSorted(edge.reasons.map((reason) => reason.clientClassName)),
|
|
236
|
+
callCount: edge.calls.length,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildAdjacency(graph, direction) {
|
|
241
|
+
const adjacency = new Map();
|
|
242
|
+
const ids = uniqueSorted(graph.services.map((service) => service.id));
|
|
243
|
+
for (const id of ids) {
|
|
244
|
+
adjacency.set(id, new Set());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const edge of graph.serviceEdges) {
|
|
248
|
+
if (direction === "upstream") {
|
|
249
|
+
adjacency.get(edge.targetServiceId)?.add(edge.sourceServiceId);
|
|
250
|
+
} else {
|
|
251
|
+
adjacency.get(edge.sourceServiceId)?.add(edge.targetServiceId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return adjacency;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function groupServices(graph) {
|
|
259
|
+
const grouped = {};
|
|
260
|
+
for (const service of graph.services) {
|
|
261
|
+
if (!grouped[service.id]) {
|
|
262
|
+
grouped[service.id] = {
|
|
263
|
+
id: service.id,
|
|
264
|
+
roots: [],
|
|
265
|
+
endpointCount: 0,
|
|
266
|
+
clientCount: 0,
|
|
267
|
+
methodInteractionCount: 0,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
grouped[service.id].roots.push(service.relativeRootDir || service.rootDir || service.id);
|
|
271
|
+
grouped[service.id].endpointCount += service.endpoints?.length || 0;
|
|
272
|
+
grouped[service.id].clientCount += service.clients?.length || 0;
|
|
273
|
+
grouped[service.id].methodInteractionCount += service.methodInteractions?.length || 0;
|
|
274
|
+
}
|
|
275
|
+
return grouped;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function uniqueSorted(values) {
|
|
279
|
+
return Array.from(new Set(values.filter(Boolean))).sort((left, right) => left.localeCompare(right));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeMaxDepth(value, graph) {
|
|
283
|
+
const graphWideDepth = Math.max(1, uniqueSorted(graph.services.map((service) => service.id)).length);
|
|
284
|
+
if (value === undefined || value === null || value === "") {
|
|
285
|
+
return graphWideDepth;
|
|
286
|
+
}
|
|
287
|
+
const numericValue = Number(value);
|
|
288
|
+
if (!Number.isFinite(numericValue) || numericValue < 1) {
|
|
289
|
+
return graphWideDepth;
|
|
290
|
+
}
|
|
291
|
+
return Math.floor(numericValue);
|
|
292
|
+
}
|