@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,704 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type {
3
+ IDeploymentStore,
4
+ IArtifactStore,
5
+ IEnvironmentStore,
6
+ IPartitionStore,
7
+ Partition,
8
+ Environment,
9
+ } from "@synth-deploy/core";
10
+ import type { EnvoyRegistry } from "../agent/envoy-registry.js";
11
+ import { requirePermission } from "../middleware/permissions.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface SignalEvidence {
18
+ label: string;
19
+ value: string;
20
+ status: "healthy" | "warning" | "info";
21
+ }
22
+
23
+ export interface SignalRecommendation {
24
+ action: string;
25
+ detail: string;
26
+ priority: "high" | "medium" | "low";
27
+ }
28
+
29
+ export interface SignalInvestigation {
30
+ title: string;
31
+ entity: string;
32
+ entityType: string;
33
+ status: string;
34
+ detectedAt: string;
35
+ synthAssessment: {
36
+ confidence: number;
37
+ summary: string;
38
+ };
39
+ evidence: SignalEvidence[];
40
+ recommendations: SignalRecommendation[];
41
+ timeline: Array<{ time: string; event: string }>;
42
+ relatedDeployments: Array<{
43
+ artifact: string;
44
+ version: string;
45
+ target: string;
46
+ status: string;
47
+ time: string;
48
+ }>;
49
+ driftConflicts?: Array<{
50
+ variable: string;
51
+ partitionValue: string;
52
+ violatedRule: string;
53
+ affectedEnvoy: string;
54
+ }>;
55
+ }
56
+
57
+ export interface AlertSignal {
58
+ type:
59
+ | "envoy-health"
60
+ | "deployment-failure-pattern"
61
+ | "drift"
62
+ | "new-version-failure-context"
63
+ | "cross-environment-inconsistency"
64
+ | "security-boundary-violation"
65
+ | "dependency-conflict"
66
+ | "stale-deployment"
67
+ | "envoy-knowledge-gap"
68
+ | "scheduled-maintenance-conflict";
69
+ severity: "critical" | "warning" | "info";
70
+ title: string;
71
+ detail: string;
72
+ relatedEntity?: { type: string; id: string; name: string };
73
+ investigation: SignalInvestigation;
74
+ }
75
+
76
+ export interface SystemStateResponse {
77
+ state: "empty" | "normal" | "alert";
78
+ signals: AlertSignal[];
79
+ stats: {
80
+ artifacts: number;
81
+ envoys: number;
82
+ deployments: { total: number; active: number; failed24h: number };
83
+ environments: number;
84
+ };
85
+ assessment: {
86
+ headline: string;
87
+ detail: string;
88
+ };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Helpers
93
+ // ---------------------------------------------------------------------------
94
+
95
+ function timeAgo(date: Date | string): string {
96
+ const diff = Date.now() - new Date(date).getTime();
97
+ const mins = Math.floor(diff / 60000);
98
+ if (mins < 1) return "just now";
99
+ if (mins < 60) return `${mins}m ago`;
100
+ const hours = Math.floor(mins / 60);
101
+ if (hours < 24) return `${hours}h ago`;
102
+ const days = Math.floor(hours / 24);
103
+ return `${days}d ago`;
104
+ }
105
+
106
+ function fmtTime(date: Date | string): string {
107
+ return new Date(date).toTimeString().slice(0, 8);
108
+ }
109
+
110
+ function nowIso(): string {
111
+ return new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
112
+ }
113
+
114
+ function nowTime(): string {
115
+ return new Date().toTimeString().slice(0, 8);
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Drift detection (reused from agent.ts pattern)
120
+ // ---------------------------------------------------------------------------
121
+
122
+ function detectDrift(partition: Partition, environment: Environment): string[] {
123
+ const conflicts: string[] = [];
124
+ const envPatterns: Record<string, RegExp[]> = {
125
+ production: [/\bstag/i, /\bdev\b/i],
126
+ staging: [/\bprod/i],
127
+ development: [/\bprod/i, /\bstag/i],
128
+ };
129
+
130
+ const patternsToCheck = envPatterns[environment.name.toLowerCase()];
131
+ if (!patternsToCheck) return conflicts;
132
+
133
+ for (const [key, value] of Object.entries(partition.variables)) {
134
+ if (patternsToCheck.some((p) => p.test(value))) {
135
+ conflicts.push(key);
136
+ }
137
+ }
138
+
139
+ return conflicts;
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Route registration
144
+ // ---------------------------------------------------------------------------
145
+
146
+ export function registerSystemRoutes(
147
+ app: FastifyInstance,
148
+ deployments: IDeploymentStore,
149
+ artifacts: IArtifactStore,
150
+ environments: IEnvironmentStore,
151
+ partitions: IPartitionStore,
152
+ envoyRegistry: EnvoyRegistry,
153
+ ): void {
154
+ app.get("/api/system/state", { preHandler: [requirePermission("deployment.view")] }, async () => {
155
+ const allArtifacts = artifacts.list();
156
+ const allEnvoys = envoyRegistry.list();
157
+ const allDeployments = deployments.list();
158
+ const allEnvironments = environments.list();
159
+ const allPartitions = partitions.list();
160
+
161
+ // --- Stats ---
162
+ const now = Date.now();
163
+ const twentyFourHoursAgo = now - 24 * 60 * 60 * 1000;
164
+
165
+ const activeDeployments = allDeployments.filter(
166
+ (d) => d.status === "running" || d.status === "pending",
167
+ );
168
+ const failed24h = allDeployments.filter(
169
+ (d) =>
170
+ d.status === "failed" &&
171
+ new Date(d.createdAt).getTime() > twentyFourHoursAgo,
172
+ );
173
+
174
+ const stats = {
175
+ artifacts: allArtifacts.length,
176
+ envoys: allEnvoys.length,
177
+ deployments: {
178
+ total: allDeployments.length,
179
+ active: activeDeployments.length,
180
+ failed24h: failed24h.length,
181
+ },
182
+ environments: allEnvironments.length,
183
+ };
184
+
185
+ // --- Empty state ---
186
+ if (allArtifacts.length === 0 && allEnvoys.length === 0) {
187
+ return {
188
+ state: "empty",
189
+ signals: [],
190
+ stats,
191
+ assessment: { headline: "Welcome to Synth.", detail: "Get started by connecting an envoy and registering your first artifact." },
192
+ } satisfies SystemStateResponse;
193
+ }
194
+
195
+ // --- Alert detection ---
196
+ const signals: AlertSignal[] = [];
197
+
198
+ // 1. Envoy health signals
199
+ for (const envoy of allEnvoys) {
200
+ if (envoy.lastHealthStatus === "degraded" || envoy.lastHealthStatus === "unreachable") {
201
+ const isDegraded = envoy.lastHealthStatus === "degraded";
202
+ const severity = isDegraded ? "warning" : "critical";
203
+ const lastCheck = envoy.lastHealthCheck ? timeAgo(envoy.lastHealthCheck) : "never";
204
+ const lastCheckTime = envoy.lastHealthCheck ? fmtTime(envoy.lastHealthCheck) : nowTime();
205
+
206
+ const recentToEnvoy = allDeployments
207
+ .filter((d) => {
208
+ const envForDep = allEnvironments.find((e) => e.id === d.environmentId);
209
+ return envForDep != null;
210
+ })
211
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
212
+ .slice(0, 2);
213
+
214
+ signals.push({
215
+ type: "envoy-health",
216
+ severity,
217
+ title: `${envoy.name} envoy ${isDegraded ? "heartbeat degraded" : "unreachable"}`,
218
+ detail: isDegraded
219
+ ? `Health check returned degraded status. Last checked: ${lastCheck}.`
220
+ : `Cannot reach envoy at ${envoy.url}. Last checked: ${lastCheck}.`,
221
+ relatedEntity: { type: "envoy", id: envoy.id, name: envoy.name },
222
+ investigation: {
223
+ title: isDegraded ? "Envoy Heartbeat Degradation" : "Envoy Unreachable",
224
+ entity: envoy.name,
225
+ entityType: "envoy",
226
+ status: "active",
227
+ detectedAt: nowIso(),
228
+ synthAssessment: {
229
+ confidence: isDegraded ? 0.78 : 0.62,
230
+ summary: isDegraded
231
+ ? `${envoy.name} is responding to health probes but returning degraded status. This typically indicates resource pressure (CPU, memory, or disk), a failed sub-component, or intermittent network connectivity. The envoy process is likely still running — deployments may succeed but with reduced reliability.`
232
+ : `Cannot reach ${envoy.name} at ${envoy.url}. The envoy process may have stopped, the host may be down, or network connectivity may be blocked. Cannot distinguish between these causes without direct host access.`,
233
+ },
234
+ evidence: [
235
+ { label: "Health status", value: isDegraded ? "Degraded — returning non-OK on health probe" : "Unreachable — no response to ping", status: "warning" },
236
+ { label: "Last health check", value: `${lastCheck} (normal: <2m)`, status: isDegraded ? "warning" : "warning" },
237
+ { label: "Envoy URL", value: envoy.url, status: isDegraded ? "info" : "warning" },
238
+ { label: "Registered name", value: envoy.name, status: "info" },
239
+ ...(isDegraded
240
+ ? [{ label: "Deployment risk", value: "Elevated — envoy running but degraded", status: "info" as const }]
241
+ : [{ label: "Deployment risk", value: "Critical — envoy cannot be reached", status: "warning" as const }]),
242
+ ],
243
+ recommendations: [
244
+ {
245
+ action: `Hold deployments to ${envoy.name}`,
246
+ detail: `Avoid deploying to this envoy while its health is ${isDegraded ? "degraded" : "unknown"}. Deployments may fail or produce inconsistent results.`,
247
+ priority: "high",
248
+ },
249
+ {
250
+ action: isDegraded ? "Check envoy process and resources" : "Verify host reachability",
251
+ detail: isDegraded
252
+ ? `SSH into the host running ${envoy.name} and check CPU, memory, and disk. Look for the envoy process in the process list.`
253
+ : `Ping the host at ${envoy.url} and verify that network routing is intact. Check if the host itself is reachable before investigating the envoy process.`,
254
+ priority: isDegraded ? "medium" : "high",
255
+ },
256
+ {
257
+ action: "Review envoy logs",
258
+ detail: `Check the envoy's log output for errors or warnings. ${isDegraded ? "Degraded status often indicates a resource constraint or failed health sub-check." : "The last log entries before the outage may indicate the cause."}`,
259
+ priority: "medium",
260
+ },
261
+ ],
262
+ timeline: [
263
+ { time: lastCheckTime, event: `Health check returned ${isDegraded ? "degraded" : "unreachable"} status` },
264
+ { time: nowTime(), event: "Signal raised" },
265
+ ],
266
+ relatedDeployments: recentToEnvoy.map((d) => {
267
+ const artName = allArtifacts.find((a) => a.id === d.artifactId)?.name ?? d.artifactId.slice(0, 8);
268
+ const envName = allEnvironments.find((e) => e.id === d.environmentId)?.name ?? "unknown";
269
+ return {
270
+ artifact: artName,
271
+ version: d.version,
272
+ target: envName,
273
+ status: d.status,
274
+ time: timeAgo(d.createdAt),
275
+ };
276
+ }),
277
+ },
278
+ });
279
+ }
280
+ }
281
+
282
+ // 2. Deployment failure pattern signals — only raised when multiple failures occur to the
283
+ // same artifact+environment without a successful recovery (not on individual failures,
284
+ // which are visible in the deployment list and debrief).
285
+ const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
286
+ const recentFailures = allDeployments.filter(
287
+ (d) => d.status === "failed" && new Date(d.createdAt).getTime() > sevenDaysAgo,
288
+ );
289
+
290
+ // Group by artifactId+environmentId
291
+ type FailureGroup = { artifactId: string; environmentId: string | undefined; failures: typeof recentFailures };
292
+ const failureGroups = new Map<string, FailureGroup>();
293
+ for (const dep of recentFailures) {
294
+ const key = `${dep.artifactId}::${dep.environmentId}`;
295
+ if (!failureGroups.has(key)) {
296
+ failureGroups.set(key, { artifactId: dep.artifactId, environmentId: dep.environmentId, failures: [] });
297
+ }
298
+ failureGroups.get(key)!.failures.push(dep);
299
+ }
300
+
301
+ for (const group of failureGroups.values()) {
302
+ if (group.failures.length < 2) continue; // Single failure = not a signal
303
+
304
+ const hasRecovery = allDeployments.some(
305
+ (d) =>
306
+ d.artifactId === group.artifactId &&
307
+ d.environmentId === group.environmentId &&
308
+ d.status === "succeeded" &&
309
+ new Date(d.createdAt).getTime() > new Date(group.failures[0].createdAt).getTime(),
310
+ );
311
+ if (hasRecovery) continue;
312
+
313
+ const envName = allEnvironments.find((e) => e.id === group.environmentId)?.name ?? "unknown";
314
+ const artifactName = allArtifacts.find((a) => a.id === group.artifactId)?.name ?? "unknown";
315
+ const n = group.failures.length;
316
+ const sorted = [...group.failures].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
317
+ const mostRecent = sorted[0];
318
+ const reasons = [...new Set(sorted.map((d) => d.failureReason).filter(Boolean))];
319
+
320
+ const prevSuccessful = allDeployments
321
+ .filter(
322
+ (d) =>
323
+ d.artifactId === group.artifactId &&
324
+ d.environmentId === group.environmentId &&
325
+ d.status === "succeeded",
326
+ )
327
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
328
+ .slice(0, 1);
329
+
330
+ signals.push({
331
+ type: "deployment-failure-pattern",
332
+ severity: "critical",
333
+ title: `Repeated failure: ${artifactName} → ${envName}`,
334
+ detail: `${n} failures in 7 days with no successful recovery. ${reasons.length > 0 ? `Last reason: ${reasons[0]}` : "No failure reason recorded."}`,
335
+ relatedEntity: { type: "artifact", id: group.artifactId, name: artifactName },
336
+ investigation: {
337
+ title: `Deployment Failure Pattern — ${artifactName} → ${envName}`,
338
+ entity: artifactName,
339
+ entityType: "artifact",
340
+ status: "active",
341
+ detectedAt: nowIso(),
342
+ synthAssessment: {
343
+ confidence: reasons.length > 0 ? 0.88 : 0.65,
344
+ summary: `${artifactName} has failed to deploy to ${envName} ${n} times in the past 7 days without a successful recovery. ${reasons.length > 0 ? `The recurring failure reason is: ${reasons.join("; ")}. ` : ""}This is a pattern, not an isolated incident — retrying without addressing the root cause will likely produce the same result. The environment may be in a degraded state.`,
345
+ },
346
+ evidence: [
347
+ { label: "Failure count", value: `${n} failures in the last 7 days`, status: "warning" },
348
+ { label: "Most recent failure", value: timeAgo(mostRecent.createdAt), status: "warning" },
349
+ { label: "Target environment", value: `${envName} — no successful recovery`, status: "warning" },
350
+ ...(reasons.length > 0
351
+ ? reasons.map((r) => ({ label: "Failure reason", value: r!, status: "warning" as const }))
352
+ : [{ label: "Failure reason", value: "Unknown — check debrief for trace", status: "warning" as const }]),
353
+ ...(prevSuccessful.length > 0
354
+ ? [{ label: "Last success", value: `${timeAgo(prevSuccessful[0].createdAt)} (v${prevSuccessful[0].version})`, status: "info" as const }]
355
+ : [{ label: "Prior successes", value: "No successful deployments on record", status: "info" as const }]),
356
+ ],
357
+ recommendations: [
358
+ {
359
+ action: "Review debriefs for all failed deployments",
360
+ detail: `Each failed deployment has a debrief with the full execution trace. Compare them to identify whether the failure mode is consistent or varying.`,
361
+ priority: "high",
362
+ },
363
+ {
364
+ action: "Check environment health before retrying",
365
+ detail: `Verify that the ${envName} environment is in a known good state. Repeated failures may have left partial state that will block future deployments.`,
366
+ priority: "high",
367
+ },
368
+ {
369
+ action: "Address root cause before next attempt",
370
+ detail: reasons.length > 0
371
+ ? `The failure reason "${reasons[0]}" has recurred. Fix it at the source before scheduling another deployment.`
372
+ : "Identify the root cause from the debrief logs. Blind retries on a recurring failure pattern waste time and may worsen environment state.",
373
+ priority: "medium",
374
+ },
375
+ ],
376
+ timeline: [
377
+ ...sorted.slice().reverse().map((d) => ({ time: fmtTime(d.createdAt), event: `Deployment failed: ${artifactName} v${d.version}${d.failureReason ? ` — ${d.failureReason}` : ""}` })),
378
+ { time: nowTime(), event: `Signal raised — ${n} failures, no recovery` },
379
+ ],
380
+ relatedDeployments: sorted.map((d) => ({
381
+ artifact: artifactName,
382
+ version: d.version,
383
+ target: envName,
384
+ status: d.status,
385
+ time: timeAgo(d.createdAt),
386
+ })),
387
+ },
388
+ });
389
+ }
390
+
391
+ // 4. Stale deployment signals — artifact has been running significantly longer than
392
+ // its average deployment lifecycle with newer versions deployed elsewhere.
393
+ const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
394
+ const succeededDeps = allDeployments.filter((d) => d.status === "succeeded");
395
+
396
+ // Group succeeded deployments by artifactId+environmentId to find "currently running"
397
+ type EnvLatest = { dep: (typeof succeededDeps)[0]; envName: string };
398
+ const latestByTarget = new Map<string, EnvLatest>();
399
+ for (const dep of succeededDeps) {
400
+ const key = `${dep.artifactId}::${dep.environmentId}`;
401
+ const existing = latestByTarget.get(key);
402
+ if (!existing || new Date(dep.createdAt) > new Date(existing.dep.createdAt)) {
403
+ const envName = allEnvironments.find((e) => e.id === dep.environmentId)?.name ?? "unknown";
404
+ latestByTarget.set(key, { dep, envName });
405
+ }
406
+ }
407
+
408
+ for (const { dep, envName } of latestByTarget.values()) {
409
+ if (new Date(dep.createdAt).getTime() > thirtyDaysAgo) continue; // Not stale yet
410
+
411
+ const artifactName = allArtifacts.find((a) => a.id === dep.artifactId)?.name ?? "unknown";
412
+ const weeksAgo = Math.floor((now - new Date(dep.createdAt).getTime()) / (7 * 24 * 60 * 60 * 1000));
413
+
414
+ // Check if newer versions of this artifact have been deployed to any other environment
415
+ const newerElsewhere = succeededDeps.filter(
416
+ (d) =>
417
+ d.artifactId === dep.artifactId &&
418
+ d.environmentId !== dep.environmentId &&
419
+ new Date(d.createdAt).getTime() > new Date(dep.createdAt).getTime(),
420
+ );
421
+ if (newerElsewhere.length === 0) continue; // No newer deployments anywhere — not actionable
422
+
423
+ const newerVersions = [...new Set(newerElsewhere.map((d) => d.version))];
424
+
425
+ signals.push({
426
+ type: "stale-deployment",
427
+ severity: "info",
428
+ title: `Stale deployment: ${artifactName} in ${envName}`,
429
+ detail: `v${dep.version} deployed ${weeksAgo}w ago. ${newerVersions.length} newer version${newerVersions.length > 1 ? "s" : ""} running elsewhere. May be intentional.`,
430
+ relatedEntity: { type: "artifact", id: dep.artifactId, name: artifactName },
431
+ investigation: {
432
+ title: `Stale Deployment — ${artifactName} in ${envName}`,
433
+ entity: `${artifactName} in ${envName}`,
434
+ entityType: "artifact",
435
+ status: "active",
436
+ detectedAt: nowIso(),
437
+ synthAssessment: {
438
+ confidence: 0.72,
439
+ summary: `${artifactName} v${dep.version} has been running in ${envName} for ${weeksAgo} weeks without an update. ${newerVersions.length} newer version${newerVersions.length > 1 ? "s have" : " has"} been deployed to other environments: ${newerVersions.join(", ")}. This may be intentional for stable workloads, or it may indicate a missed promotion. Synth is not recommending action — only confirming you're aware.`,
440
+ },
441
+ evidence: [
442
+ { label: "Running version", value: `v${dep.version} — deployed ${weeksAgo}w ago`, status: "info" },
443
+ { label: "Environment", value: envName, status: "info" },
444
+ { label: "Newer versions elsewhere", value: newerVersions.join(", "), status: "info" },
445
+ { label: "Last deployment", value: timeAgo(dep.createdAt), status: "info" },
446
+ ],
447
+ recommendations: [
448
+ {
449
+ action: "Confirm this is intentional",
450
+ detail: `If ${artifactName} in ${envName} is a stable workload that intentionally lags behind, no action needed. If newer versions should have been promoted, schedule a deployment.`,
451
+ priority: "low",
452
+ },
453
+ {
454
+ action: "Review changes in newer versions",
455
+ detail: `Check what changed between v${dep.version} and ${newerVersions[newerVersions.length - 1]} before promoting to ${envName}.`,
456
+ priority: "low",
457
+ },
458
+ ],
459
+ timeline: [
460
+ { time: fmtTime(dep.createdAt), event: `${artifactName} v${dep.version} deployed to ${envName}` },
461
+ { time: nowTime(), event: `Signal raised — ${weeksAgo}w without update, newer versions exist` },
462
+ ],
463
+ relatedDeployments: [
464
+ { artifact: artifactName, version: dep.version, target: envName, status: "succeeded", time: timeAgo(dep.createdAt) },
465
+ ...newerElsewhere.slice(0, 3).map((d) => {
466
+ const env = allEnvironments.find((e) => e.id === d.environmentId)?.name ?? "unknown";
467
+ return { artifact: artifactName, version: d.version, target: env, status: d.status, time: timeAgo(d.createdAt) };
468
+ }),
469
+ ],
470
+ },
471
+ });
472
+ }
473
+
474
+ // 5. Cross-environment inconsistency — same artifact running across environments in a
475
+ // pattern that suggests a missed or skipped promotion.
476
+ const artifactEnvVersions = new Map<string, Map<string, { version: string; deployedAt: Date }>>();
477
+ for (const { dep, envName } of latestByTarget.values()) {
478
+ if (!artifactEnvVersions.has(dep.artifactId)) {
479
+ artifactEnvVersions.set(dep.artifactId, new Map());
480
+ }
481
+ artifactEnvVersions.get(dep.artifactId)!.set(envName, {
482
+ version: dep.version,
483
+ deployedAt: new Date(dep.createdAt),
484
+ });
485
+ }
486
+
487
+ for (const [artifactId, envMap] of artifactEnvVersions.entries()) {
488
+ if (envMap.size < 2) continue; // Only relevant with 2+ environments
489
+
490
+ const entries = [...envMap.entries()];
491
+ const artifactName = allArtifacts.find((a) => a.id === artifactId)?.name ?? "unknown";
492
+
493
+ // Find the most-recently-updated environment (the "ahead" env)
494
+ const sorted = entries.sort((a, b) => b[1].deployedAt.getTime() - a[1].deployedAt.getTime());
495
+ const [aheadEnv, aheadData] = sorted[0];
496
+ const [behindEnv, behindData] = sorted[sorted.length - 1];
497
+
498
+ if (aheadData.version === behindData.version) continue; // Same version everywhere — OK
499
+
500
+ // Only flag if the behind environment hasn't been updated in 14+ days while the ahead env has newer
501
+ const daysBehind = Math.floor((aheadData.deployedAt.getTime() - behindData.deployedAt.getTime()) / (24 * 60 * 60 * 1000));
502
+ if (daysBehind < 14) continue;
503
+
504
+ // Also require that the ahead env has more recent deployments of this artifact (not just same artifact)
505
+ const aheadHasMultiple = succeededDeps.filter(
506
+ (d) => d.artifactId === artifactId &&
507
+ allEnvironments.find((e) => e.id === d.environmentId)?.name === aheadEnv,
508
+ ).length >= 2;
509
+ if (!aheadHasMultiple) continue;
510
+
511
+ signals.push({
512
+ type: "cross-environment-inconsistency",
513
+ severity: "warning",
514
+ title: `Version gap: ${artifactName} (${behindEnv} vs ${aheadEnv})`,
515
+ detail: `${behindEnv} is on v${behindData.version}, ${aheadEnv} has v${aheadData.version} (${daysBehind}d ahead). Promotion may have been missed.`,
516
+ relatedEntity: { type: "artifact", id: artifactId, name: artifactName },
517
+ investigation: {
518
+ title: `Cross-Environment Version Gap — ${artifactName}`,
519
+ entity: artifactName,
520
+ entityType: "artifact",
521
+ status: "active",
522
+ detectedAt: nowIso(),
523
+ synthAssessment: {
524
+ confidence: 0.76,
525
+ summary: `${artifactName} is running different versions across environments in a pattern that may indicate a missed promotion. ${aheadEnv} has v${aheadData.version} (updated ${timeAgo(aheadData.deployedAt)}), while ${behindEnv} is still on v${behindData.version} (updated ${timeAgo(behindData.deployedAt)}, ${daysBehind} days behind). Normal staging-to-production lag is expected, but a ${daysBehind}-day gap with active updates in ${aheadEnv} suggests the ${behindEnv} promotion may have been overlooked.`,
526
+ },
527
+ evidence: [
528
+ { label: `${aheadEnv} version`, value: `v${aheadData.version} — updated ${timeAgo(aheadData.deployedAt)}`, status: "healthy" },
529
+ { label: `${behindEnv} version`, value: `v${behindData.version} — updated ${timeAgo(behindData.deployedAt)}`, status: "warning" },
530
+ { label: "Version gap", value: `${daysBehind} days between last promotions`, status: "warning" },
531
+ ...entries.slice(2).map(([env, data]) => ({
532
+ label: `${env} version`,
533
+ value: `v${data.version} — updated ${timeAgo(data.deployedAt)}`,
534
+ status: "info" as const,
535
+ })),
536
+ ],
537
+ recommendations: [
538
+ {
539
+ action: `Review changes before promoting to ${behindEnv}`,
540
+ detail: `Check what changed between v${behindData.version} and v${aheadData.version} before scheduling the promotion. ${daysBehind} days of changes may require careful review.`,
541
+ priority: "medium",
542
+ },
543
+ {
544
+ action: `Promote ${artifactName} to ${behindEnv}`,
545
+ detail: `If the version gap is unintentional, schedule a deployment of ${artifactName} v${aheadData.version} to ${behindEnv}.`,
546
+ priority: "low",
547
+ },
548
+ ],
549
+ timeline: [
550
+ { time: fmtTime(behindData.deployedAt), event: `${artifactName} v${behindData.version} deployed to ${behindEnv}` },
551
+ { time: fmtTime(aheadData.deployedAt), event: `${artifactName} v${aheadData.version} deployed to ${aheadEnv}` },
552
+ { time: nowTime(), event: `Signal raised — ${daysBehind}-day version gap detected` },
553
+ ],
554
+ relatedDeployments: entries.map(([env, data]) => ({
555
+ artifact: artifactName,
556
+ version: data.version,
557
+ target: env,
558
+ status: "succeeded",
559
+ time: timeAgo(data.deployedAt),
560
+ })),
561
+ },
562
+ });
563
+ }
564
+
565
+ // 3. Configuration drift signals
566
+ for (const partition of allPartitions) {
567
+ for (const env of allEnvironments) {
568
+ const conflicts = detectDrift(partition, env);
569
+ if (conflicts.length > 0) {
570
+ const n = conflicts.length;
571
+ const recentToEnv = allDeployments
572
+ .filter((d) => d.environmentId === env.id)
573
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
574
+ .slice(0, 2);
575
+
576
+ signals.push({
577
+ type: "drift",
578
+ severity: "warning",
579
+ title: `Config drift: ${partition.name} / ${env.name}`,
580
+ detail: `${n} variable${n > 1 ? "s" : ""} may conflict: ${conflicts.join(", ")}`,
581
+ relatedEntity: { type: "environment", id: env.id, name: env.name },
582
+ investigation: {
583
+ title: "Configuration Drift Detected",
584
+ entity: `${partition.name} / ${env.name}`,
585
+ entityType: "partition",
586
+ status: "active",
587
+ detectedAt: nowIso(),
588
+ synthAssessment: {
589
+ confidence: 0.9,
590
+ summary: `${n} variable${n > 1 ? "s" : ""} in partition "${partition.name}" contain${n === 1 ? "s" : ""} values that don't match the expected pattern for the "${env.name}" environment. This was detected through environment-pattern analysis — the values reference identifiers (like hostnames or URLs) that suggest a different tier. This could cause runtime failures if deployed as-is.`,
591
+ },
592
+ evidence: [
593
+ { label: "Partition", value: `${partition.name} · ${Object.keys(partition.variables).length} variables defined`, status: "info" },
594
+ { label: "Environment", value: `${env.name} · ${conflicts.length} conflict${conflicts.length > 1 ? "s" : ""} detected`, status: "info" },
595
+ ...conflicts.map((key) => ({
596
+ label: `Variable: ${key}`,
597
+ value: `"${String(partition.variables[key] ?? "(empty)")}" — conflicts with ${env.name} tier`,
598
+ status: "warning" as const,
599
+ })),
600
+ ],
601
+ recommendations: [
602
+ {
603
+ action: "Review the drifted variables",
604
+ detail: `Open the partition detail for "${partition.name}" and inspect: ${conflicts.join(", ")}. Confirm whether they contain the correct values for the "${env.name}" environment.`,
605
+ priority: "high",
606
+ },
607
+ {
608
+ action: "Update partition variables",
609
+ detail: `If the values reference the wrong environment tier, correct them in the partition before the next deployment. The next deploy will apply the corrected values.`,
610
+ priority: "medium",
611
+ },
612
+ {
613
+ action: "Redeploy after correction",
614
+ detail: "Once variables are corrected, trigger a fresh deployment to apply them. The previous deployment used the drifted values.",
615
+ priority: "low",
616
+ },
617
+ ],
618
+ timeline: [
619
+ { time: nowTime(), event: `Routine config scan detected ${n} variable${n > 1 ? "s" : ""} with environment mismatch` },
620
+ { time: nowTime(), event: "Signal raised" },
621
+ ],
622
+ relatedDeployments: recentToEnv.map((d) => {
623
+ const artName = allArtifacts.find((a) => a.id === d.artifactId)?.name ?? d.artifactId.slice(0, 8);
624
+ return {
625
+ artifact: artName,
626
+ version: d.version,
627
+ target: env.name,
628
+ status: d.status,
629
+ time: timeAgo(d.createdAt),
630
+ };
631
+ }),
632
+ driftConflicts: conflicts.map((key) => {
633
+ const val = String(partition.variables[key] ?? "(empty)");
634
+ const envLower = env.name.toLowerCase();
635
+ const hasProd = /\bprod/i.test(val);
636
+ const hasStag = /\bstag/i.test(val);
637
+ let rule: string;
638
+ if (envLower === "development") {
639
+ rule = hasProd ? `Must not reference production tier in development` : `Must not reference staging tier in development`;
640
+ } else if (envLower === "staging") {
641
+ rule = `Must not reference production tier in staging`;
642
+ } else if (envLower === "production") {
643
+ rule = hasStag ? `Must not reference staging tier in production` : `Must not reference development tier in production`;
644
+ } else {
645
+ rule = `Value conflicts with expected ${env.name} environment pattern`;
646
+ }
647
+ // Find most recent envoy that executed a deployment to this environment
648
+ const affectedEnvoyName = recentToEnv
649
+ .map((d) => d.envoyId ? allEnvoys.find((e) => e.id === d.envoyId)?.name : null)
650
+ .find(Boolean) ?? env.name;
651
+ return {
652
+ variable: key,
653
+ partitionValue: val,
654
+ violatedRule: rule,
655
+ affectedEnvoy: affectedEnvoyName,
656
+ };
657
+ }),
658
+ },
659
+ });
660
+ }
661
+ }
662
+ }
663
+
664
+ const critical = signals.filter((s) => s.severity === "critical");
665
+ const warnings = signals.filter((s) => s.severity === "warning");
666
+ const infos = signals.filter((s) => s.severity === "info");
667
+
668
+ const state = (critical.length > 0 || warnings.length > 0) ? "alert" : "normal";
669
+
670
+ // Derive editorial assessment
671
+ let headline: string;
672
+ let detail: string;
673
+
674
+ if (critical.length > 0) {
675
+ headline = critical.length === 1 ? "One thing before you deploy." : `${critical.length} issues need your attention.`;
676
+ detail = critical[0].detail;
677
+ } else if (warnings.length > 0) {
678
+ headline = warnings.length === 1 ? "One thing to keep in mind." : `${warnings.length} signals worth reviewing.`;
679
+ detail = warnings[0].detail;
680
+ } else if (infos.length > 0) {
681
+ headline = "Systems clear. A few things worth knowing.";
682
+ detail = infos[0].detail;
683
+ } else if (activeDeployments.length > 0) {
684
+ const d = activeDeployments[0];
685
+ const artName = allArtifacts.find((a) => a.id === d.artifactId)?.name ?? "A deployment";
686
+ const envName = allEnvironments.find((e) => e.id === d.environmentId)?.name ?? "target";
687
+ headline = "Deployment in progress.";
688
+ detail = `${artName} is being deployed to ${envName}. All other environments are stable.`;
689
+ } else {
690
+ const totalDeps = allDeployments.length;
691
+ headline = "Looking good. Systems are clear.";
692
+ detail = totalDeps > 0
693
+ ? `No active alerts. ${stats.deployments.failed24h === 0 ? "All recent deployments succeeded." : `${stats.deployments.failed24h} failure${stats.deployments.failed24h > 1 ? "s" : ""} in the last 24h.`}`
694
+ : "No active alerts. Ready for your first deployment.";
695
+ }
696
+
697
+ return {
698
+ state,
699
+ signals,
700
+ stats,
701
+ assessment: { headline, detail },
702
+ } satisfies SystemStateResponse;
703
+ });
704
+ }