@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.46

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 (212) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/HEALTH.md +468 -0
  3. package/infrastructure/README.md +51 -0
  4. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  5. package/infrastructure/__tests__/template-generation.test.js +687 -0
  6. package/infrastructure/create-frigg-infrastructure.js +1 -1
  7. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  8. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  9. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  10. package/infrastructure/domains/database/aurora-builder.js +809 -0
  11. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  12. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  13. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  14. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  15. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  16. package/infrastructure/domains/database/migration-builder.js +633 -0
  17. package/infrastructure/domains/database/migration-builder.test.js +294 -0
  18. package/infrastructure/domains/database/migration-resolver.js +163 -0
  19. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  20. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  21. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  22. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  23. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  24. package/infrastructure/domains/health/application/ports/index.js +26 -0
  25. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  26. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  27. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  28. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  29. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  30. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  31. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  32. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  33. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  34. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  35. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  36. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  37. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  38. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  39. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  40. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  41. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  42. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  43. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  44. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  45. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  46. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  47. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  48. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  49. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  50. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  51. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  52. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  53. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  54. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  55. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  56. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  57. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  58. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  59. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  60. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  61. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  62. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  63. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  64. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  65. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  66. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  67. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  68. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  69. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  70. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  71. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  72. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  73. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  74. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  75. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  76. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  77. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  78. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  84. package/infrastructure/domains/integration/integration-builder.js +397 -0
  85. package/infrastructure/domains/integration/integration-builder.test.js +593 -0
  86. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  87. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  88. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  89. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  90. package/infrastructure/domains/networking/vpc-builder.js +1829 -0
  91. package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
  92. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  93. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  94. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  95. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  96. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  97. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  98. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  99. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  100. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  101. package/infrastructure/domains/security/kms-builder.js +366 -0
  102. package/infrastructure/domains/security/kms-builder.test.js +374 -0
  103. package/infrastructure/domains/security/kms-discovery.js +80 -0
  104. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  105. package/infrastructure/domains/security/kms-resolver.js +96 -0
  106. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  107. package/infrastructure/domains/shared/base-builder.js +112 -0
  108. package/infrastructure/domains/shared/base-resolver.js +186 -0
  109. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  110. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  111. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  112. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  113. package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
  114. package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
  115. package/infrastructure/domains/shared/environment-builder.js +119 -0
  116. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  117. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
  118. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
  119. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  120. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  121. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  122. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  123. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  124. package/infrastructure/domains/shared/resource-discovery.js +192 -0
  125. package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
  126. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  127. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  128. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  129. package/infrastructure/domains/shared/types/index.js +46 -0
  130. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  131. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  132. package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
  133. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  134. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  135. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  136. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  137. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  138. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
  139. package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
  140. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  141. package/infrastructure/esbuild.config.js +53 -0
  142. package/infrastructure/infrastructure-composer.js +87 -0
  143. package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
  144. package/infrastructure/scripts/build-prisma-layer.js +553 -0
  145. package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
  146. package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
  147. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  148. package/layers/prisma/nodejs/package.json +8 -0
  149. package/management-ui/server/utils/cliIntegration.js +1 -1
  150. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  151. package/package.json +11 -11
  152. package/frigg-cli/.eslintrc.js +0 -141
  153. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
  154. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
  155. package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
  156. package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
  157. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
  158. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
  159. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
  160. package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
  161. package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
  162. package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
  163. package/frigg-cli/__tests__/utils/test-setup.js +0 -287
  164. package/frigg-cli/build-command/index.js +0 -65
  165. package/frigg-cli/db-setup-command/index.js +0 -193
  166. package/frigg-cli/deploy-command/index.js +0 -175
  167. package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
  168. package/frigg-cli/generate-command/azure-generator.js +0 -43
  169. package/frigg-cli/generate-command/gcp-generator.js +0 -47
  170. package/frigg-cli/generate-command/index.js +0 -332
  171. package/frigg-cli/generate-command/terraform-generator.js +0 -555
  172. package/frigg-cli/generate-iam-command.js +0 -118
  173. package/frigg-cli/index.js +0 -75
  174. package/frigg-cli/index.test.js +0 -158
  175. package/frigg-cli/init-command/backend-first-handler.js +0 -756
  176. package/frigg-cli/init-command/index.js +0 -93
  177. package/frigg-cli/init-command/template-handler.js +0 -143
  178. package/frigg-cli/install-command/backend-js.js +0 -33
  179. package/frigg-cli/install-command/commit-changes.js +0 -16
  180. package/frigg-cli/install-command/environment-variables.js +0 -127
  181. package/frigg-cli/install-command/environment-variables.test.js +0 -136
  182. package/frigg-cli/install-command/index.js +0 -54
  183. package/frigg-cli/install-command/install-package.js +0 -13
  184. package/frigg-cli/install-command/integration-file.js +0 -30
  185. package/frigg-cli/install-command/logger.js +0 -12
  186. package/frigg-cli/install-command/template.js +0 -90
  187. package/frigg-cli/install-command/validate-package.js +0 -75
  188. package/frigg-cli/jest.config.js +0 -124
  189. package/frigg-cli/package.json +0 -54
  190. package/frigg-cli/start-command/index.js +0 -149
  191. package/frigg-cli/start-command/start-command.test.js +0 -297
  192. package/frigg-cli/test/init-command.test.js +0 -180
  193. package/frigg-cli/test/npm-registry.test.js +0 -319
  194. package/frigg-cli/ui-command/index.js +0 -154
  195. package/frigg-cli/utils/app-resolver.js +0 -319
  196. package/frigg-cli/utils/backend-path.js +0 -25
  197. package/frigg-cli/utils/database-validator.js +0 -161
  198. package/frigg-cli/utils/error-messages.js +0 -257
  199. package/frigg-cli/utils/npm-registry.js +0 -167
  200. package/frigg-cli/utils/prisma-runner.js +0 -280
  201. package/frigg-cli/utils/process-manager.js +0 -199
  202. package/frigg-cli/utils/repo-detection.js +0 -405
  203. package/infrastructure/aws-discovery.js +0 -1176
  204. package/infrastructure/aws-discovery.test.js +0 -1220
  205. package/infrastructure/serverless-template.js +0 -2094
  206. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  207. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  208. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  209. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  210. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  211. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  212. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * UpdateProgressMonitor - Monitor CloudFormation Update Operation Progress
