@pax8-cta/core 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 (226) hide show
  1. package/LICENSE +198 -0
  2. package/dist/auth/device-code-login.d.ts +40 -0
  3. package/dist/auth/device-code-login.d.ts.map +1 -0
  4. package/dist/auth/device-code-login.js +59 -0
  5. package/dist/auth/device-code-login.js.map +1 -0
  6. package/dist/auth/gdap-client.d.ts +81 -0
  7. package/dist/auth/gdap-client.d.ts.map +1 -0
  8. package/dist/auth/gdap-client.js +128 -0
  9. package/dist/auth/gdap-client.js.map +1 -0
  10. package/dist/auth/index.d.ts +19 -0
  11. package/dist/auth/index.d.ts.map +1 -0
  12. package/dist/auth/index.js +19 -0
  13. package/dist/auth/index.js.map +1 -0
  14. package/dist/auth/token-manager.d.ts +54 -0
  15. package/dist/auth/token-manager.d.ts.map +1 -0
  16. package/dist/auth/token-manager.js +150 -0
  17. package/dist/auth/token-manager.js.map +1 -0
  18. package/dist/client.d.ts +27 -0
  19. package/dist/client.d.ts.map +1 -0
  20. package/dist/client.js +27 -0
  21. package/dist/client.js.map +1 -0
  22. package/dist/config/client.d.ts +24 -0
  23. package/dist/config/client.d.ts.map +1 -0
  24. package/dist/config/client.js +18 -0
  25. package/dist/config/client.js.map +1 -0
  26. package/dist/config/index.d.ts +18 -0
  27. package/dist/config/index.d.ts.map +1 -0
  28. package/dist/config/index.js +18 -0
  29. package/dist/config/index.js.map +1 -0
  30. package/dist/config/loader.d.ts +81 -0
  31. package/dist/config/loader.d.ts.map +1 -0
  32. package/dist/config/loader.js +271 -0
  33. package/dist/config/loader.js.map +1 -0
  34. package/dist/config/schema.d.ts +751 -0
  35. package/dist/config/schema.d.ts.map +1 -0
  36. package/dist/config/schema.js +556 -0
  37. package/dist/config/schema.js.map +1 -0
  38. package/dist/constants.d.ts +116 -0
  39. package/dist/constants.d.ts.map +1 -0
  40. package/dist/constants.js +170 -0
  41. package/dist/constants.js.map +1 -0
  42. package/dist/dataverse/agent-resolver.d.ts +98 -0
  43. package/dist/dataverse/agent-resolver.d.ts.map +1 -0
  44. package/dist/dataverse/agent-resolver.js +185 -0
  45. package/dist/dataverse/agent-resolver.js.map +1 -0
  46. package/dist/dataverse/client.d.ts +104 -0
  47. package/dist/dataverse/client.d.ts.map +1 -0
  48. package/dist/dataverse/client.js +272 -0
  49. package/dist/dataverse/client.js.map +1 -0
  50. package/dist/dataverse/connection-refs.d.ts +115 -0
  51. package/dist/dataverse/connection-refs.d.ts.map +1 -0
  52. package/dist/dataverse/connection-refs.js +203 -0
  53. package/dist/dataverse/connection-refs.js.map +1 -0
  54. package/dist/dataverse/index.d.ts +20 -0
  55. package/dist/dataverse/index.d.ts.map +1 -0
  56. package/dist/dataverse/index.js +20 -0
  57. package/dist/dataverse/index.js.map +1 -0
  58. package/dist/dataverse/solution-ops.d.ts +100 -0
  59. package/dist/dataverse/solution-ops.d.ts.map +1 -0
  60. package/dist/dataverse/solution-ops.js +288 -0
  61. package/dist/dataverse/solution-ops.js.map +1 -0
  62. package/dist/errors.d.ts +171 -0
  63. package/dist/errors.d.ts.map +1 -0
  64. package/dist/errors.js +178 -0
  65. package/dist/errors.js.map +1 -0
  66. package/dist/index.d.ts +27 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +40 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/mock/demo-data.d.ts +213 -0
  71. package/dist/mock/demo-data.d.ts.map +1 -0
  72. package/dist/mock/demo-data.js +1096 -0
  73. package/dist/mock/demo-data.js.map +1 -0
  74. package/dist/mock/demo-deployment-store.d.ts +77 -0
  75. package/dist/mock/demo-deployment-store.d.ts.map +1 -0
  76. package/dist/mock/demo-deployment-store.js +85 -0
  77. package/dist/mock/demo-deployment-store.js.map +1 -0
  78. package/dist/powerplatform/admin-client.d.ts +226 -0
  79. package/dist/powerplatform/admin-client.d.ts.map +1 -0
  80. package/dist/powerplatform/admin-client.js +315 -0
  81. package/dist/powerplatform/admin-client.js.map +1 -0
  82. package/dist/powerplatform/index.d.ts +18 -0
  83. package/dist/powerplatform/index.d.ts.map +1 -0
  84. package/dist/powerplatform/index.js +18 -0
  85. package/dist/powerplatform/index.js.map +1 -0
  86. package/dist/powerplatform/tenant-discovery.d.ts +100 -0
  87. package/dist/powerplatform/tenant-discovery.d.ts.map +1 -0
  88. package/dist/powerplatform/tenant-discovery.js +205 -0
  89. package/dist/powerplatform/tenant-discovery.js.map +1 -0
  90. package/dist/preconditions/check.d.ts +41 -0
  91. package/dist/preconditions/check.d.ts.map +1 -0
  92. package/dist/preconditions/check.js +173 -0
  93. package/dist/preconditions/check.js.map +1 -0
  94. package/dist/preconditions/index.d.ts +20 -0
  95. package/dist/preconditions/index.d.ts.map +1 -0
  96. package/dist/preconditions/index.js +20 -0
  97. package/dist/preconditions/index.js.map +1 -0
  98. package/dist/preconditions/loader.d.ts +33 -0
  99. package/dist/preconditions/loader.d.ts.map +1 -0
  100. package/dist/preconditions/loader.js +65 -0
  101. package/dist/preconditions/loader.js.map +1 -0
  102. package/dist/preconditions/schema.d.ts +103 -0
  103. package/dist/preconditions/schema.d.ts.map +1 -0
  104. package/dist/preconditions/schema.js +93 -0
  105. package/dist/preconditions/schema.js.map +1 -0
  106. package/dist/preconditions/types.d.ts +118 -0
  107. package/dist/preconditions/types.d.ts.map +1 -0
  108. package/dist/preconditions/types.js +17 -0
  109. package/dist/preconditions/types.js.map +1 -0
  110. package/dist/queue/index.d.ts +17 -0
  111. package/dist/queue/index.d.ts.map +1 -0
  112. package/dist/queue/index.js +17 -0
  113. package/dist/queue/index.js.map +1 -0
  114. package/dist/queue/memory-queue.d.ts +86 -0
  115. package/dist/queue/memory-queue.d.ts.map +1 -0
  116. package/dist/queue/memory-queue.js +221 -0
  117. package/dist/queue/memory-queue.js.map +1 -0
  118. package/dist/services/audit-log.d.ts +59 -0
  119. package/dist/services/audit-log.d.ts.map +1 -0
  120. package/dist/services/audit-log.js +193 -0
  121. package/dist/services/audit-log.js.map +1 -0
  122. package/dist/services/auth-error-parser.d.ts +36 -0
  123. package/dist/services/auth-error-parser.d.ts.map +1 -0
  124. package/dist/services/auth-error-parser.js +90 -0
  125. package/dist/services/auth-error-parser.js.map +1 -0
  126. package/dist/services/deployment-doctor.d.ts +109 -0
  127. package/dist/services/deployment-doctor.d.ts.map +1 -0
  128. package/dist/services/deployment-doctor.js +476 -0
  129. package/dist/services/deployment-doctor.js.map +1 -0
  130. package/dist/services/deployment-notifications.d.ts +41 -0
  131. package/dist/services/deployment-notifications.d.ts.map +1 -0
  132. package/dist/services/deployment-notifications.js +161 -0
  133. package/dist/services/deployment-notifications.js.map +1 -0
  134. package/dist/services/deployment-progress.d.ts +89 -0
  135. package/dist/services/deployment-progress.d.ts.map +1 -0
  136. package/dist/services/deployment-progress.js +244 -0
  137. package/dist/services/deployment-progress.js.map +1 -0
  138. package/dist/services/deployment-service.d.ts +97 -0
  139. package/dist/services/deployment-service.d.ts.map +1 -0
  140. package/dist/services/deployment-service.js +375 -0
  141. package/dist/services/deployment-service.js.map +1 -0
  142. package/dist/services/drift-analyzer.d.ts +86 -0
  143. package/dist/services/drift-analyzer.d.ts.map +1 -0
  144. package/dist/services/drift-analyzer.js +273 -0
  145. package/dist/services/drift-analyzer.js.map +1 -0
  146. package/dist/services/environment-setup.d.ts +97 -0
  147. package/dist/services/environment-setup.d.ts.map +1 -0
  148. package/dist/services/environment-setup.js +250 -0
  149. package/dist/services/environment-setup.js.map +1 -0
  150. package/dist/services/health-check.d.ts +168 -0
  151. package/dist/services/health-check.d.ts.map +1 -0
  152. package/dist/services/health-check.js +705 -0
  153. package/dist/services/health-check.js.map +1 -0
  154. package/dist/services/index.d.ts +39 -0
  155. package/dist/services/index.d.ts.map +1 -0
  156. package/dist/services/index.js +39 -0
  157. package/dist/services/index.js.map +1 -0
  158. package/dist/services/logger.d.ts +139 -0
  159. package/dist/services/logger.d.ts.map +1 -0
  160. package/dist/services/logger.js +268 -0
  161. package/dist/services/logger.js.map +1 -0
  162. package/dist/services/notification-service.d.ts +55 -0
  163. package/dist/services/notification-service.d.ts.map +1 -0
  164. package/dist/services/notification-service.js +184 -0
  165. package/dist/services/notification-service.js.map +1 -0
  166. package/dist/services/risk-analyzer.d.ts +252 -0
  167. package/dist/services/risk-analyzer.d.ts.map +1 -0
  168. package/dist/services/risk-analyzer.js +866 -0
  169. package/dist/services/risk-analyzer.js.map +1 -0
  170. package/dist/services/rollback.d.ts +57 -0
  171. package/dist/services/rollback.d.ts.map +1 -0
  172. package/dist/services/rollback.js +270 -0
  173. package/dist/services/rollback.js.map +1 -0
  174. package/dist/services/scheduler.d.ts +80 -0
  175. package/dist/services/scheduler.d.ts.map +1 -0
  176. package/dist/services/scheduler.js +350 -0
  177. package/dist/services/scheduler.js.map +1 -0
  178. package/dist/services/secrets.d.ts +31 -0
  179. package/dist/services/secrets.d.ts.map +1 -0
  180. package/dist/services/secrets.js +206 -0
  181. package/dist/services/secrets.js.map +1 -0
  182. package/dist/services/settings-service.d.ts +132 -0
  183. package/dist/services/settings-service.d.ts.map +1 -0
  184. package/dist/services/settings-service.js +378 -0
  185. package/dist/services/settings-service.js.map +1 -0
  186. package/dist/services/solution-diff.d.ts +127 -0
  187. package/dist/services/solution-diff.d.ts.map +1 -0
  188. package/dist/services/solution-diff.js +260 -0
  189. package/dist/services/solution-diff.js.map +1 -0
  190. package/dist/services/solution-mode-detector.d.ts +35 -0
  191. package/dist/services/solution-mode-detector.d.ts.map +1 -0
  192. package/dist/services/solution-mode-detector.js +84 -0
  193. package/dist/services/solution-mode-detector.js.map +1 -0
  194. package/dist/services/tenant-resolver.d.ts +55 -0
  195. package/dist/services/tenant-resolver.d.ts.map +1 -0
  196. package/dist/services/tenant-resolver.js +126 -0
  197. package/dist/services/tenant-resolver.js.map +1 -0
  198. package/dist/services/unmanaged-customizations.d.ts +104 -0
  199. package/dist/services/unmanaged-customizations.d.ts.map +1 -0
  200. package/dist/services/unmanaged-customizations.js +521 -0
  201. package/dist/services/unmanaged-customizations.js.map +1 -0
  202. package/dist/services/url-templater.d.ts +184 -0
  203. package/dist/services/url-templater.d.ts.map +1 -0
  204. package/dist/services/url-templater.js +327 -0
  205. package/dist/services/url-templater.js.map +1 -0
  206. package/dist/services/version-checker.d.ts +108 -0
  207. package/dist/services/version-checker.d.ts.map +1 -0
  208. package/dist/services/version-checker.js +403 -0
  209. package/dist/services/version-checker.js.map +1 -0
  210. package/dist/services/waves.d.ts +90 -0
  211. package/dist/services/waves.d.ts.map +1 -0
  212. package/dist/services/waves.js +222 -0
  213. package/dist/services/waves.js.map +1 -0
  214. package/dist/services/webhook.d.ts +95 -0
  215. package/dist/services/webhook.d.ts.map +1 -0
  216. package/dist/services/webhook.js +244 -0
  217. package/dist/services/webhook.js.map +1 -0
  218. package/dist/utils/deployment-tools.d.ts +110 -0
  219. package/dist/utils/deployment-tools.d.ts.map +1 -0
  220. package/dist/utils/deployment-tools.js +121 -0
  221. package/dist/utils/deployment-tools.js.map +1 -0
  222. package/dist/utils/index.d.ts +17 -0
  223. package/dist/utils/index.d.ts.map +1 -0
  224. package/dist/utils/index.js +18 -0
  225. package/dist/utils/index.js.map +1 -0
  226. package/package.json +49 -0
