@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.
Files changed (317) hide show
  1. package/dist/agent/debrief-retention.d.ts +12 -0
  2. package/dist/agent/debrief-retention.d.ts.map +1 -0
  3. package/dist/agent/debrief-retention.js +27 -0
  4. package/dist/agent/debrief-retention.js.map +1 -0
  5. package/dist/agent/envoy-client.d.ts +216 -0
  6. package/dist/agent/envoy-client.d.ts.map +1 -0
  7. package/dist/agent/envoy-client.js +266 -0
  8. package/dist/agent/envoy-client.js.map +1 -0
  9. package/dist/agent/envoy-registry.d.ts +102 -0
  10. package/dist/agent/envoy-registry.d.ts.map +1 -0
  11. package/dist/agent/envoy-registry.js +319 -0
  12. package/dist/agent/envoy-registry.js.map +1 -0
  13. package/dist/agent/health-checker.d.ts +39 -0
  14. package/dist/agent/health-checker.d.ts.map +1 -0
  15. package/dist/agent/health-checker.js +49 -0
  16. package/dist/agent/health-checker.js.map +1 -0
  17. package/dist/agent/mcp-client-manager.d.ts +36 -0
  18. package/dist/agent/mcp-client-manager.d.ts.map +1 -0
  19. package/dist/agent/mcp-client-manager.js +106 -0
  20. package/dist/agent/mcp-client-manager.js.map +1 -0
  21. package/dist/agent/stale-deployment-detector.d.ts +15 -0
  22. package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
  23. package/dist/agent/stale-deployment-detector.js +50 -0
  24. package/dist/agent/stale-deployment-detector.js.map +1 -0
  25. package/dist/agent/step-runner.d.ts +31 -0
  26. package/dist/agent/step-runner.d.ts.map +1 -0
  27. package/dist/agent/step-runner.js +80 -0
  28. package/dist/agent/step-runner.js.map +1 -0
  29. package/dist/agent/synth-agent.d.ts +168 -0
  30. package/dist/agent/synth-agent.d.ts.map +1 -0
  31. package/dist/agent/synth-agent.js +1195 -0
  32. package/dist/agent/synth-agent.js.map +1 -0
  33. package/dist/api/agent.d.ts +36 -0
  34. package/dist/api/agent.d.ts.map +1 -0
  35. package/dist/api/agent.js +867 -0
  36. package/dist/api/agent.js.map +1 -0
  37. package/dist/api/api-keys.d.ts +4 -0
  38. package/dist/api/api-keys.d.ts.map +1 -0
  39. package/dist/api/api-keys.js +118 -0
  40. package/dist/api/api-keys.js.map +1 -0
  41. package/dist/api/artifacts.d.ts +5 -0
  42. package/dist/api/artifacts.d.ts.map +1 -0
  43. package/dist/api/artifacts.js +142 -0
  44. package/dist/api/artifacts.js.map +1 -0
  45. package/dist/api/auth.d.ts +4 -0
  46. package/dist/api/auth.d.ts.map +1 -0
  47. package/dist/api/auth.js +280 -0
  48. package/dist/api/auth.js.map +1 -0
  49. package/dist/api/deployments.d.ts +11 -0
  50. package/dist/api/deployments.d.ts.map +1 -0
  51. package/dist/api/deployments.js +1098 -0
  52. package/dist/api/deployments.js.map +1 -0
  53. package/dist/api/environments.d.ts +5 -0
  54. package/dist/api/environments.d.ts.map +1 -0
  55. package/dist/api/environments.js +69 -0
  56. package/dist/api/environments.js.map +1 -0
  57. package/dist/api/envoy-reports.d.ts +17 -0
  58. package/dist/api/envoy-reports.d.ts.map +1 -0
  59. package/dist/api/envoy-reports.js +138 -0
  60. package/dist/api/envoy-reports.js.map +1 -0
  61. package/dist/api/envoys.d.ts +5 -0
  62. package/dist/api/envoys.d.ts.map +1 -0
  63. package/dist/api/envoys.js +192 -0
  64. package/dist/api/envoys.js.map +1 -0
  65. package/dist/api/fleet.d.ts +11 -0
  66. package/dist/api/fleet.d.ts.map +1 -0
  67. package/dist/api/fleet.js +394 -0
  68. package/dist/api/fleet.js.map +1 -0
  69. package/dist/api/graph.d.ts +8 -0
  70. package/dist/api/graph.d.ts.map +1 -0
  71. package/dist/api/graph.js +355 -0
  72. package/dist/api/graph.js.map +1 -0
  73. package/dist/api/health.d.ts +20 -0
  74. package/dist/api/health.d.ts.map +1 -0
  75. package/dist/api/health.js +248 -0
  76. package/dist/api/health.js.map +1 -0
  77. package/dist/api/idp-schemas.d.ts +41 -0
  78. package/dist/api/idp-schemas.d.ts.map +1 -0
  79. package/dist/api/idp-schemas.js +17 -0
  80. package/dist/api/idp-schemas.js.map +1 -0
  81. package/dist/api/idp.d.ts +6 -0
  82. package/dist/api/idp.d.ts.map +1 -0
  83. package/dist/api/idp.js +620 -0
  84. package/dist/api/idp.js.map +1 -0
  85. package/dist/api/intake.d.ts +10 -0
  86. package/dist/api/intake.d.ts.map +1 -0
  87. package/dist/api/intake.js +418 -0
  88. package/dist/api/intake.js.map +1 -0
  89. package/dist/api/partitions.d.ts +5 -0
  90. package/dist/api/partitions.d.ts.map +1 -0
  91. package/dist/api/partitions.js +113 -0
  92. package/dist/api/partitions.js.map +1 -0
  93. package/dist/api/progress-event-store.d.ts +62 -0
  94. package/dist/api/progress-event-store.d.ts.map +1 -0
  95. package/dist/api/progress-event-store.js +118 -0
  96. package/dist/api/progress-event-store.js.map +1 -0
  97. package/dist/api/schemas.d.ts +1000 -0
  98. package/dist/api/schemas.d.ts.map +1 -0
  99. package/dist/api/schemas.js +328 -0
  100. package/dist/api/schemas.js.map +1 -0
  101. package/dist/api/security-boundaries.d.ts +4 -0
  102. package/dist/api/security-boundaries.d.ts.map +1 -0
  103. package/dist/api/security-boundaries.js +32 -0
  104. package/dist/api/security-boundaries.js.map +1 -0
  105. package/dist/api/settings.d.ts +4 -0
  106. package/dist/api/settings.d.ts.map +1 -0
  107. package/dist/api/settings.js +99 -0
  108. package/dist/api/settings.js.map +1 -0
  109. package/dist/api/system.d.ts +75 -0
  110. package/dist/api/system.d.ts.map +1 -0
  111. package/dist/api/system.js +558 -0
  112. package/dist/api/system.js.map +1 -0
  113. package/dist/api/telemetry.d.ts +4 -0
  114. package/dist/api/telemetry.d.ts.map +1 -0
  115. package/dist/api/telemetry.js +24 -0
  116. package/dist/api/telemetry.js.map +1 -0
  117. package/dist/api/users.d.ts +4 -0
  118. package/dist/api/users.d.ts.map +1 -0
  119. package/dist/api/users.js +173 -0
  120. package/dist/api/users.js.map +1 -0
  121. package/dist/archive-unpacker.d.ts +24 -0
  122. package/dist/archive-unpacker.d.ts.map +1 -0
  123. package/dist/archive-unpacker.js +239 -0
  124. package/dist/archive-unpacker.js.map +1 -0
  125. package/dist/artifact-analyzer.d.ts +59 -0
  126. package/dist/artifact-analyzer.d.ts.map +1 -0
  127. package/dist/artifact-analyzer.js +334 -0
  128. package/dist/artifact-analyzer.js.map +1 -0
  129. package/dist/auth/idp/index.d.ts +9 -0
  130. package/dist/auth/idp/index.d.ts.map +1 -0
  131. package/dist/auth/idp/index.js +5 -0
  132. package/dist/auth/idp/index.js.map +1 -0
  133. package/dist/auth/idp/ldap.d.ts +56 -0
  134. package/dist/auth/idp/ldap.d.ts.map +1 -0
  135. package/dist/auth/idp/ldap.js +276 -0
  136. package/dist/auth/idp/ldap.js.map +1 -0
  137. package/dist/auth/idp/oidc.d.ts +27 -0
  138. package/dist/auth/idp/oidc.d.ts.map +1 -0
  139. package/dist/auth/idp/oidc.js +97 -0
  140. package/dist/auth/idp/oidc.js.map +1 -0
  141. package/dist/auth/idp/role-mapping.d.ts +9 -0
  142. package/dist/auth/idp/role-mapping.d.ts.map +1 -0
  143. package/dist/auth/idp/role-mapping.js +16 -0
  144. package/dist/auth/idp/role-mapping.js.map +1 -0
  145. package/dist/auth/idp/saml.d.ts +40 -0
  146. package/dist/auth/idp/saml.d.ts.map +1 -0
  147. package/dist/auth/idp/saml.js +117 -0
  148. package/dist/auth/idp/saml.js.map +1 -0
  149. package/dist/auth/idp/types.d.ts +23 -0
  150. package/dist/auth/idp/types.d.ts.map +1 -0
  151. package/dist/auth/idp/types.js +2 -0
  152. package/dist/auth/idp/types.js.map +1 -0
  153. package/dist/fleet/fleet-executor.d.ts +35 -0
  154. package/dist/fleet/fleet-executor.d.ts.map +1 -0
  155. package/dist/fleet/fleet-executor.js +228 -0
  156. package/dist/fleet/fleet-executor.js.map +1 -0
  157. package/dist/fleet/fleet-store.d.ts +13 -0
  158. package/dist/fleet/fleet-store.d.ts.map +1 -0
  159. package/dist/fleet/fleet-store.js +13 -0
  160. package/dist/fleet/fleet-store.js.map +1 -0
  161. package/dist/fleet/index.d.ts +5 -0
  162. package/dist/fleet/index.d.ts.map +1 -0
  163. package/dist/fleet/index.js +4 -0
  164. package/dist/fleet/index.js.map +1 -0
  165. package/dist/fleet/representative-selector.d.ts +15 -0
  166. package/dist/fleet/representative-selector.d.ts.map +1 -0
  167. package/dist/fleet/representative-selector.js +71 -0
  168. package/dist/fleet/representative-selector.js.map +1 -0
  169. package/dist/graph/graph-executor.d.ts +36 -0
  170. package/dist/graph/graph-executor.d.ts.map +1 -0
  171. package/dist/graph/graph-executor.js +348 -0
  172. package/dist/graph/graph-executor.js.map +1 -0
  173. package/dist/graph/graph-inference.d.ts +22 -0
  174. package/dist/graph/graph-inference.d.ts.map +1 -0
  175. package/dist/graph/graph-inference.js +149 -0
  176. package/dist/graph/graph-inference.js.map +1 -0
  177. package/dist/graph/graph-store.d.ts +12 -0
  178. package/dist/graph/graph-store.d.ts.map +1 -0
  179. package/dist/graph/graph-store.js +61 -0
  180. package/dist/graph/graph-store.js.map +1 -0
  181. package/dist/graph/index.d.ts +5 -0
  182. package/dist/graph/index.d.ts.map +1 -0
  183. package/dist/graph/index.js +4 -0
  184. package/dist/graph/index.js.map +1 -0
  185. package/dist/index.d.ts +2 -0
  186. package/dist/index.d.ts.map +1 -0
  187. package/dist/index.js +837 -0
  188. package/dist/index.js.map +1 -0
  189. package/dist/intake/index.d.ts +6 -0
  190. package/dist/intake/index.d.ts.map +1 -0
  191. package/dist/intake/index.js +5 -0
  192. package/dist/intake/index.js.map +1 -0
  193. package/dist/intake/intake-processor.d.ts +17 -0
  194. package/dist/intake/intake-processor.d.ts.map +1 -0
  195. package/dist/intake/intake-processor.js +99 -0
  196. package/dist/intake/intake-processor.js.map +1 -0
  197. package/dist/intake/intake-store.d.ts +7 -0
  198. package/dist/intake/intake-store.d.ts.map +1 -0
  199. package/dist/intake/intake-store.js +7 -0
  200. package/dist/intake/intake-store.js.map +1 -0
  201. package/dist/intake/registry-poller.d.ts +41 -0
  202. package/dist/intake/registry-poller.d.ts.map +1 -0
  203. package/dist/intake/registry-poller.js +202 -0
  204. package/dist/intake/registry-poller.js.map +1 -0
  205. package/dist/intake/webhook-handlers.d.ts +37 -0
  206. package/dist/intake/webhook-handlers.d.ts.map +1 -0
  207. package/dist/intake/webhook-handlers.js +268 -0
  208. package/dist/intake/webhook-handlers.js.map +1 -0
  209. package/dist/logger.d.ts +5 -0
  210. package/dist/logger.d.ts.map +1 -0
  211. package/dist/logger.js +15 -0
  212. package/dist/logger.js.map +1 -0
  213. package/dist/mcp/resources.d.ts +9 -0
  214. package/dist/mcp/resources.d.ts.map +1 -0
  215. package/dist/mcp/resources.js +72 -0
  216. package/dist/mcp/resources.js.map +1 -0
  217. package/dist/mcp/server.d.ts +15 -0
  218. package/dist/mcp/server.d.ts.map +1 -0
  219. package/dist/mcp/server.js +20 -0
  220. package/dist/mcp/server.js.map +1 -0
  221. package/dist/mcp/tools.d.ts +9 -0
  222. package/dist/mcp/tools.d.ts.map +1 -0
  223. package/dist/mcp/tools.js +88 -0
  224. package/dist/mcp/tools.js.map +1 -0
  225. package/dist/middleware/auth.d.ts +29 -0
  226. package/dist/middleware/auth.d.ts.map +1 -0
  227. package/dist/middleware/auth.js +76 -0
  228. package/dist/middleware/auth.js.map +1 -0
  229. package/dist/middleware/permissions.d.ts +13 -0
  230. package/dist/middleware/permissions.d.ts.map +1 -0
  231. package/dist/middleware/permissions.js +32 -0
  232. package/dist/middleware/permissions.js.map +1 -0
  233. package/dist/pattern-store.d.ts +104 -0
  234. package/dist/pattern-store.d.ts.map +1 -0
  235. package/dist/pattern-store.js +299 -0
  236. package/dist/pattern-store.js.map +1 -0
  237. package/package.json +54 -0
  238. package/src/agent/debrief-retention.ts +44 -0
  239. package/src/agent/envoy-client.ts +474 -0
  240. package/src/agent/envoy-registry.ts +384 -0
  241. package/src/agent/health-checker.ts +70 -0
  242. package/src/agent/mcp-client-manager.ts +131 -0
  243. package/src/agent/stale-deployment-detector.ts +79 -0
  244. package/src/agent/step-runner.ts +124 -0
  245. package/src/agent/synth-agent.ts +1567 -0
  246. package/src/api/agent.ts +1075 -0
  247. package/src/api/api-keys.ts +129 -0
  248. package/src/api/artifacts.ts +194 -0
  249. package/src/api/auth.ts +320 -0
  250. package/src/api/deployments.ts +1347 -0
  251. package/src/api/environments.ts +97 -0
  252. package/src/api/envoy-reports.ts +159 -0
  253. package/src/api/envoys.ts +237 -0
  254. package/src/api/fleet.ts +510 -0
  255. package/src/api/graph.ts +516 -0
  256. package/src/api/health.ts +311 -0
  257. package/src/api/idp-schemas.ts +19 -0
  258. package/src/api/idp.ts +735 -0
  259. package/src/api/intake.ts +537 -0
  260. package/src/api/partitions.ts +147 -0
  261. package/src/api/progress-event-store.ts +153 -0
  262. package/src/api/schemas.ts +376 -0
  263. package/src/api/security-boundaries.ts +54 -0
  264. package/src/api/settings.ts +118 -0
  265. package/src/api/system.ts +704 -0
  266. package/src/api/telemetry.ts +32 -0
  267. package/src/api/users.ts +210 -0
  268. package/src/archive-unpacker.ts +271 -0
  269. package/src/artifact-analyzer.ts +438 -0
  270. package/src/auth/idp/index.ts +8 -0
  271. package/src/auth/idp/ldap.ts +340 -0
  272. package/src/auth/idp/oidc.ts +117 -0
  273. package/src/auth/idp/role-mapping.ts +22 -0
  274. package/src/auth/idp/saml.ts +148 -0
  275. package/src/auth/idp/types.ts +22 -0
  276. package/src/fleet/fleet-executor.ts +309 -0
  277. package/src/fleet/fleet-store.ts +13 -0
  278. package/src/fleet/index.ts +4 -0
  279. package/src/fleet/representative-selector.ts +83 -0
  280. package/src/graph/graph-executor.ts +446 -0
  281. package/src/graph/graph-inference.ts +184 -0
  282. package/src/graph/graph-store.ts +75 -0
  283. package/src/graph/index.ts +4 -0
  284. package/src/index.ts +916 -0
  285. package/src/intake/index.ts +5 -0
  286. package/src/intake/intake-processor.ts +111 -0
  287. package/src/intake/intake-store.ts +7 -0
  288. package/src/intake/registry-poller.ts +230 -0
  289. package/src/intake/webhook-handlers.ts +328 -0
  290. package/src/logger.ts +19 -0
  291. package/src/mcp/resources.ts +98 -0
  292. package/src/mcp/server.ts +34 -0
  293. package/src/mcp/tools.ts +117 -0
  294. package/src/middleware/auth.ts +103 -0
  295. package/src/middleware/permissions.ts +35 -0
  296. package/src/pattern-store.ts +409 -0
  297. package/tests/agent-mode.test.ts +536 -0
  298. package/tests/api-handlers.test.ts +1245 -0
  299. package/tests/archive-unpacker.test.ts +179 -0
  300. package/tests/artifact-analyzer.test.ts +240 -0
  301. package/tests/auth-middleware.test.ts +189 -0
  302. package/tests/decision-diary.test.ts +957 -0
  303. package/tests/diary-reader.test.ts +782 -0
  304. package/tests/envoy-client.test.ts +342 -0
  305. package/tests/envoy-reports.test.ts +156 -0
  306. package/tests/mcp-tools.test.ts +213 -0
  307. package/tests/orchestration.test.ts +536 -0
  308. package/tests/partition-deletion.test.ts +143 -0
  309. package/tests/partition-isolation.test.ts +830 -0
  310. package/tests/pattern-store.test.ts +371 -0
  311. package/tests/rbac-enforcement.test.ts +409 -0
  312. package/tests/ssrf-validation.test.ts +56 -0
  313. package/tests/stale-deployment.test.ts +85 -0
  314. package/tests/step-runner.test.ts +308 -0
  315. package/tests/ui-journey.test.ts +330 -0
  316. package/tsconfig.json +11 -0
  317. 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
+ }
@@ -0,0 +1,4 @@
1
+ export { GraphExecutor, topologicalSort } from "./graph-executor.js";
2
+ export type { GraphProgressEvent } from "./graph-executor.js";
3
+ export { GraphInferenceEngine } from "./graph-inference.js";
4
+ export { DeploymentGraphStore } from "./graph-store.js";