3
+ *
4
+ * Domain Layer - Service
5
+ *
6
+ * Monitors CloudFormation UPDATE operations by polling stack events and tracking
7
+ * resource update progress. Provides real-time progress callbacks and detects
8
+ * failures, rollbacks, and timeouts.
9
+ *
10
+ * Responsibilities:
11
+ * - Poll CloudFormation stack events during update
12
+ * - Track progress per resource (IN_PROGRESS, COMPLETE, FAILED)
13
+ * - Detect stack rollback states
14
+ * - Timeout after 5 minutes
15
+ * - Provide progress callbacks for UI updates
16
+ */
17
+
18
+ class UpdateProgressMonitor {
19
+ /**
20
+ * Create progress monitor with CloudFormation repository dependency
21
+ *
22
+ * @param {Object} params
23
+ * @param {Object} params.cloudFormationRepository - CloudFormation operations
24
+ */
25
+ constructor({ cloudFormationRepository }) {
26
+ if (!cloudFormationRepository) {
27
+ throw new Error('cloudFormationRepository is required');
28
+ }
29
+ this.cfRepo = cloudFormationRepository;
30
+ }
31
+
32
+ /**
33
+ * Monitor update operation progress
34
+ *
35
+ * Polls CloudFormation stack events every 2 seconds to track resource update progress.
36
+ * Calls onProgress callback with status updates for each resource.
37
+ * Detects failures, rollbacks, and timeouts.
38
+ *
39
+ * @param {Object} params
40
+ * @param {Object} params.stackIdentifier - Stack identifier { stackName, region }
41
+ * @param {Array<string>} params.resourceLogicalIds - Logical IDs to track
42
+ * @param {Function} params.onProgress - Progress callback function
43
+ * @returns {Promise<Object>} Update result
44
+ */
45
+ async monitorUpdate({ stackIdentifier, resourceLogicalIds, onProgress }) {
46
+ // If no resources to track, return immediately
47
+ if (!resourceLogicalIds || resourceLogicalIds.length === 0) {
48
+ return {
49
+ success: true,
50
+ updatedCount: 0,
51
+ failedCount: 0,
52
+ failedResources: [],
53
+ };
54
+ }
55
+
56
+ const updatedResources = new Set();
57
+ const failedResources = [];
58
+ const processedEvents = new Set(); // Track processed events by timestamp + logicalId
59
+ let elapsedTime = 0; // Track elapsed time manually for fake timers compatibility
60
+ const TIMEOUT_MS = 300000; // 5 minutes
61
+ const POLL_INTERVAL_MS = 2000; // 2 seconds
62
+
63
+ // Continue polling until all resources are complete or failed
64
+ while (
65
+ updatedResources.size + failedResources.length <
66
+ resourceLogicalIds.length
67
+ ) {
68
+ // Wait 2 seconds before polling
69
+ await this._delay(POLL_INTERVAL_MS);
70
+ elapsedTime += POLL_INTERVAL_MS;
71
+
72
+ // Check for timeout
73
+ if (elapsedTime > TIMEOUT_MS) {
74
+ throw new Error('Update operation timed out');
75
+ }
76
+
77
+ // Get stack events
78
+ const events = await this.cfRepo.getStackEvents({
79
+ stackIdentifier,
80
+ });
81
+
82
+ // Sort events by timestamp (oldest first) for consistent processing
83
+ const sortedEvents = [...events].sort(
84
+ (a, b) => new Date(a.Timestamp) - new Date(b.Timestamp)
85
+ );
86
+
87
+ // Process events for tracked resources
88
+ for (const event of sortedEvents) {
89
+ const logicalId = event.LogicalResourceId;
90
+
91
+ // Skip if not a tracked resource
92
+ if (!resourceLogicalIds.includes(logicalId)) {
93
+ continue;
94
+ }
95
+
96
+ // Create unique event key to avoid duplicate processing
97
+ const eventKey = `${event.Timestamp.toISOString()}_${logicalId}_${event.ResourceStatus}`;
98
+
99
+ // Skip if already processed
100
+ if (processedEvents.has(eventKey)) {
101
+ continue;
102
+ }
103
+
104
+ processedEvents.add(eventKey);
105
+
106
+ // Handle different resource statuses
107
+ if (event.ResourceStatus === 'UPDATE_IN_PROGRESS') {
108
+ // Call progress callback with IN_PROGRESS status
109
+ if (onProgress) {
110
+ onProgress({
111
+ logicalId,
112
+ status: 'IN_PROGRESS',
113
+ });
114
+ }
115
+ } else if (event.ResourceStatus === 'UPDATE_COMPLETE') {
116
+ // Mark resource as updated
117
+ updatedResources.add(logicalId);
118
+
119
+ // Call progress callback with COMPLETE status
120
+ if (onProgress) {
121
+ onProgress({
122
+ logicalId,
123
+ status: 'COMPLETE',
124
+ progress: updatedResources.size,
125
+ total: resourceLogicalIds.length,
126
+ });
127
+ }
128
+ } else if (event.ResourceStatus === 'UPDATE_FAILED') {
129
+ // Add to failed resources
130
+ const reason = event.ResourceStatusReason || 'Unknown error';
131
+ failedResources.push({
132
+ logicalId,
133
+ reason,
134
+ });
135
+
136
+ // Call progress callback with FAILED status
137
+ if (onProgress) {
138
+ onProgress({
139
+ logicalId,
140
+ status: 'FAILED',
141
+ reason,
142
+ });
143
+ }
144
+ }
145
+ }
146
+
147
+ // Check if all resources are now accounted for
148
+ const allResourcesProcessed =
149
+ updatedResources.size + failedResources.length >=
150
+ resourceLogicalIds.length;
151
+
152
+ // If all resources processed, exit loop to return result
153
+ if (allResourcesProcessed) {
154
+ break;
155
+ }
156
+
157
+ // Check stack status AFTER processing events - if rollback in progress, throw
158
+ const stackStatus = await this.cfRepo.getStackStatus(stackIdentifier);
159
+ if (
160
+ stackStatus.includes('ROLLBACK') &&
161
+ stackStatus !== 'UPDATE_ROLLBACK_COMPLETE'
162
+ ) {
163
+ throw new Error('Update operation failed and rolled back');
164
+ }
165
+ }
166
+
167
+ // Check final stack status before returning
168
+ const finalStackStatus = await this.cfRepo.getStackStatus(stackIdentifier);
169
+
170
+ // Return result (success = no failures)
171
+ const success = failedResources.length === 0;
172
+ return {
173
+ success,
174
+ updatedCount: updatedResources.size,
175
+ failedCount: failedResources.length,
176
+ failedResources,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Delay helper for polling intervals
182
+ *
183
+ * @param {number} ms - Milliseconds to delay
184
+ * @returns {Promise<void>}
185
+ * @private
186
+ */
187
+ async _delay(ms) {
188
+ return new Promise((resolve) => setTimeout(resolve, ms));
189
+ }
190
+ }
191
+
192
+ module.exports = { UpdateProgressMonitor };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * HealthScore Value Object
3
+ *
4
+ * Immutable health score from 0-100 with qualitative assessment
5
+ * - 80-100: healthy
6
+ * - 40-79: degraded
7
+ * - 0-39: unhealthy
8
+ */
9
+
10
+ class HealthScore {
11
+ /**
12
+ * Create a new HealthScore
13
+ *
14
+ * @param {number} value - Score from 0 to 100
15
+ */
16
+ constructor(value) {
17
+ // Validate type
18
+ if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) {
19
+ throw new Error('Health score must be a number');
20
+ }
21
+
22
+ // Validate range
23
+ if (value < 0 || value > 100) {
24
+ throw new Error('Health score must be between 0 and 100');
25
+ }
26
+
27
+ // Assign property
28
+ this._value = value;
29
+
30
+ // Make immutable
31
+ Object.freeze(this);
32
+ }
33
+
34
+ /**
35
+ * Get score value
36
+ * @returns {number}
37
+ */
38
+ get value() {
39
+ return this._value;
40
+ }
41
+
42
+ /**
43
+ * Prevent modification of value
44
+ * @throws {TypeError}
45
+ */
46
+ set value(newValue) {
47
+ throw new TypeError('Cannot modify immutable property value');
48
+ }
49
+
50
+ /**
51
+ * Get qualitative assessment
52
+ *
53
+ * @returns {'healthy' | 'degraded' | 'unhealthy'}
54
+ */
55
+ qualitativeAssessment() {
56
+ if (this._value >= 80) {
57
+ return 'healthy';
58
+ } else if (this._value >= 40) {
59
+ return 'degraded';
60
+ } else {
61
+ return 'unhealthy';
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check if score is healthy (>= 80)
67
+ *
68
+ * @returns {boolean}
69
+ */
70
+ isHealthy() {
71
+ return this._value >= 80;
72
+ }
73
+
74
+ /**
75
+ * Check if score is degraded (40-79)
76
+ *
77
+ * @returns {boolean}
78
+ */
79
+ isDegraded() {
80
+ return this._value >= 40 && this._value < 80;
81
+ }
82
+
83
+ /**
84
+ * Check if score is unhealthy (< 40)
85
+ *
86
+ * @returns {boolean}
87
+ */
88
+ isUnhealthy() {
89
+ return this._value < 40;
90
+ }
91
+
92
+ /**
93
+ * Get string representation
94
+ *
95
+ * @returns {string}
96
+ */
97
+ toString() {
98
+ return `${this._value} (${this.qualitativeAssessment()})`;
99
+ }
100
+
101
+ /**
102
+ * Create perfect health score (100)
103
+ *
104
+ * @returns {HealthScore}
105
+ */
106
+ static perfect() {
107
+ return new HealthScore(100);
108
+ }
109
+
110
+ /**
111
+ * Create failed health score (0)
112
+ *
113
+ * @returns {HealthScore}
114
+ */
115
+ static failed() {
116
+ return new HealthScore(0);
117
+ }
118
+
119
+ /**
120
+ * Create HealthScore from percentage (0.0 to 1.0)
121
+ *
122
+ * @param {number} percentage - Percentage as decimal (0.75 = 75%)
123
+ * @returns {HealthScore}
124
+ */
125
+ static fromPercentage(percentage) {
126
+ if (typeof percentage !== 'number' || isNaN(percentage) || !isFinite(percentage)) {
127
+ throw new Error('Percentage must be a number');
128
+ }
129
+
130
+ if (percentage < 0 || percentage > 1) {
131
+ throw new Error('Percentage must be between 0 and 1');
132
+ }
133
+
134
+ return new HealthScore(Math.round(percentage * 100));
135
+ }
136
+ }
137
+
138
+ module.exports = HealthScore;
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Tests for HealthScore Value Object
3
+ */
4
+
5
+ const HealthScore = require('./health-score');
6
+
7
+ describe('HealthScore', () => {
8
+ describe('constructor', () => {
9
+ it('should create health score with valid value', () => {
10
+ const score = new HealthScore(75);
11
+
12
+ expect(score.value).toBe(75);
13
+ });
14
+
15
+ it('should accept 0 as minimum score', () => {
16
+ const score = new HealthScore(0);
17
+
18
+ expect(score.value).toBe(0);
19
+ });
20
+
21
+ it('should accept 100 as maximum score', () => {
22
+ const score = new HealthScore(100);
23
+
24
+ expect(score.value).toBe(100);
25
+ });
26
+
27
+ it('should reject negative scores', () => {
28
+ expect(() => {
29
+ new HealthScore(-1);
30
+ }).toThrow('Health score must be between 0 and 100');
31
+ });
32
+
33
+ it('should reject scores above 100', () => {
34
+ expect(() => {
35
+ new HealthScore(101);
36
+ }).toThrow('Health score must be between 0 and 100');
37
+ });
38
+
39
+ it('should reject non-numeric scores', () => {
40
+ expect(() => {
41
+ new HealthScore('75');
42
+ }).toThrow('Health score must be a number');
43
+ });
44
+
45
+ it('should reject NaN', () => {
46
+ expect(() => {
47
+ new HealthScore(NaN);
48
+ }).toThrow('Health score must be a number');
49
+ });
50
+
51
+ it('should reject Infinity', () => {
52
+ expect(() => {
53
+ new HealthScore(Infinity);
54
+ }).toThrow('Health score must be a number');
55
+ });
56
+ });
57
+
58
+ describe('qualitativeAssessment', () => {
59
+ it('should return "healthy" for score 100', () => {
60
+ const score = new HealthScore(100);
61
+
62
+ expect(score.qualitativeAssessment()).toBe('healthy');
63
+ });
64
+
65
+ it('should return "healthy" for score 90', () => {
66
+ const score = new HealthScore(90);
67
+
68
+ expect(score.qualitativeAssessment()).toBe('healthy');
69
+ });
70
+
71
+ it('should return "healthy" for score 80', () => {
72
+ const score = new HealthScore(80);
73
+
74
+ expect(score.qualitativeAssessment()).toBe('healthy');
75
+ });
76
+
77
+ it('should return "degraded" for score 79', () => {
78
+ const score = new HealthScore(79);
79
+
80
+ expect(score.qualitativeAssessment()).toBe('degraded');
81
+ });
82
+
83
+ it('should return "degraded" for score 50', () => {
84
+ const score = new HealthScore(50);
85
+
86
+ expect(score.qualitativeAssessment()).toBe('degraded');
87
+ });
88
+
89
+ it('should return "degraded" for score 40', () => {
90
+ const score = new HealthScore(40);
91
+
92
+ expect(score.qualitativeAssessment()).toBe('degraded');
93
+ });
94
+
95
+ it('should return "unhealthy" for score 39', () => {
96
+ const score = new HealthScore(39);
97
+
98
+ expect(score.qualitativeAssessment()).toBe('unhealthy');
99
+ });
100
+
101
+ it('should return "unhealthy" for score 0', () => {
102
+ const score = new HealthScore(0);
103
+
104
+ expect(score.qualitativeAssessment()).toBe('unhealthy');
105
+ });
106
+ });
107
+
108
+ describe('isHealthy', () => {
109
+ it('should return true for score 80', () => {
110
+ const score = new HealthScore(80);
111
+
112
+ expect(score.isHealthy()).toBe(true);
113
+ });
114
+
115
+ it('should return true for score 100', () => {
116
+ const score = new HealthScore(100);
117
+
118
+ expect(score.isHealthy()).toBe(true);
119
+ });
120
+
121
+ it('should return false for score 79', () => {
122
+ const score = new HealthScore(79);
123
+
124
+ expect(score.isHealthy()).toBe(false);
125
+ });
126
+
127
+ it('should return false for score 0', () => {
128
+ const score = new HealthScore(0);
129
+
130
+ expect(score.isHealthy()).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe('isDegraded', () => {
135
+ it('should return true for score 79', () => {
136
+ const score = new HealthScore(79);
137
+
138
+ expect(score.isDegraded()).toBe(true);
139
+ });
140
+
141
+ it('should return true for score 40', () => {
142
+ const score = new HealthScore(40);
143
+
144
+ expect(score.isDegraded()).toBe(true);
145
+ });
146
+
147
+ it('should return false for score 80', () => {
148
+ const score = new HealthScore(80);
149
+
150
+ expect(score.isDegraded()).toBe(false);
151
+ });
152
+
153
+ it('should return false for score 39', () => {
154
+ const score = new HealthScore(39);
155
+
156
+ expect(score.isDegraded()).toBe(false);
157
+ });
158
+ });
159
+
160
+ describe('isUnhealthy', () => {
161
+ it('should return true for score 39', () => {
162
+ const score = new HealthScore(39);
163
+
164
+ expect(score.isUnhealthy()).toBe(true);
165
+ });
166
+
167
+ it('should return true for score 0', () => {
168
+ const score = new HealthScore(0);
169
+
170
+ expect(score.isUnhealthy()).toBe(true);
171
+ });
172
+
173
+ it('should return false for score 40', () => {
174
+ const score = new HealthScore(40);
175
+
176
+ expect(score.isUnhealthy()).toBe(false);
177
+ });
178
+
179
+ it('should return false for score 80', () => {
180
+ const score = new HealthScore(80);
181
+
182
+ expect(score.isUnhealthy()).toBe(false);
183
+ });
184
+ });
185
+
186
+ describe('toString', () => {
187
+ it('should return string representation for healthy score', () => {
188
+ const score = new HealthScore(100);
189
+
190
+ expect(score.toString()).toBe('100 (healthy)');
191
+ });
192
+
193
+ it('should return string representation for degraded score', () => {
194
+ const score = new HealthScore(50);
195
+
196
+ expect(score.toString()).toBe('50 (degraded)');
197
+ });
198
+
199
+ it('should return string representation for unhealthy score', () => {
200
+ const score = new HealthScore(25);
201
+
202
+ expect(score.toString()).toBe('25 (unhealthy)');
203
+ });
204
+ });
205
+
206
+ describe('static factory methods', () => {
207
+ it('should create perfect score', () => {
208
+ const score = HealthScore.perfect();
209
+
210
+ expect(score.value).toBe(100);
211
+ expect(score.isHealthy()).toBe(true);
212
+ });
213
+
214
+ it('should create failed score', () => {
215
+ const score = HealthScore.failed();
216
+
217
+ expect(score.value).toBe(0);
218
+ expect(score.isUnhealthy()).toBe(true);
219
+ });
220
+
221
+ it('should create from percentage (1.0 = 100)', () => {
222
+ const score = HealthScore.fromPercentage(0.75);
223
+
224
+ expect(score.value).toBe(75);
225
+ });
226
+
227
+ it('should create from percentage (0.0 = 0)', () => {
228
+ const score = HealthScore.fromPercentage(0);
229
+
230
+ expect(score.value).toBe(0);
231
+ });
232
+
233
+ it('should create from percentage (1.0 = 100)', () => {
234
+ const score = HealthScore.fromPercentage(1.0);
235
+
236
+ expect(score.value).toBe(100);
237
+ });
238
+
239
+ it('should reject percentage below 0', () => {
240
+ expect(() => {
241
+ HealthScore.fromPercentage(-0.1);
242
+ }).toThrow('Percentage must be between 0 and 1');
243
+ });
244
+
245
+ it('should reject percentage above 1', () => {
246
+ expect(() => {
247
+ HealthScore.fromPercentage(1.1);
248
+ }).toThrow('Percentage must be between 0 and 1');
249
+ });
250
+ });
251
+
252
+ describe('immutability', () => {
253
+ it('should not allow modification of value', () => {
254
+ const score = new HealthScore(75);
255
+
256
+ expect(() => {
257
+ score.value = 50;
258
+ }).toThrow();
259
+ });
260
+
261
+ it('should be frozen', () => {
262
+ const score = new HealthScore(75);
263
+
264
+ expect(Object.isFrozen(score)).toBe(true);
265
+ });
266
+ });
267
+ });