@@ -0,0 +1,866 @@
1
+ /**
2
+ * Copyright 2024 Pax8, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Deployment Risk Analyzer
18
+ * Analyzes deployment risk before execution
19
+ */
20
+ import { getDemoTenantMetadata } from "../mock/demo-data.js";
21
+ import { loadPreconditionManifest, checkPreconditions, PreconditionManifestValidationError, } from "../preconditions/index.js";
22
+ import { join } from "node:path";
23
+ /**
24
+ * Default parallelism when no wave config is supplied. Five concurrent
25
+ * deploys is what `deploy --all` does in practice and matches the
26
+ * Dataverse `importSolutionAsync` rate-limit headroom we have observed.
27
+ */
28
+ export const DEFAULT_MAX_PARALLEL = 5;
29
+ /**
30
+ * Per-tenant Dataverse `importSolutionAsync` median, in minutes. Real
31
+ * deploys land in the 30-90 second range; 1 minute is the median we use
32
+ * as the calibration point. Tighten with history once we collect it.
33
+ */
34
+ const PER_TENANT_MINUTES = 1;
35
+ /**
36
+ * Build a Phase 1 state resolver against `DEMO_TENANTS[].metadata.preconditionState`.
37
+ *
38
+ * - If a tenant has no `preconditionState` set, we synthesize a state where
39
+ * every requirement passes. "We didn't snapshot this tenant" is treated
40
+ * as "assume good"; this keeps non-demo tenants and tenants that didn't
41
+ * opt into preflight quiet.
42
+ * - If a tenant has `preconditionState` defined, we look for a snapshot
43
+ * matching every key/value in the precondition's `matcher`. Found → real
44
+ * state. Not found → "missing-resource".
45
+ *
46
+ * Phase 2: replace this whole factory with one that calls Microsoft Graph
47
+ * TCM endpoints. The diff engine and manifest format don't change.
48
+ */
49
+ function synthesizeTenantStateResolver() {
50
+ return (tenant, precondition) => {
51
+ const meta = getDemoTenantMetadata(tenant.tenantId);
52
+ const states = meta?.preconditionState;
53
+ if (!states) {
54
+ // No preflight snapshot at all — synthesize a passing state. Each
55
+ // requirement's required value lands at its `property` dot-path.
56
+ const props = {};
57
+ for (const req of precondition.requirements) {
58
+ setByDotPath(props, req.property, req.value);
59
+ }
60
+ return {
61
+ resourceType: precondition.resourceType,
62
+ resourceMatcher: precondition.matcher,
63
+ resourceDisplayName: precondition.matcher.displayName ??
64
+ Object.values(precondition.matcher)[0] ??
65
+ "(synthesized)",
66
+ currentProperties: props,
67
+ };
68
+ }
69
+ // Look for a snapshot whose matcher fields all line up.
70
+ const match = states.find((s) => s.resourceType === precondition.resourceType &&
71
+ Object.entries(precondition.matcher).every(([k, v]) => s.resourceMatcher[k] === v));
72
+ if (!match)
73
+ return "missing-resource";
74
+ return match;
75
+ };
76
+ }
77
+ /**
78
+ * Walk into `obj` along a dot-path and assign `value` at the leaf, creating
79
+ * intermediate objects as needed. Used by the synthesized passing-state.
80
+ */
81
+ function setByDotPath(obj, path, value) {
82
+ const parts = path.split(".");
83
+ let current = obj;
84
+ for (let i = 0; i < parts.length - 1; i++) {
85
+ const key = parts[i];
86
+ const next = current[key];
87
+ if (typeof next !== "object" || next === null || Array.isArray(next)) {
88
+ current[key] = {};
89
+ }
90
+ current = current[key];
91
+ }
92
+ current[parts[parts.length - 1]] = value;
93
+ }
94
+ /**
95
+ * Map a numeric confidence value to its coarse qualifier bucket.
96
+ * Boundaries: < 70 -> limited, 70..89 -> moderate, >= 90 -> high.
97
+ * Exposed (not just internal) so CLI rendering can render
98
+ * `Risk Score: LOW (limited data)` without re-implementing thresholds.
99
+ */
100
+ export function confidenceQualifier(confidence) {
101
+ if (confidence >= 90)
102
+ return "high";
103
+ if (confidence >= 70)
104
+ return "moderate";
105
+ return "limited";
106
+ }
107
+ /**
108
+ * Map a confidence qualifier to the human-readable suffix shown next to
109
+ * the risk score (e.g. `LOW (limited data)`).
110
+ */
111
+ export function confidenceQualifierLabel(qualifier) {
112
+ switch (qualifier) {
113
+ case "limited":
114
+ return "limited data";
115
+ case "moderate":
116
+ return "moderate confidence";
117
+ case "high":
118
+ return "high confidence";
119
+ }
120
+ }
121
+ /**
122
+ * Main risk analyzer class
123
+ */
124
+ export class DeploymentRiskAnalyzer {
125
+ /**
126
+ * Analyze deployment risk
127
+ */
128
+ async analyze(context) {
129
+ const issues = [];
130
+ const recommendations = [];
131
+ // Run all checks in parallel. Each check returns its issues plus a
132
+ // `dataAvailable` flag — used to compute coverage for confidence.
133
+ const [gdap, connections, health, history, preconditions] = await Promise.all([
134
+ this.checkGDAPPermissions(context),
135
+ this.checkConnections(context),
136
+ this.checkTenantHealth(context),
137
+ this.analyzeHistory(context),
138
+ this.checkPreconditions(context),
139
+ ]);
140
+ issues.push(...gdap.issues, ...connections.issues, ...health.issues, ...history.issues, ...preconditions.issues);
141
+ const coverage = [gdap, connections, health, history, preconditions].filter((r) => r.dataAvailable).length;
142
+ // Calculate risk score
143
+ const riskScore = this.calculateRiskScore(issues);
144
+ // Identify blockers (critical issues)
145
+ const blockers = issues.filter((i) => i.severity === "critical");
146
+ // Generate recommendations
147
+ const recs = this.generateRecommendations(issues, context);
148
+ recommendations.push(...recs);
149
+ // Estimate duration (wave-aware)
150
+ const duration = this.estimateDuration(context, issues);
151
+ // Calculate success probability
152
+ const successProbability = this.calculateSuccessProbability(context, issues);
153
+ // Confidence factors in coverage + per-tenant sample size
154
+ const confidence = this.calculateConfidence(context, coverage);
155
+ const qualifier = confidenceQualifier(confidence);
156
+ // Per-tenant rollup of dimensions. Issues are already attributed via
157
+ // `affectedTenants`; we just project them per-tenant and pick the
158
+ // highest-severity finding as the "top factor".
159
+ const perTenantBreakdown = this.buildPerTenantBreakdown(context, issues);
160
+ // Determine if deployment can proceed
161
+ const canProceed = blockers.length === 0;
162
+ // Determine if approval is required
163
+ const requiresApproval = riskScore === "high" || riskScore === "critical" || context.isProduction;
164
+ return {
165
+ score: riskScore,
166
+ confidence,
167
+ confidenceQualifier: qualifier,
168
+ confidence_qualifier: qualifier,
169
+ estimatedDuration: duration,
170
+ successProbability,
171
+ issues,
172
+ recommendations,
173
+ blockers,
174
+ canProceed,
175
+ requiresApproval,
176
+ perTenantBreakdown,
177
+ preconditions: {
178
+ manifestFound: preconditions.manifestFound,
179
+ solution: preconditions.manifest?.solution,
180
+ manifestVersion: preconditions.manifest?.version,
181
+ failures: preconditions.failures,
182
+ },
183
+ };
184
+ }
185
+ /**
186
+ * Check GDAP permissions for all tenants. Returns `dataAvailable=true`
187
+ * when at least one tenant produced a determination — used as a
188
+ * coverage signal for `calculateConfidence`.
189
+ */
190
+ async checkGDAPPermissions(context) {
191
+ const issues = [];
192
+ let dataAvailable = false;
193
+ // In demo mode, use tenant metadata for deterministic GDAP scenarios
194
+ if (process.env.DEMO_MODE === "true") {
195
+ // Coverage proxy: any tenant with demo metadata counts as "checked".
196
+ dataAvailable = context.tenants.some((t) => getDemoTenantMetadata(t.id) !== undefined);
197
+ const missingRoleTenants = [];
198
+ const expiredTenants = [];
199
+ const propagatingTenants = [];
200
+ const expiringSoonTenants = [];
201
+ for (const tenant of context.tenants) {
202
+ const meta = getDemoTenantMetadata(tenant.id);
203
+ if (!meta)
204
+ continue;
205
+ switch (meta.gdapStatus) {
206
+ case "missing_role":
207
+ missingRoleTenants.push(tenant.name);
208
+ break;
209
+ case "expired":
210
+ expiredTenants.push(tenant.name);
211
+ break;
212
+ case "propagating":
213
+ propagatingTenants.push(tenant.name);
214
+ break;
215
+ case "expiring_soon":
216
+ expiringSoonTenants.push(tenant.name);
217
+ break;
218
+ }
219
+ }
220
+ if (missingRoleTenants.length > 0) {
221
+ issues.push({
222
+ severity: "critical",
223
+ category: "permissions",
224
+ message: `${missingRoleTenants.length} tenant${missingRoleTenants.length > 1 ? "s" : ""} missing Power Platform Admin role`,
225
+ affectedTenants: missingRoleTenants,
226
+ resolution: "Add Power Platform Admin role to GDAP relationship in Partner Center",
227
+ link: "https://partner.microsoft.com/en-us/dashboard/commerce2/customers/delegatedadmin",
228
+ details: {
229
+ missingRole: "Power Platform Administrator",
230
+ requiredFor: ["Solution import", "Connection management", "Flow activation"],
231
+ },
232
+ });
233
+ }
234
+ if (expiredTenants.length > 0) {
235
+ issues.push({
236
+ severity: "critical",
237
+ category: "permissions",
238
+ message: `${expiredTenants.length} tenant${expiredTenants.length > 1 ? "s" : ""} with expired GDAP relationship`,
239
+ affectedTenants: expiredTenants,
240
+ resolution: "Renew GDAP relationship in Partner Center",
241
+ link: "https://partner.microsoft.com/en-us/dashboard/commerce2/customers/delegatedadmin",
242
+ details: {
243
+ issue: "GDAP relationship has ended",
244
+ impact: "Cannot perform any delegated admin operations",
245
+ },
246
+ });
247
+ }
248
+ if (propagatingTenants.length > 0) {
249
+ issues.push({
250
+ severity: "warning",
251
+ category: "permissions",
252
+ message: `${propagatingTenants.length} tenant${propagatingTenants.length > 1 ? "s" : ""} with recently added GDAP (permissions may not be propagated)`,
253
+ affectedTenants: propagatingTenants,
254
+ resolution: "Wait 24-48 hours for GDAP permissions to fully propagate",
255
+ });
256
+ }
257
+ if (expiringSoonTenants.length > 0) {
258
+ issues.push({
259
+ severity: "warning",
260
+ category: "permissions",
261
+ message: `${expiringSoonTenants.length} tenant${expiringSoonTenants.length > 1 ? "s" : ""} with GDAP relationship expiring within 7 days`,
262
+ affectedTenants: expiringSoonTenants,
263
+ resolution: "Renew GDAP relationships before they expire",
264
+ link: "https://partner.microsoft.com/en-us/dashboard/commerce2/customers/delegatedadmin",
265
+ });
266
+ }
267
+ }
268
+ // Live GDAP risk checks via Microsoft Graph API are planned for a future release.
269
+ // Currently uses demo metadata and deployment history for risk assessment.
270
+ return { issues, dataAvailable };
271
+ }
272
+ /**
273
+ * Check connection references. Returns `dataAvailable=true` when we had
274
+ * any signal (currently: demo metadata).
275
+ */
276
+ async checkConnections(context) {
277
+ const issues = [];
278
+ let dataAvailable = false;
279
+ // In demo mode, use tenant metadata for deterministic connection scenarios
280
+ if (process.env.DEMO_MODE === "true") {
281
+ dataAvailable = context.tenants.some((t) => getDemoTenantMetadata(t.id) !== undefined);
282
+ const expiredTenants = [];
283
+ const missingTenants = [];
284
+ const expiringCertTenants = [];
285
+ for (const tenant of context.tenants) {
286
+ const meta = getDemoTenantMetadata(tenant.id);
287
+ if (!meta)
288
+ continue;
289
+ switch (meta.connectionStatus) {
290
+ case "expired":
291
+ expiredTenants.push(tenant.name);
292
+ break;
293
+ case "missing":
294
+ missingTenants.push(tenant.name);
295
+ break;
296
+ case "expiring_certificate":
297
+ expiringCertTenants.push(tenant.name);
298
+ break;
299
+ }
300
+ }
301
+ if (expiredTenants.length > 0) {
302
+ issues.push({
303
+ severity: "critical",
304
+ category: "connections",
305
+ message: `${expiredTenants.length} tenant${expiredTenants.length > 1 ? "s" : ""} with expired connection references`,
306
+ affectedTenants: expiredTenants,
307
+ resolution: "Renew expired connections in the Connections page of each affected tenant",
308
+ details: {
309
+ connectionType: "OAuth",
310
+ impact: "Solution import will fail without valid connections",
311
+ },
312
+ });
313
+ }
314
+ if (missingTenants.length > 0) {
315
+ issues.push({
316
+ severity: "critical",
317
+ category: "connections",
318
+ message: `${missingTenants.length} tenant${missingTenants.length > 1 ? "s" : ""} with missing required connections`,
319
+ affectedTenants: missingTenants,
320
+ resolution: "Configure required connections before deploying",
321
+ details: {
322
+ impact: "Solution cannot function without required connections",
323
+ },
324
+ });
325
+ }
326
+ if (expiringCertTenants.length > 0) {
327
+ issues.push({
328
+ severity: "warning",
329
+ category: "connections",
330
+ message: `${expiringCertTenants.length} tenant${expiringCertTenants.length > 1 ? "s" : ""} with certificates expiring within 30 days`,
331
+ affectedTenants: expiringCertTenants,
332
+ resolution: "Rotate certificates before they expire to avoid deployment failures",
333
+ });
334
+ }
335
+ }
336
+ // Live connection reference checks via Dataverse API are planned for a future release.
337
+ // Currently uses demo metadata for connection risk assessment.
338
+ return { issues, dataAvailable };
339
+ }
340
+ /**
341
+ * Check tenant health. `dataAvailable=true` when we had health-relevant
342
+ * input (deployment history with at least one failure-bearing entry, or
343
+ * demo metadata). Note: an empty deploymentHistory still counts as "we
344
+ * looked but found nothing," not "we couldn't check."
345
+ */
346
+ async checkTenantHealth(context) {
347
+ const issues = [];
348
+ let dataAvailable = false;
349
+ // Check for recurring failures (same error appearing multiple times)
350
+ if (context.deploymentHistory) {
351
+ dataAvailable = true;
352
+ const recentFailures = context.deploymentHistory.filter((h) => h.status === "failure" &&
353
+ new Date(h.completedAt) > new Date(Date.now() - 24 * 60 * 60 * 1000));
354
+ if (recentFailures.length > 0) {
355
+ // Group failures by error message to identify recurring patterns
356
+ const errorCounts = new Map();
357
+ for (const failure of recentFailures) {
358
+ if (!failure.error)
359
+ continue;
360
+ // Extract error type (first line or first 100 chars for grouping)
361
+ const errorType = failure.error.split("\n")[0].substring(0, 100);
362
+ if (!errorCounts.has(errorType)) {
363
+ errorCounts.set(errorType, {
364
+ count: 0,
365
+ tenantIds: new Set(),
366
+ fullError: failure.error,
367
+ });
368
+ }
369
+ const errorInfo = errorCounts.get(errorType);
370
+ errorInfo.count++;
371
+ errorInfo.tenantIds.add(failure.tenantId);
372
+ }
373
+ // Only warn about recurring errors (appeared 2+ times)
374
+ const recurringErrors = Array.from(errorCounts.entries())
375
+ .filter(([_, info]) => info.count >= 2)
376
+ .sort((a, b) => b[1].count - a[1].count); // Sort by frequency
377
+ if (recurringErrors.length > 0) {
378
+ const [mostCommonError, errorInfo] = recurringErrors[0];
379
+ const affectedTenantIds = Array.from(errorInfo.tenantIds);
380
+ const tenantNames = context.tenants
381
+ .filter((t) => affectedTenantIds.includes(t.id))
382
+ .map((t) => t.name);
383
+ issues.push({
384
+ severity: "warning",
385
+ category: "health",
386
+ message: `Recurring deployment failure detected (${errorInfo.count}x in last 24 hours)`,
387
+ affectedTenants: tenantNames,
388
+ resolution: "This error has occurred multiple times. Fix the root cause before deploying again",
389
+ details: {
390
+ errorType: mostCommonError,
391
+ occurrences: errorInfo.count,
392
+ affectedTenantCount: affectedTenantIds.length,
393
+ fullError: errorInfo.fullError,
394
+ allRecurringErrors: recurringErrors.map(([err, info]) => ({
395
+ error: err,
396
+ count: info.count,
397
+ })),
398
+ },
399
+ });
400
+ }
401
+ }
402
+ }
403
+ // Demo mode supplies tenant-level health hints via metadata even when
404
+ // no deploymentHistory was provided.
405
+ if (process.env.DEMO_MODE === "true" && !dataAvailable) {
406
+ dataAvailable = context.tenants.some((t) => getDemoTenantMetadata(t.id) !== undefined);
407
+ }
408
+ return { issues, dataAvailable };
409
+ }
410
+ /**
411
+ * Analyze deployment history. `dataAvailable=true` when we had real
412
+ * deploymentHistory entries or demo metadata to draw from.
413
+ */
414
+ async analyzeHistory(context) {
415
+ const issues = [];
416
+ let dataAvailable = false;
417
+ // In demo mode, generate history issues from tenant metadata
418
+ if (process.env.DEMO_MODE === "true" && !context.deploymentHistory) {
419
+ dataAvailable = context.tenants.some((t) => getDemoTenantMetadata(t.id) !== undefined);
420
+ const tenantsWithNoDeployments = [];
421
+ const tenantsWithHighFailRate = [];
422
+ const tenantsWithRecentFailures = [];
423
+ const tenantsWithStaleData = [];
424
+ for (const tenant of context.tenants) {
425
+ const meta = getDemoTenantMetadata(tenant.id);
426
+ if (!meta) {
427
+ tenantsWithNoDeployments.push(tenant.name);
428
+ continue;
429
+ }
430
+ if (!meta.lastSuccessfulDeployment) {
431
+ tenantsWithNoDeployments.push(tenant.name);
432
+ continue;
433
+ }
434
+ // Check for stale deployments (> 30 days since last success)
435
+ const lastSuccess = new Date(meta.lastSuccessfulDeployment);
436
+ const daysSinceSuccess = (Date.now() - lastSuccess.getTime()) / (24 * 60 * 60 * 1000);
437
+ if (daysSinceSuccess > 30) {
438
+ tenantsWithStaleData.push(tenant.name);
439
+ }
440
+ // Check for high failure rate
441
+ if (meta.recentFailures >= 3) {
442
+ tenantsWithHighFailRate.push(tenant.name);
443
+ }
444
+ else if (meta.recentFailures >= 1 && meta.lastDeploymentError) {
445
+ tenantsWithRecentFailures.push(tenant.name);
446
+ }
447
+ }
448
+ if (tenantsWithNoDeployments.length > 0) {
449
+ issues.push({
450
+ severity: "info",
451
+ category: "history",
452
+ message: `${tenantsWithNoDeployments.length} tenant${tenantsWithNoDeployments.length > 1 ? "s" : ""} with no deployment history`,
453
+ affectedTenants: tenantsWithNoDeployments,
454
+ resolution: "Consider deploying to a test tenant first",
455
+ });
456
+ }
457
+ if (tenantsWithHighFailRate.length > 0) {
458
+ issues.push({
459
+ severity: "warning",
460
+ category: "history",
461
+ message: `${tenantsWithHighFailRate.length} tenant${tenantsWithHighFailRate.length > 1 ? "s" : ""} with high recent failure rate`,
462
+ affectedTenants: tenantsWithHighFailRate,
463
+ resolution: "Review and fix common failure patterns before deploying",
464
+ details: {
465
+ threshold: "3+ recent failures",
466
+ },
467
+ });
468
+ }
469
+ if (tenantsWithRecentFailures.length > 0) {
470
+ issues.push({
471
+ severity: "info",
472
+ category: "history",
473
+ message: `${tenantsWithRecentFailures.length} tenant${tenantsWithRecentFailures.length > 1 ? "s" : ""} with recent deployment failures`,
474
+ affectedTenants: tenantsWithRecentFailures,
475
+ resolution: "Monitor these tenants during deployment",
476
+ });
477
+ }
478
+ if (tenantsWithStaleData.length > 0) {
479
+ issues.push({
480
+ severity: "warning",
481
+ category: "history",
482
+ message: `${tenantsWithStaleData.length} tenant${tenantsWithStaleData.length > 1 ? "s" : ""} not deployed to in over 30 days`,
483
+ affectedTenants: tenantsWithStaleData,
484
+ resolution: "Validate environment connectivity before deploying to stale tenants",
485
+ });
486
+ }
487
+ return { issues, dataAvailable };
488
+ }
489
+ if (!context.deploymentHistory || context.deploymentHistory.length === 0) {
490
+ issues.push({
491
+ severity: "info",
492
+ category: "history",
493
+ message: "No deployment history available",
494
+ resolution: "Consider deploying to test tenant first",
495
+ });
496
+ return { issues, dataAvailable };
497
+ }
498
+ dataAvailable = true;
499
+ const totalDeployments = context.deploymentHistory.length;
500
+ const successCount = context.deploymentHistory.filter((h) => h.status === "success").length;
501
+ const successRate = (successCount / totalDeployments) * 100;
502
+ // Minimum sample size threshold: need at least 20 deployments for meaningful statistics
503
+ const MIN_SAMPLE_SIZE = 20;
504
+ if (totalDeployments < MIN_SAMPLE_SIZE) {
505
+ // Not enough data for statistical analysis
506
+ issues.push({
507
+ severity: "info",
508
+ category: "history",
509
+ message: `Limited deployment history (${totalDeployments} deployments)`,
510
+ resolution: "Build more deployment history for accurate risk assessment",
511
+ details: {
512
+ totalDeployments,
513
+ successfulDeployments: successCount,
514
+ successRate: `${successRate.toFixed(0)}%`,
515
+ minimumRequired: MIN_SAMPLE_SIZE,
516
+ },
517
+ });
518
+ }
519
+ else if (successRate < 70) {
520
+ // Enough data and success rate is low - this is a real warning
521
+ issues.push({
522
+ severity: "warning",
523
+ category: "history",
524
+ message: `Low historical success rate: ${successRate.toFixed(0)}%`,
525
+ resolution: "Review and fix common failure patterns before deploying",
526
+ details: {
527
+ totalDeployments,
528
+ successfulDeployments: successCount,
529
+ successRate: `${successRate.toFixed(0)}%`,
530
+ },
531
+ });
532
+ }
533
+ return { issues, dataAvailable };
534
+ }
535
+ /**
536
+ * Fifth dimension — tenant-config preflight. Loads a sibling YAML
537
+ * `<solution>.preconditions.yaml`, runs it against synthetic per-tenant
538
+ * state (Phase 1), and emits one `RiskIssue` per failed requirement plus a
539
+ * structured `failures[]` payload the CLI can render with deep-link / CLI
540
+ * / manual remediation steps.
541
+ *
542
+ * When no manifest is found we emit a single `info` issue ("preflight
543
+ * skipped") and return — deploy still proceeds.
544
+ *
545
+ * Phase 2 will replace `synthesizeTenantStateResolver` with a real
546
+ * Microsoft Graph TCM client; the manifest format and the diff engine
547
+ * stay the same.
548
+ */
549
+ async checkPreconditions(context) {
550
+ const issues = [];
551
+ const failures = [];
552
+ if (!context.solutionFile) {
553
+ return { issues, dataAvailable: false, manifestFound: false, failures };
554
+ }
555
+ const searchDirs = context.preconditionSearchDirs ?? [
556
+ join(process.cwd(), "agent packages"),
557
+ process.cwd(),
558
+ ];
559
+ let manifest;
560
+ try {
561
+ manifest = await loadPreconditionManifest(context.solutionFile, searchDirs);
562
+ }
563
+ catch (error) {
564
+ // Surface validation errors as a non-blocking warning rather than
565
+ // crashing analyze — the user has a manifest but it's malformed and
566
+ // they need to know why.
567
+ const message = error instanceof PreconditionManifestValidationError
568
+ ? error.message
569
+ : `Failed to load precondition manifest: ${error instanceof Error ? error.message : String(error)}`;
570
+ issues.push({
571
+ severity: "warning",
572
+ category: "preconditions",
573
+ message: "Precondition manifest failed to load",
574
+ resolution: "Fix the manifest schema errors above and re-run analyze",
575
+ details: { error: message },
576
+ });
577
+ return { issues, dataAvailable: false, manifestFound: false, failures };
578
+ }
579
+ if (!manifest) {
580
+ // Visible note — preflight skipped because there's no manifest. Not
581
+ // a warning, not blocking; just transparency about coverage.
582
+ issues.push({
583
+ severity: "info",
584
+ category: "preconditions",
585
+ message: "No precondition manifest for this solution; preflight skipped.",
586
+ });
587
+ return { issues, dataAvailable: false, manifestFound: false, failures };
588
+ }
589
+ // Build the Phase 1 state resolver from demo metadata. This is the
590
+ // single seam Phase 2 will replace with a Graph TCM client.
591
+ const resolver = synthesizeTenantStateResolver();
592
+ // The check engine wants TenantConfig[]; map our richer-but-stripped
593
+ // `Tenant` view back. We fall back to a minimal TenantConfig-shaped
594
+ // value so the resolver can read tenantId/name without a deeper lookup.
595
+ const tenantConfigs = context.tenants.map((t) => ({
596
+ name: t.name,
597
+ tenantId: t.id,
598
+ environmentUrl: t.environmentUrl,
599
+ tags: t.tags ?? [],
600
+ enabled: true,
601
+ autoSetup: true,
602
+ }));
603
+ failures.push(...checkPreconditions(manifest, tenantConfigs, resolver));
604
+ // Mirror each failure into a RiskIssue so the existing aggregate score,
605
+ // recommendations, and per-tenant breakdown all see preflight findings.
606
+ for (const failure of failures) {
607
+ issues.push({
608
+ severity: failure.severity === "error" ? "error" : "warning",
609
+ category: "preconditions",
610
+ message: `${failure.description} — ${failure.resourceDisplayName} (${failure.failedProperty})`,
611
+ affectedTenants: [failure.tenantName],
612
+ resolution: failure.remediation.title,
613
+ details: {
614
+ preconditionId: failure.preconditionId,
615
+ resourceType: failure.resourceType,
616
+ resourceDisplayName: failure.resourceDisplayName,
617
+ failedProperty: failure.failedProperty,
618
+ currentValue: failure.currentValue,
619
+ requiredValue: failure.requiredValue,
620
+ comparisonOp: failure.comparisonOp,
621
+ remediation: failure.remediation,
622
+ },
623
+ });
624
+ }
625
+ return {
626
+ issues,
627
+ dataAvailable: true,
628
+ manifestFound: true,
629
+ manifest: { solution: manifest.solution, version: manifest.version },
630
+ failures,
631
+ };
632
+ }
633
+ /**
634
+ * Calculate overall risk score
635
+ */
636
+ calculateRiskScore(issues) {
637
+ let score = 0;
638
+ for (const issue of issues) {
639
+ switch (issue.severity) {
640
+ case "critical":
641
+ score += 40;
642
+ break;
643
+ case "error":
644
+ score += 25;
645
+ break;
646
+ case "warning":
647
+ score += 10;
648
+ break;
649
+ case "info":
650
+ score += 2;
651
+ break;
652
+ }
653
+ }
654
+ if (score >= 80)
655
+ return "critical";
656
+ if (score >= 50)
657
+ return "high";
658
+ if (score >= 25)
659
+ return "medium";
660
+ return "low";
661
+ }
662
+ /**
663
+ * Calculate confidence in the risk assessment.
664
+ *
665
+ * Two inputs drive confidence:
666
+ * - **Coverage** — number of dimensions (GDAP / connections / health /
667
+ * history) that returned data. Each adds 10 points.
668
+ * - **Sample size** — median per-tenant deploy history count, log-scaled
669
+ * and capped at 40 points.
670
+ *
671
+ * Formula: `50 + 10 * coverage + min(40, ceil(log2(median_history + 1)) * 10)`.
672
+ * Clamped to [0, 100]. Lands in the 50–95% range for realistic inputs:
673
+ * - 0 coverage, 0 history -> 50
674
+ * - 4 coverage, 0 history -> 90
675
+ * - 4 coverage, 5 history -> 90 + log2(6)*10 ≈ 90, capped to 95 by
676
+ * the 40-cap fallback (40 + 50 = 90, plus +5 if median ≥ 16).
677
+ * - 4 coverage, 50+ history -> 50 + 40 + 40 = 100, clamped to 95.
678
+ */
679
+ calculateConfidence(context, coverageCount) {
680
+ const sampleSize = this.medianSampleSize(context);
681
+ const coveragePoints = 10 * coverageCount;
682
+ // log2(n+1)*10, capped at 40 — diminishing returns past ~16 deploys.
683
+ const samplePoints = Math.min(40, Math.ceil(Math.log2(sampleSize + 1)) * 10);
684
+ const confidence = 50 + coveragePoints + samplePoints;
685
+ // Cap at 95 in non-demo to leave headroom for "perfect" only when we
686
+ // actually have live API data; cap at 95 in demo too — demo is by
687
+ // construction simulated and shouldn't ever brag.
688
+ return Math.max(0, Math.min(95, confidence));
689
+ }
690
+ /**
691
+ * Median per-tenant historical deploy count for the requested solution
692
+ * (best-effort — falls back to overall demo `totalDeploys` or to the
693
+ * supplied `deploymentHistory` length / tenant count).
694
+ */
695
+ medianSampleSize(context) {
696
+ const counts = [];
697
+ if (process.env.DEMO_MODE === "true") {
698
+ for (const tenant of context.tenants) {
699
+ const meta = getDemoTenantMetadata(tenant.id);
700
+ if (meta?.deploymentHistory) {
701
+ counts.push(meta.deploymentHistory.totalDeploys);
702
+ }
703
+ else if (meta) {
704
+ counts.push(0);
705
+ }
706
+ }
707
+ }
708
+ if (context.deploymentHistory && counts.length === 0) {
709
+ // Distribute history entries evenly across tenants as a coarse proxy.
710
+ const perTenant = context.deploymentHistory.length / Math.max(context.tenants.length, 1);
711
+ for (let i = 0; i < context.tenants.length; i++)
712
+ counts.push(perTenant);
713
+ }
714
+ if (counts.length === 0)
715
+ return 0;
716
+ const sorted = [...counts].sort((a, b) => a - b);
717
+ const mid = Math.floor(sorted.length / 2);
718
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
719
+ }
720
+ /**
721
+ * Estimate deployment duration with wave-aware parallelism.
722
+ *
723
+ * Today's deploys are not sequential — `WaveService` runs up to
724
+ * `maxParallel` tenants concurrently per wave. The previous estimate
725
+ * (2 min × N tenants, sequential) overstated duration ~5×.
726
+ *
727
+ * Compute: `Σ_per_wave (ceil(wave.tenants / wave.maxParallel) ×
728
+ * PER_TENANT_MINUTES) + Σ wave.waitAfterCompletion`. When no wave plan
729
+ * is supplied, fall back to a single wave with `DEFAULT_MAX_PARALLEL`.
730
+ */
731
+ estimateDuration(context, issues) {
732
+ const waves = context.waves?.waves ?? [
733
+ {
734
+ waveNumber: 1,
735
+ name: "Default",
736
+ tenants: context.tenants.map((t) => ({
737
+ name: t.name,
738
+ tenantId: t.id,
739
+ environmentUrl: t.environmentUrl,
740
+ tags: t.tags,
741
+ enabled: true,
742
+ })),
743
+ maxParallel: context.maxParallel ?? DEFAULT_MAX_PARALLEL,
744
+ waitAfterCompletion: undefined,
745
+ continueOnFailure: false,
746
+ },
747
+ ];
748
+ let baseMinutes = 0;
749
+ for (const wave of waves) {
750
+ const parallel = wave.maxParallel ?? wave.tenants.length;
751
+ const batches = Math.ceil(wave.tenants.length / Math.max(parallel, 1));
752
+ baseMinutes += batches * PER_TENANT_MINUTES;
753
+ // waitAfterCompletion is in milliseconds; convert to minutes.
754
+ if (wave.waitAfterCompletion) {
755
+ baseMinutes += wave.waitAfterCompletion / 60_000;
756
+ }
757
+ }
758
+ // Critical/error issues add real overhead (validation, retries). Was 3
759
+ // min each; calibrated down to 2 to match observed retry costs.
760
+ const issueOverhead = issues.filter((i) => i.severity === "critical" || i.severity === "error").length * 2;
761
+ // Large solutions take longer to upload and import. Was 10 min flat;
762
+ // calibrated to 5 min for >50 MB to match real Dataverse upload medians.
763
+ const sizeOverhead = context.solutionSize && context.solutionSize > 50_000_000 ? 5 : 0;
764
+ const min = Math.max(1, Math.ceil(baseMinutes + issueOverhead + sizeOverhead));
765
+ const max = Math.ceil(min * 1.5); // 50% buffer for tail latency
766
+ return { min, max };
767
+ }
768
+ /**
769
+ * Build per-tenant rollup. For each tenant, find issues that name them
770
+ * (or that have no `affectedTenants` field — those apply to the whole
771
+ * fleet) and pick the highest-severity one as the top factor.
772
+ */
773
+ buildPerTenantBreakdown(context, issues) {
774
+ const SEVERITY_ORDER = {
775
+ critical: 4,
776
+ error: 3,
777
+ warning: 2,
778
+ info: 1,
779
+ };
780
+ return context.tenants.map((tenant) => {
781
+ const tenantIssues = issues.filter((issue) => {
782
+ if (!issue.affectedTenants || issue.affectedTenants.length === 0) {
783
+ // Fleet-wide issue — include with reduced weight (info-tier).
784
+ return issue.severity === "info";
785
+ }
786
+ return issue.affectedTenants.includes(tenant.name);
787
+ });
788
+ // Pick the single highest-severity issue as "top factor"
789
+ const top = tenantIssues
790
+ .slice()
791
+ .sort((a, b) => SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity])[0];
792
+ const score = this.calculateRiskScore(tenantIssues);
793
+ return {
794
+ tenantId: tenant.id,
795
+ tenantName: tenant.name,
796
+ score,
797
+ topFactor: top
798
+ ? { severity: top.severity, category: top.category, message: top.message }
799
+ : undefined,
800
+ };
801
+ });
802
+ }
803
+ /**
804
+ * Calculate success probability
805
+ */
806
+ calculateSuccessProbability(context, issues) {
807
+ let probability = 95; // Start optimistic
808
+ // Reduce based on issues
809
+ for (const issue of issues) {
810
+ switch (issue.severity) {
811
+ case "critical":
812
+ probability -= 25;
813
+ break;
814
+ case "error":
815
+ probability -= 15;
816
+ break;
817
+ case "warning":
818
+ probability -= 8;
819
+ break;
820
+ case "info":
821
+ probability -= 2;
822
+ break;
823
+ }
824
+ }
825
+ // Adjust based on history
826
+ if (context.deploymentHistory && context.deploymentHistory.length > 0) {
827
+ const successCount = context.deploymentHistory.filter((h) => h.status === "success").length;
828
+ const historicalRate = (successCount / context.deploymentHistory.length) * 100;
829
+ // Blend historical rate with calculated probability
830
+ probability = probability * 0.6 + historicalRate * 0.4;
831
+ }
832
+ return Math.max(Math.min(Math.round(probability), 100), 0);
833
+ }
834
+ /**
835
+ * Generate actionable recommendations
836
+ */
837
+ generateRecommendations(issues, context) {
838
+ const recommendations = [];
839
+ const criticalIssues = issues.filter((i) => i.severity === "critical");
840
+ const warningIssues = issues.filter((i) => i.severity === "warning");
841
+ if (criticalIssues.length > 0) {
842
+ recommendations.push(`Fix ${criticalIssues.length} critical issues before deploying`);
843
+ // Add specific recommendations for each critical issue
844
+ for (const issue of criticalIssues) {
845
+ if (issue.resolution) {
846
+ recommendations.push(issue.resolution);
847
+ }
848
+ }
849
+ }
850
+ if (warningIssues.length > 0 && context.tenants.length > 5) {
851
+ recommendations.push("Consider deploying to a subset of tenants first");
852
+ }
853
+ if (context.isProduction && criticalIssues.length === 0) {
854
+ recommendations.push("Deploy to test environment first to validate");
855
+ }
856
+ if (recommendations.length === 0) {
857
+ recommendations.push("All checks passed - ready to deploy");
858
+ }
859
+ return recommendations;
860
+ }
861
+ }
862
+ /**
863
+ * Singleton instance
864
+ */
865
+ export const riskAnalyzer = new DeploymentRiskAnalyzer();
866
+ //# sourceMappingURL=risk-analyzer.js.map