@synth-deploy/server 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/dist/agent/debrief-retention.d.ts +12 -0
- package/dist/agent/debrief-retention.d.ts.map +1 -0
- package/dist/agent/debrief-retention.js +27 -0
- package/dist/agent/debrief-retention.js.map +1 -0
- package/dist/agent/envoy-client.d.ts +216 -0
- package/dist/agent/envoy-client.d.ts.map +1 -0
- package/dist/agent/envoy-client.js +266 -0
- package/dist/agent/envoy-client.js.map +1 -0
- package/dist/agent/envoy-registry.d.ts +102 -0
- package/dist/agent/envoy-registry.d.ts.map +1 -0
- package/dist/agent/envoy-registry.js +319 -0
- package/dist/agent/envoy-registry.js.map +1 -0
- package/dist/agent/health-checker.d.ts +39 -0
- package/dist/agent/health-checker.d.ts.map +1 -0
- package/dist/agent/health-checker.js +49 -0
- package/dist/agent/health-checker.js.map +1 -0
- package/dist/agent/mcp-client-manager.d.ts +36 -0
- package/dist/agent/mcp-client-manager.d.ts.map +1 -0
- package/dist/agent/mcp-client-manager.js +106 -0
- package/dist/agent/mcp-client-manager.js.map +1 -0
- package/dist/agent/stale-deployment-detector.d.ts +15 -0
- package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
- package/dist/agent/stale-deployment-detector.js +50 -0
- package/dist/agent/stale-deployment-detector.js.map +1 -0
- package/dist/agent/step-runner.d.ts +31 -0
- package/dist/agent/step-runner.d.ts.map +1 -0
- package/dist/agent/step-runner.js +80 -0
- package/dist/agent/step-runner.js.map +1 -0
- package/dist/agent/synth-agent.d.ts +168 -0
- package/dist/agent/synth-agent.d.ts.map +1 -0
- package/dist/agent/synth-agent.js +1195 -0
- package/dist/agent/synth-agent.js.map +1 -0
- package/dist/api/agent.d.ts +36 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +867 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/api-keys.d.ts +4 -0
- package/dist/api/api-keys.d.ts.map +1 -0
- package/dist/api/api-keys.js +118 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/artifacts.d.ts +5 -0
- package/dist/api/artifacts.d.ts.map +1 -0
- package/dist/api/artifacts.js +142 -0
- package/dist/api/artifacts.js.map +1 -0
- package/dist/api/auth.d.ts +4 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +280 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/deployments.d.ts +11 -0
- package/dist/api/deployments.d.ts.map +1 -0
- package/dist/api/deployments.js +1098 -0
- package/dist/api/deployments.js.map +1 -0
- package/dist/api/environments.d.ts +5 -0
- package/dist/api/environments.d.ts.map +1 -0
- package/dist/api/environments.js +69 -0
- package/dist/api/environments.js.map +1 -0
- package/dist/api/envoy-reports.d.ts +17 -0
- package/dist/api/envoy-reports.d.ts.map +1 -0
- package/dist/api/envoy-reports.js +138 -0
- package/dist/api/envoy-reports.js.map +1 -0
- package/dist/api/envoys.d.ts +5 -0
- package/dist/api/envoys.d.ts.map +1 -0
- package/dist/api/envoys.js +192 -0
- package/dist/api/envoys.js.map +1 -0
- package/dist/api/fleet.d.ts +11 -0
- package/dist/api/fleet.d.ts.map +1 -0
- package/dist/api/fleet.js +394 -0
- package/dist/api/fleet.js.map +1 -0
- package/dist/api/graph.d.ts +8 -0
- package/dist/api/graph.d.ts.map +1 -0
- package/dist/api/graph.js +355 -0
- package/dist/api/graph.js.map +1 -0
- package/dist/api/health.d.ts +20 -0
- package/dist/api/health.d.ts.map +1 -0
- package/dist/api/health.js +248 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/idp-schemas.d.ts +41 -0
- package/dist/api/idp-schemas.d.ts.map +1 -0
- package/dist/api/idp-schemas.js +17 -0
- package/dist/api/idp-schemas.js.map +1 -0
- package/dist/api/idp.d.ts +6 -0
- package/dist/api/idp.d.ts.map +1 -0
- package/dist/api/idp.js +620 -0
- package/dist/api/idp.js.map +1 -0
- package/dist/api/intake.d.ts +10 -0
- package/dist/api/intake.d.ts.map +1 -0
- package/dist/api/intake.js +418 -0
- package/dist/api/intake.js.map +1 -0
- package/dist/api/partitions.d.ts +5 -0
- package/dist/api/partitions.d.ts.map +1 -0
- package/dist/api/partitions.js +113 -0
- package/dist/api/partitions.js.map +1 -0
- package/dist/api/progress-event-store.d.ts +62 -0
- package/dist/api/progress-event-store.d.ts.map +1 -0
- package/dist/api/progress-event-store.js +118 -0
- package/dist/api/progress-event-store.js.map +1 -0
- package/dist/api/schemas.d.ts +1000 -0
- package/dist/api/schemas.d.ts.map +1 -0
- package/dist/api/schemas.js +328 -0
- package/dist/api/schemas.js.map +1 -0
- package/dist/api/security-boundaries.d.ts +4 -0
- package/dist/api/security-boundaries.d.ts.map +1 -0
- package/dist/api/security-boundaries.js +32 -0
- package/dist/api/security-boundaries.js.map +1 -0
- package/dist/api/settings.d.ts +4 -0
- package/dist/api/settings.d.ts.map +1 -0
- package/dist/api/settings.js +99 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/system.d.ts +75 -0
- package/dist/api/system.d.ts.map +1 -0
- package/dist/api/system.js +558 -0
- package/dist/api/system.js.map +1 -0
- package/dist/api/telemetry.d.ts +4 -0
- package/dist/api/telemetry.d.ts.map +1 -0
- package/dist/api/telemetry.js +24 -0
- package/dist/api/telemetry.js.map +1 -0
- package/dist/api/users.d.ts +4 -0
- package/dist/api/users.d.ts.map +1 -0
- package/dist/api/users.js +173 -0
- package/dist/api/users.js.map +1 -0
- package/dist/archive-unpacker.d.ts +24 -0
- package/dist/archive-unpacker.d.ts.map +1 -0
- package/dist/archive-unpacker.js +239 -0
- package/dist/archive-unpacker.js.map +1 -0
- package/dist/artifact-analyzer.d.ts +59 -0
- package/dist/artifact-analyzer.d.ts.map +1 -0
- package/dist/artifact-analyzer.js +334 -0
- package/dist/artifact-analyzer.js.map +1 -0
- package/dist/auth/idp/index.d.ts +9 -0
- package/dist/auth/idp/index.d.ts.map +1 -0
- package/dist/auth/idp/index.js +5 -0
- package/dist/auth/idp/index.js.map +1 -0
- package/dist/auth/idp/ldap.d.ts +56 -0
- package/dist/auth/idp/ldap.d.ts.map +1 -0
- package/dist/auth/idp/ldap.js +276 -0
- package/dist/auth/idp/ldap.js.map +1 -0
- package/dist/auth/idp/oidc.d.ts +27 -0
- package/dist/auth/idp/oidc.d.ts.map +1 -0
- package/dist/auth/idp/oidc.js +97 -0
- package/dist/auth/idp/oidc.js.map +1 -0
- package/dist/auth/idp/role-mapping.d.ts +9 -0
- package/dist/auth/idp/role-mapping.d.ts.map +1 -0
- package/dist/auth/idp/role-mapping.js +16 -0
- package/dist/auth/idp/role-mapping.js.map +1 -0
- package/dist/auth/idp/saml.d.ts +40 -0
- package/dist/auth/idp/saml.d.ts.map +1 -0
- package/dist/auth/idp/saml.js +117 -0
- package/dist/auth/idp/saml.js.map +1 -0
- package/dist/auth/idp/types.d.ts +23 -0
- package/dist/auth/idp/types.d.ts.map +1 -0
- package/dist/auth/idp/types.js +2 -0
- package/dist/auth/idp/types.js.map +1 -0
- package/dist/fleet/fleet-executor.d.ts +35 -0
- package/dist/fleet/fleet-executor.d.ts.map +1 -0
- package/dist/fleet/fleet-executor.js +228 -0
- package/dist/fleet/fleet-executor.js.map +1 -0
- package/dist/fleet/fleet-store.d.ts +13 -0
- package/dist/fleet/fleet-store.d.ts.map +1 -0
- package/dist/fleet/fleet-store.js +13 -0
- package/dist/fleet/fleet-store.js.map +1 -0
- package/dist/fleet/index.d.ts +5 -0
- package/dist/fleet/index.d.ts.map +1 -0
- package/dist/fleet/index.js +4 -0
- package/dist/fleet/index.js.map +1 -0
- package/dist/fleet/representative-selector.d.ts +15 -0
- package/dist/fleet/representative-selector.d.ts.map +1 -0
- package/dist/fleet/representative-selector.js +71 -0
- package/dist/fleet/representative-selector.js.map +1 -0
- package/dist/graph/graph-executor.d.ts +36 -0
- package/dist/graph/graph-executor.d.ts.map +1 -0
- package/dist/graph/graph-executor.js +348 -0
- package/dist/graph/graph-executor.js.map +1 -0
- package/dist/graph/graph-inference.d.ts +22 -0
- package/dist/graph/graph-inference.d.ts.map +1 -0
- package/dist/graph/graph-inference.js +149 -0
- package/dist/graph/graph-inference.js.map +1 -0
- package/dist/graph/graph-store.d.ts +12 -0
- package/dist/graph/graph-store.d.ts.map +1 -0
- package/dist/graph/graph-store.js +61 -0
- package/dist/graph/graph-store.js.map +1 -0
- package/dist/graph/index.d.ts +5 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/index.d.ts +6 -0
- package/dist/intake/index.d.ts.map +1 -0
- package/dist/intake/index.js +5 -0
- package/dist/intake/index.js.map +1 -0
- package/dist/intake/intake-processor.d.ts +17 -0
- package/dist/intake/intake-processor.d.ts.map +1 -0
- package/dist/intake/intake-processor.js +99 -0
- package/dist/intake/intake-processor.js.map +1 -0
- package/dist/intake/intake-store.d.ts +7 -0
- package/dist/intake/intake-store.d.ts.map +1 -0
- package/dist/intake/intake-store.js +7 -0
- package/dist/intake/intake-store.js.map +1 -0
- package/dist/intake/registry-poller.d.ts +41 -0
- package/dist/intake/registry-poller.d.ts.map +1 -0
- package/dist/intake/registry-poller.js +202 -0
- package/dist/intake/registry-poller.js.map +1 -0
- package/dist/intake/webhook-handlers.d.ts +37 -0
- package/dist/intake/webhook-handlers.d.ts.map +1 -0
- package/dist/intake/webhook-handlers.js +268 -0
- package/dist/intake/webhook-handlers.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +88 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/middleware/auth.d.ts +29 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +76 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/permissions.d.ts +13 -0
- package/dist/middleware/permissions.d.ts.map +1 -0
- package/dist/middleware/permissions.js +32 -0
- package/dist/middleware/permissions.js.map +1 -0
- package/dist/pattern-store.d.ts +104 -0
- package/dist/pattern-store.d.ts.map +1 -0
- package/dist/pattern-store.js +299 -0
- package/dist/pattern-store.js.map +1 -0
- package/package.json +54 -0
- package/src/agent/debrief-retention.ts +44 -0
- package/src/agent/envoy-client.ts +474 -0
- package/src/agent/envoy-registry.ts +384 -0
- package/src/agent/health-checker.ts +70 -0
- package/src/agent/mcp-client-manager.ts +131 -0
- package/src/agent/stale-deployment-detector.ts +79 -0
- package/src/agent/step-runner.ts +124 -0
- package/src/agent/synth-agent.ts +1567 -0
- package/src/api/agent.ts +1075 -0
- package/src/api/api-keys.ts +129 -0
- package/src/api/artifacts.ts +194 -0
- package/src/api/auth.ts +320 -0
- package/src/api/deployments.ts +1347 -0
- package/src/api/environments.ts +97 -0
- package/src/api/envoy-reports.ts +159 -0
- package/src/api/envoys.ts +237 -0
- package/src/api/fleet.ts +510 -0
- package/src/api/graph.ts +516 -0
- package/src/api/health.ts +311 -0
- package/src/api/idp-schemas.ts +19 -0
- package/src/api/idp.ts +735 -0
- package/src/api/intake.ts +537 -0
- package/src/api/partitions.ts +147 -0
- package/src/api/progress-event-store.ts +153 -0
- package/src/api/schemas.ts +376 -0
- package/src/api/security-boundaries.ts +54 -0
- package/src/api/settings.ts +118 -0
- package/src/api/system.ts +704 -0
- package/src/api/telemetry.ts +32 -0
- package/src/api/users.ts +210 -0
- package/src/archive-unpacker.ts +271 -0
- package/src/artifact-analyzer.ts +438 -0
- package/src/auth/idp/index.ts +8 -0
- package/src/auth/idp/ldap.ts +340 -0
- package/src/auth/idp/oidc.ts +117 -0
- package/src/auth/idp/role-mapping.ts +22 -0
- package/src/auth/idp/saml.ts +148 -0
- package/src/auth/idp/types.ts +22 -0
- package/src/fleet/fleet-executor.ts +309 -0
- package/src/fleet/fleet-store.ts +13 -0
- package/src/fleet/index.ts +4 -0
- package/src/fleet/representative-selector.ts +83 -0
- package/src/graph/graph-executor.ts +446 -0
- package/src/graph/graph-inference.ts +184 -0
- package/src/graph/graph-store.ts +75 -0
- package/src/graph/index.ts +4 -0
- package/src/index.ts +916 -0
- package/src/intake/index.ts +5 -0
- package/src/intake/intake-processor.ts +111 -0
- package/src/intake/intake-store.ts +7 -0
- package/src/intake/registry-poller.ts +230 -0
- package/src/intake/webhook-handlers.ts +328 -0
- package/src/logger.ts +19 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/server.ts +34 -0
- package/src/mcp/tools.ts +117 -0
- package/src/middleware/auth.ts +103 -0
- package/src/middleware/permissions.ts +35 -0
- package/src/pattern-store.ts +409 -0
- package/tests/agent-mode.test.ts +536 -0
- package/tests/api-handlers.test.ts +1245 -0
- package/tests/archive-unpacker.test.ts +179 -0
- package/tests/artifact-analyzer.test.ts +240 -0
- package/tests/auth-middleware.test.ts +189 -0
- package/tests/decision-diary.test.ts +957 -0
- package/tests/diary-reader.test.ts +782 -0
- package/tests/envoy-client.test.ts +342 -0
- package/tests/envoy-reports.test.ts +156 -0
- package/tests/mcp-tools.test.ts +213 -0
- package/tests/orchestration.test.ts +536 -0
- package/tests/partition-deletion.test.ts +143 -0
- package/tests/partition-isolation.test.ts +830 -0
- package/tests/pattern-store.test.ts +371 -0
- package/tests/rbac-enforcement.test.ts +409 -0
- package/tests/ssrf-validation.test.ts +56 -0
- package/tests/stale-deployment.test.ts +85 -0
- package/tests/step-runner.test.ts +308 -0
- package/tests/ui-journey.test.ts +330 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeploymentGraph,
|
|
3
|
+
DeploymentGraphNode,
|
|
4
|
+
DeploymentGraphEdge,
|
|
5
|
+
DeploymentPlan,
|
|
6
|
+
} from "@synth-deploy/core";
|
|
7
|
+
import type { EnvoyRegistry } from "../agent/envoy-registry.js";
|
|
8
|
+
import type { EnvoyClient, EnvoyDeployResult } from "../agent/envoy-client.js";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Progress events emitted during graph execution
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export interface GraphProgressEvent {
|
|
15
|
+
type:
|
|
16
|
+
| "node-started"
|
|
17
|
+
| "node-completed"
|
|
18
|
+
| "node-failed"
|
|
19
|
+
| "node-skipped"
|
|
20
|
+
| "graph-completed"
|
|
21
|
+
| "graph-failed";
|
|
22
|
+
nodeId?: string;
|
|
23
|
+
graphId: string;
|
|
24
|
+
progress: {
|
|
25
|
+
completed: number;
|
|
26
|
+
total: number;
|
|
27
|
+
executing: number;
|
|
28
|
+
failed: number;
|
|
29
|
+
};
|
|
30
|
+
outputCapture?: Record<string, string>;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Topological sort — Kahn's algorithm
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export function topologicalSort(
|
|
39
|
+
nodes: DeploymentGraphNode[],
|
|
40
|
+
edges: DeploymentGraphEdge[],
|
|
41
|
+
): string[] {
|
|
42
|
+
const inDegree = new Map<string, number>();
|
|
43
|
+
const adjacency = new Map<string, string[]>();
|
|
44
|
+
|
|
45
|
+
for (const node of nodes) {
|
|
46
|
+
inDegree.set(node.id, 0);
|
|
47
|
+
adjacency.set(node.id, []);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const edge of edges) {
|
|
51
|
+
inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
|
|
52
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const queue = nodes
|
|
56
|
+
.filter((n) => (inDegree.get(n.id) ?? 0) === 0)
|
|
57
|
+
.map((n) => n.id);
|
|
58
|
+
const result: string[] = [];
|
|
59
|
+
|
|
60
|
+
while (queue.length > 0) {
|
|
61
|
+
const current = queue.shift()!;
|
|
62
|
+
result.push(current);
|
|
63
|
+
|
|
64
|
+
for (const neighbor of adjacency.get(current) ?? []) {
|
|
65
|
+
const deg = (inDegree.get(neighbor) ?? 1) - 1;
|
|
66
|
+
inDegree.set(neighbor, deg);
|
|
67
|
+
if (deg === 0) queue.push(neighbor);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (result.length !== nodes.length) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Cycle detected in deployment graph — topological sort is impossible",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Depth computation — group nodes by distance from roots for parallel exec
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export function computeDepths(
|
|
85
|
+
nodes: DeploymentGraphNode[],
|
|
86
|
+
edges: DeploymentGraphEdge[],
|
|
87
|
+
): Map<string, number> {
|
|
88
|
+
const depths = new Map<string, number>();
|
|
89
|
+
const inEdges = new Map<string, string[]>(); // nodeId -> list of "from" nodeIds
|
|
90
|
+
for (const node of nodes) {
|
|
91
|
+
depths.set(node.id, 0);
|
|
92
|
+
inEdges.set(node.id, []);
|
|
93
|
+
}
|
|
94
|
+
for (const edge of edges) {
|
|
95
|
+
inEdges.get(edge.to)?.push(edge.from);
|
|
96
|
+
}
|
|
97
|
+
// BFS to compute max depth
|
|
98
|
+
let changed = true;
|
|
99
|
+
while (changed) {
|
|
100
|
+
changed = false;
|
|
101
|
+
for (const node of nodes) {
|
|
102
|
+
const parents = inEdges.get(node.id) ?? [];
|
|
103
|
+
const maxParent = Math.max(0, ...parents.map((p) => depths.get(p) ?? 0));
|
|
104
|
+
const newDepth = parents.length > 0 ? maxParent + 1 : 0;
|
|
105
|
+
if (newDepth > (depths.get(node.id) ?? 0)) {
|
|
106
|
+
depths.set(node.id, newDepth);
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return depths;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Downstream node computation — find all transitive dependents of a node
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
function getDownstreamNodeIds(
|
|
119
|
+
nodeId: string,
|
|
120
|
+
edges: DeploymentGraphEdge[],
|
|
121
|
+
): Set<string> {
|
|
122
|
+
const downstream = new Set<string>();
|
|
123
|
+
const adjacency = new Map<string, string[]>();
|
|
124
|
+
for (const edge of edges) {
|
|
125
|
+
if (!adjacency.has(edge.from)) adjacency.set(edge.from, []);
|
|
126
|
+
adjacency.get(edge.from)!.push(edge.to);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const queue = [nodeId];
|
|
130
|
+
while (queue.length > 0) {
|
|
131
|
+
const current = queue.shift()!;
|
|
132
|
+
for (const child of adjacency.get(current) ?? []) {
|
|
133
|
+
if (!downstream.has(child)) {
|
|
134
|
+
downstream.add(child);
|
|
135
|
+
queue.push(child);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return downstream;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// GraphExecutor — depth-based parallel execution
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
export class GraphExecutor {
|
|
147
|
+
constructor(
|
|
148
|
+
private envoyRegistry: EnvoyRegistry,
|
|
149
|
+
private createClient: (url: string, timeoutMs: number) => EnvoyClient,
|
|
150
|
+
) {}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Execute a deployment graph with depth-based parallelism.
|
|
154
|
+
* Nodes at the same depth run concurrently via Promise.allSettled().
|
|
155
|
+
* If a node fails, all its downstream dependents are skipped,
|
|
156
|
+
* but sibling nodes at the same depth continue executing.
|
|
157
|
+
*/
|
|
158
|
+
async *execute(
|
|
159
|
+
graph: DeploymentGraph,
|
|
160
|
+
plans: Map<string, DeploymentPlan>,
|
|
161
|
+
partitionVariables?: Record<string, string>,
|
|
162
|
+
): AsyncGenerator<GraphProgressEvent> {
|
|
163
|
+
// Validate topological order (detects cycles)
|
|
164
|
+
topologicalSort(graph.nodes, graph.edges);
|
|
165
|
+
|
|
166
|
+
const depths = computeDepths(graph.nodes, graph.edges);
|
|
167
|
+
const completed = new Map<string, Record<string, string>>();
|
|
168
|
+
let completedCount = 0;
|
|
169
|
+
let failedCount = 0;
|
|
170
|
+
const skippedNodes = new Set<string>();
|
|
171
|
+
|
|
172
|
+
// Group nodes by depth
|
|
173
|
+
const maxDepth = Math.max(0, ...Array.from(depths.values()));
|
|
174
|
+
const depthGroups: DeploymentGraphNode[][] = [];
|
|
175
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
176
|
+
depthGroups.push(
|
|
177
|
+
graph.nodes.filter((n) => (depths.get(n.id) ?? 0) === d),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const group of depthGroups) {
|
|
182
|
+
// Filter out nodes that should be skipped (downstream of failed nodes)
|
|
183
|
+
const executableNodes = group.filter((n) => !skippedNodes.has(n.id));
|
|
184
|
+
|
|
185
|
+
if (executableNodes.length === 0) continue;
|
|
186
|
+
|
|
187
|
+
// Collect events from parallel execution, then yield them after
|
|
188
|
+
const levelEvents: GraphProgressEvent[] = [];
|
|
189
|
+
const executingCount = executableNodes.length;
|
|
190
|
+
|
|
191
|
+
// Execute all nodes at this depth concurrently
|
|
192
|
+
const results = await Promise.allSettled(
|
|
193
|
+
executableNodes.map(async (node) => {
|
|
194
|
+
// Resolve input bindings from completed upstream outputs
|
|
195
|
+
const resolvedVars: Record<string, string> = {};
|
|
196
|
+
|
|
197
|
+
// Merge partition variables first (input bindings override)
|
|
198
|
+
if (partitionVariables) {
|
|
199
|
+
Object.assign(resolvedVars, partitionVariables);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const binding of node.inputBindings ?? []) {
|
|
203
|
+
const upstreamOutputs = completed.get(binding.sourceNodeId);
|
|
204
|
+
if (upstreamOutputs?.[binding.sourceOutputName]) {
|
|
205
|
+
resolvedVars[binding.variable] =
|
|
206
|
+
upstreamOutputs[binding.sourceOutputName];
|
|
207
|
+
binding.resolvedValue =
|
|
208
|
+
upstreamOutputs[binding.sourceOutputName];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
levelEvents.push({
|
|
213
|
+
type: "node-started",
|
|
214
|
+
nodeId: node.id,
|
|
215
|
+
graphId: graph.id,
|
|
216
|
+
progress: {
|
|
217
|
+
completed: completedCount,
|
|
218
|
+
total: graph.nodes.length,
|
|
219
|
+
executing: executingCount,
|
|
220
|
+
failed: failedCount,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const entry = this.envoyRegistry.get(node.envoyId);
|
|
225
|
+
if (!entry) {
|
|
226
|
+
throw new Error(`Envoy not found: ${node.envoyId}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const plan = plans.get(node.id);
|
|
230
|
+
if (!plan) {
|
|
231
|
+
throw new Error(`No plan found for node: ${node.id}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const client = this.createClient(entry.url, 60_000);
|
|
235
|
+
|
|
236
|
+
// Inject resolved variables into the plan's reasoning for traceability
|
|
237
|
+
const enrichedPlan: DeploymentPlan =
|
|
238
|
+
Object.keys(resolvedVars).length > 0
|
|
239
|
+
? {
|
|
240
|
+
...plan,
|
|
241
|
+
reasoning: `${plan.reasoning}\n\nResolved variables from upstream: ${JSON.stringify(resolvedVars)}`,
|
|
242
|
+
}
|
|
243
|
+
: plan;
|
|
244
|
+
|
|
245
|
+
const result: EnvoyDeployResult = await client.executeApprovedPlan({
|
|
246
|
+
deploymentId: node.deploymentId ?? node.id,
|
|
247
|
+
plan: enrichedPlan,
|
|
248
|
+
rollbackPlan: {
|
|
249
|
+
steps: [],
|
|
250
|
+
reasoning: "No rollback plan provided",
|
|
251
|
+
},
|
|
252
|
+
artifactType: "graph-node",
|
|
253
|
+
artifactName: node.artifactId,
|
|
254
|
+
environmentId: "",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Capture outputs from step results
|
|
258
|
+
const outputs: Record<string, string> = {};
|
|
259
|
+
for (const binding of node.outputBindings ?? []) {
|
|
260
|
+
if (
|
|
261
|
+
binding.source === "plan_step_output" &&
|
|
262
|
+
binding.stepIndex != null &&
|
|
263
|
+
binding.outputKey
|
|
264
|
+
) {
|
|
265
|
+
const stepResult = result.debriefEntries?.[binding.stepIndex];
|
|
266
|
+
if (stepResult) {
|
|
267
|
+
outputs[binding.name] = String(
|
|
268
|
+
(stepResult.context as Record<string, unknown>)?.[
|
|
269
|
+
binding.outputKey
|
|
270
|
+
] ?? "",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
} else if (binding.source === "manual" && binding.value) {
|
|
274
|
+
outputs[binding.name] = binding.value;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { nodeId: node.id, outputs };
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Process results: update state and collect events
|
|
283
|
+
for (let i = 0; i < results.length; i++) {
|
|
284
|
+
const result = results[i];
|
|
285
|
+
const node = executableNodes[i];
|
|
286
|
+
|
|
287
|
+
if (result.status === "fulfilled") {
|
|
288
|
+
completed.set(result.value.nodeId, result.value.outputs);
|
|
289
|
+
completedCount++;
|
|
290
|
+
|
|
291
|
+
levelEvents.push({
|
|
292
|
+
type: "node-completed",
|
|
293
|
+
nodeId: node.id,
|
|
294
|
+
graphId: graph.id,
|
|
295
|
+
outputCapture: result.value.outputs,
|
|
296
|
+
progress: {
|
|
297
|
+
completed: completedCount,
|
|
298
|
+
total: graph.nodes.length,
|
|
299
|
+
executing: 0,
|
|
300
|
+
failed: failedCount,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
failedCount++;
|
|
305
|
+
const message =
|
|
306
|
+
result.reason instanceof Error
|
|
307
|
+
? result.reason.message
|
|
308
|
+
: String(result.reason);
|
|
309
|
+
|
|
310
|
+
levelEvents.push({
|
|
311
|
+
type: "node-failed",
|
|
312
|
+
nodeId: node.id,
|
|
313
|
+
graphId: graph.id,
|
|
314
|
+
error: message,
|
|
315
|
+
progress: {
|
|
316
|
+
completed: completedCount,
|
|
317
|
+
total: graph.nodes.length,
|
|
318
|
+
executing: 0,
|
|
319
|
+
failed: failedCount,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Mark all downstream nodes as skipped
|
|
324
|
+
const downstream = getDownstreamNodeIds(node.id, graph.edges);
|
|
325
|
+
for (const downId of downstream) {
|
|
326
|
+
skippedNodes.add(downId);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Yield all events from this depth level
|
|
332
|
+
for (const event of levelEvents) {
|
|
333
|
+
yield event;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Yield skipped events for nodes in future depth levels that were just marked
|
|
337
|
+
for (const node of graph.nodes) {
|
|
338
|
+
if (skippedNodes.has(node.id) && !completed.has(node.id)) {
|
|
339
|
+
// Only yield skipped event once — remove from set tracking after yield
|
|
340
|
+
// We'll check depth to avoid yielding for nodes not yet reached
|
|
341
|
+
const nodeDepth = depths.get(node.id) ?? 0;
|
|
342
|
+
const currentDepth = depths.get(group[0].id) ?? 0;
|
|
343
|
+
if (nodeDepth === currentDepth + 1) {
|
|
344
|
+
// Don't yield yet — will be handled when we reach that depth level
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Yield skip events for any remaining skipped nodes
|
|
351
|
+
for (const nodeId of skippedNodes) {
|
|
352
|
+
if (!completed.has(nodeId)) {
|
|
353
|
+
yield {
|
|
354
|
+
type: "node-skipped",
|
|
355
|
+
nodeId,
|
|
356
|
+
graphId: graph.id,
|
|
357
|
+
error: "Skipped due to upstream failure",
|
|
358
|
+
progress: {
|
|
359
|
+
completed: completedCount,
|
|
360
|
+
total: graph.nodes.length,
|
|
361
|
+
executing: 0,
|
|
362
|
+
failed: failedCount,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (failedCount === 0) {
|
|
369
|
+
yield {
|
|
370
|
+
type: "graph-completed",
|
|
371
|
+
graphId: graph.id,
|
|
372
|
+
progress: {
|
|
373
|
+
completed: completedCount,
|
|
374
|
+
total: graph.nodes.length,
|
|
375
|
+
executing: 0,
|
|
376
|
+
failed: 0,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
} else {
|
|
380
|
+
yield {
|
|
381
|
+
type: "graph-failed",
|
|
382
|
+
graphId: graph.id,
|
|
383
|
+
progress: {
|
|
384
|
+
completed: completedCount,
|
|
385
|
+
total: graph.nodes.length,
|
|
386
|
+
executing: 0,
|
|
387
|
+
failed: failedCount,
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Rollback completed nodes in reverse topological order.
|
|
395
|
+
* Only rolls back nodes that completed successfully.
|
|
396
|
+
*/
|
|
397
|
+
async *rollback(
|
|
398
|
+
graph: DeploymentGraph,
|
|
399
|
+
rollbackPlans: Map<string, DeploymentPlan>,
|
|
400
|
+
): AsyncGenerator<GraphProgressEvent> {
|
|
401
|
+
const sorted = topologicalSort(graph.nodes, graph.edges).reverse();
|
|
402
|
+
let rolledBack = 0;
|
|
403
|
+
let rollbackFailed = 0;
|
|
404
|
+
|
|
405
|
+
for (const nodeId of sorted) {
|
|
406
|
+
const node = graph.nodes.find((n) => n.id === nodeId)!;
|
|
407
|
+
if (node.status !== "completed") continue;
|
|
408
|
+
|
|
409
|
+
const plan = rollbackPlans.get(nodeId);
|
|
410
|
+
if (!plan) continue;
|
|
411
|
+
|
|
412
|
+
const entry = this.envoyRegistry.get(node.envoyId);
|
|
413
|
+
if (!entry) continue;
|
|
414
|
+
|
|
415
|
+
const client = this.createClient(entry.url, 60_000);
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
await client.executeApprovedPlan({
|
|
419
|
+
deploymentId: node.deploymentId ?? nodeId,
|
|
420
|
+
plan,
|
|
421
|
+
rollbackPlan: {
|
|
422
|
+
steps: [],
|
|
423
|
+
reasoning: "Rollback of rollback not supported",
|
|
424
|
+
},
|
|
425
|
+
artifactType: "graph-node-rollback",
|
|
426
|
+
artifactName: node.artifactId,
|
|
427
|
+
environmentId: "",
|
|
428
|
+
});
|
|
429
|
+
rolledBack++;
|
|
430
|
+
} catch {
|
|
431
|
+
rollbackFailed++;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
yield {
|
|
436
|
+
type: rollbackFailed === 0 ? "graph-completed" : "graph-failed",
|
|
437
|
+
graphId: graph.id,
|
|
438
|
+
progress: {
|
|
439
|
+
completed: rolledBack,
|
|
440
|
+
total: graph.nodes.filter((n) => n.status === "completed").length,
|
|
441
|
+
executing: 0,
|
|
442
|
+
failed: rollbackFailed,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type {
|
|
4
|
+
DeploymentGraph,
|
|
5
|
+
DeploymentGraphNode,
|
|
6
|
+
DeploymentGraphEdge,
|
|
7
|
+
} from "@synth-deploy/core";
|
|
8
|
+
import type { LlmClient, LlmResult, IArtifactStore } from "@synth-deploy/core";
|
|
9
|
+
import { sanitizeForPrompt } from "@synth-deploy/core";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Zod schema for LLM graph inference response validation
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const InferredEdgeSchema = z.object({
|
|
16
|
+
from: z.string(),
|
|
17
|
+
to: z.string(),
|
|
18
|
+
type: z.enum(["depends_on", "data_flow"]),
|
|
19
|
+
dataBinding: z.object({
|
|
20
|
+
outputName: z.string(),
|
|
21
|
+
inputVariable: z.string(),
|
|
22
|
+
}).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const GraphInferenceResponseSchema = z.object({
|
|
26
|
+
edges: z.array(InferredEdgeSchema),
|
|
27
|
+
reasoning: z.string().optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// GraphInferenceEngine — uses LLM to reason about deployment ordering
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
interface InferGraphParams {
|
|
35
|
+
artifactIds: string[];
|
|
36
|
+
envoyAssignments: Record<string, string>; // artifactId -> envoyId
|
|
37
|
+
partitionId?: string;
|
|
38
|
+
graphName?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const GRAPH_INFERENCE_SYSTEM_PROMPT = `You are a deployment orchestration expert. Given a set of deployment artifacts with their analyses, determine the correct execution order and any data flow between them.
|
|
42
|
+
|
|
43
|
+
Your response must be valid JSON with this structure:
|
|
44
|
+
{
|
|
45
|
+
"edges": [
|
|
46
|
+
{
|
|
47
|
+
"from": "<artifactId that must deploy FIRST>",
|
|
48
|
+
"to": "<artifactId that depends on it>",
|
|
49
|
+
"type": "depends_on" | "data_flow",
|
|
50
|
+
"dataBinding": { "outputName": "<name>", "inputVariable": "<var>" } // only for data_flow edges
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"reasoning": "Plain-language explanation of why this ordering was chosen."
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Rules:
|
|
57
|
+
- "from" deploys BEFORE "to"
|
|
58
|
+
- Only add edges where there is a genuine dependency (shared database, API dependency, config requirement)
|
|
59
|
+
- Use "data_flow" when one artifact produces a value (e.g., a URL, port, hostname) that another needs
|
|
60
|
+
- Use "depends_on" for ordering-only dependencies (e.g., database must be up before the app)
|
|
61
|
+
- Do not create cycles
|
|
62
|
+
- If artifacts are independent, return an empty edges array`;
|
|
63
|
+
|
|
64
|
+
export class GraphInferenceEngine {
|
|
65
|
+
constructor(
|
|
66
|
+
private llm: LlmClient,
|
|
67
|
+
private artifactStore: IArtifactStore,
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Infer a deployment graph from a set of artifacts and their envoy assignments.
|
|
72
|
+
* Uses the LLM to reason about ordering and data flow when available.
|
|
73
|
+
* Falls back to a flat graph (all parallel) when LLM is unavailable.
|
|
74
|
+
*/
|
|
75
|
+
async inferGraph(params: InferGraphParams): Promise<DeploymentGraph> {
|
|
76
|
+
const { artifactIds, envoyAssignments, partitionId, graphName } = params;
|
|
77
|
+
const now = new Date();
|
|
78
|
+
|
|
79
|
+
// Build nodes from artifact/envoy assignments
|
|
80
|
+
const nodes: DeploymentGraphNode[] = artifactIds.map((artifactId) => ({
|
|
81
|
+
id: crypto.randomUUID(),
|
|
82
|
+
artifactId,
|
|
83
|
+
envoyId: envoyAssignments[artifactId] ?? "",
|
|
84
|
+
outputBindings: [],
|
|
85
|
+
inputBindings: [],
|
|
86
|
+
status: "pending" as const,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// Map from artifactId to nodeId for edge resolution
|
|
90
|
+
const artifactToNodeId = new Map<string, string>();
|
|
91
|
+
for (const node of nodes) {
|
|
92
|
+
artifactToNodeId.set(node.artifactId, node.id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let edges: DeploymentGraphEdge[] = [];
|
|
96
|
+
|
|
97
|
+
// Attempt LLM inference
|
|
98
|
+
if (this.llm.isAvailable() && artifactIds.length > 1) {
|
|
99
|
+
edges = await this._inferEdgesWithLlm(artifactIds, artifactToNodeId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
id: crypto.randomUUID(),
|
|
104
|
+
name: graphName ?? `Graph ${now.toISOString().slice(0, 19)}`,
|
|
105
|
+
partitionId,
|
|
106
|
+
nodes,
|
|
107
|
+
edges,
|
|
108
|
+
status: "draft",
|
|
109
|
+
approvalMode: "graph",
|
|
110
|
+
createdAt: now,
|
|
111
|
+
updatedAt: now,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async _inferEdgesWithLlm(
|
|
116
|
+
artifactIds: string[],
|
|
117
|
+
artifactToNodeId: Map<string, string>,
|
|
118
|
+
): Promise<DeploymentGraphEdge[]> {
|
|
119
|
+
// Build context from artifact analyses
|
|
120
|
+
const artifactContext: string[] = [];
|
|
121
|
+
for (const artifactId of artifactIds) {
|
|
122
|
+
const artifact = this.artifactStore.get(artifactId);
|
|
123
|
+
if (!artifact) {
|
|
124
|
+
artifactContext.push(`- ${artifactId}: (artifact not found)`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
artifactContext.push(
|
|
129
|
+
`- ID: ${artifactId}\n` +
|
|
130
|
+
` Name: ${sanitizeForPrompt(artifact.name)}\n` +
|
|
131
|
+
` Type: ${sanitizeForPrompt(artifact.type)}\n` +
|
|
132
|
+
` Summary: ${sanitizeForPrompt(artifact.analysis.summary)}\n` +
|
|
133
|
+
` Dependencies: ${sanitizeForPrompt(JSON.stringify(artifact.analysis.dependencies))}\n` +
|
|
134
|
+
` Config expectations: ${sanitizeForPrompt(JSON.stringify(artifact.analysis.configurationExpectations))}\n` +
|
|
135
|
+
` Deployment intent: ${sanitizeForPrompt(artifact.analysis.deploymentIntent ?? "unknown")}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const prompt = `Determine the deployment ordering for these artifacts:\n\n${artifactContext.join("\n\n")}\n\nArtifact IDs to use in edges: ${JSON.stringify(artifactIds)}`;
|
|
140
|
+
|
|
141
|
+
let result: LlmResult;
|
|
142
|
+
try {
|
|
143
|
+
result = await this.llm.reason({
|
|
144
|
+
prompt,
|
|
145
|
+
systemPrompt: GRAPH_INFERENCE_SYSTEM_PROMPT,
|
|
146
|
+
promptSummary: `Graph inference for ${artifactIds.length} artifacts`,
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!result.ok) return [];
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const jsonMatch = result.text.match(/\{[\s\S]*\}/);
|
|
156
|
+
if (!jsonMatch) return [];
|
|
157
|
+
|
|
158
|
+
const raw = JSON.parse(jsonMatch[0]);
|
|
159
|
+
const parseResult = GraphInferenceResponseSchema.safeParse(raw);
|
|
160
|
+
if (!parseResult.success) return [];
|
|
161
|
+
|
|
162
|
+
const parsed = parseResult.data;
|
|
163
|
+
|
|
164
|
+
// Convert artifact-level edges to node-level edges
|
|
165
|
+
const graphEdges: DeploymentGraphEdge[] = [];
|
|
166
|
+
for (const edge of parsed.edges) {
|
|
167
|
+
const fromNodeId = artifactToNodeId.get(edge.from);
|
|
168
|
+
const toNodeId = artifactToNodeId.get(edge.to);
|
|
169
|
+
if (!fromNodeId || !toNodeId) continue;
|
|
170
|
+
|
|
171
|
+
graphEdges.push({
|
|
172
|
+
from: fromNodeId,
|
|
173
|
+
to: toNodeId,
|
|
174
|
+
type: edge.type === "data_flow" ? "data_flow" : "depends_on",
|
|
175
|
+
dataBinding: edge.dataBinding,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return graphEdges;
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeploymentGraph,
|
|
3
|
+
DeploymentGraphStatus,
|
|
4
|
+
DeploymentGraphNode,
|
|
5
|
+
} from "@synth-deploy/core";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// DeploymentGraphStore — in-memory store for deployment graphs
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export class DeploymentGraphStore {
|
|
12
|
+
private graphs = new Map<string, DeploymentGraph>();
|
|
13
|
+
|
|
14
|
+
create(graph: DeploymentGraph): DeploymentGraph {
|
|
15
|
+
this.graphs.set(graph.id, graph);
|
|
16
|
+
return graph;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getById(id: string): DeploymentGraph | undefined {
|
|
20
|
+
return this.graphs.get(id);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
update(
|
|
24
|
+
id: string,
|
|
25
|
+
updates: Partial<Pick<DeploymentGraph, "name" | "nodes" | "edges" | "status" | "approvalMode" | "partitionId">>,
|
|
26
|
+
): DeploymentGraph | undefined {
|
|
27
|
+
const existing = this.graphs.get(id);
|
|
28
|
+
if (!existing) return undefined;
|
|
29
|
+
|
|
30
|
+
if (updates.name !== undefined) existing.name = updates.name;
|
|
31
|
+
if (updates.nodes !== undefined) existing.nodes = updates.nodes;
|
|
32
|
+
if (updates.edges !== undefined) existing.edges = updates.edges;
|
|
33
|
+
if (updates.status !== undefined) existing.status = updates.status;
|
|
34
|
+
if (updates.approvalMode !== undefined) existing.approvalMode = updates.approvalMode;
|
|
35
|
+
if (updates.partitionId !== undefined) existing.partitionId = updates.partitionId;
|
|
36
|
+
existing.updatedAt = new Date();
|
|
37
|
+
|
|
38
|
+
return existing;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
updateStatus(id: string, status: DeploymentGraphStatus): DeploymentGraph | undefined {
|
|
42
|
+
const existing = this.graphs.get(id);
|
|
43
|
+
if (!existing) return undefined;
|
|
44
|
+
|
|
45
|
+
existing.status = status;
|
|
46
|
+
existing.updatedAt = new Date();
|
|
47
|
+
return existing;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
updateNode(
|
|
51
|
+
graphId: string,
|
|
52
|
+
nodeId: string,
|
|
53
|
+
updates: Partial<Pick<DeploymentGraphNode, "status" | "deploymentId">>,
|
|
54
|
+
): DeploymentGraphNode | undefined {
|
|
55
|
+
const graph = this.graphs.get(graphId);
|
|
56
|
+
if (!graph) return undefined;
|
|
57
|
+
|
|
58
|
+
const node = graph.nodes.find((n) => n.id === nodeId);
|
|
59
|
+
if (!node) return undefined;
|
|
60
|
+
|
|
61
|
+
if (updates.status !== undefined) node.status = updates.status;
|
|
62
|
+
if (updates.deploymentId !== undefined) node.deploymentId = updates.deploymentId;
|
|
63
|
+
graph.updatedAt = new Date();
|
|
64
|
+
|
|
65
|
+
return node;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
list(): DeploymentGraph[] {
|
|
69
|
+
return Array.from(this.graphs.values());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
delete(id: string): boolean {
|
|
73
|
+
return this.graphs.delete(id);
|
|
74
|
+
}
|
|
75
|
+
}
|