@friggframework/devtools 2.0.0--canary.474.27d9425.0 → 2.0.0--canary.474.97bfcf0.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.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * PropertyMismatch Entity
3
+ *
4
+ * Represents a difference between expected and actual property values
5
+ * for a CloudFormation resource.
6
+ */
7
+
8
+ const PropertyMutability = require('../value-objects/property-mutability');
9
+
10
+ class PropertyMismatch {
11
+ /**
12
+ * Create a new PropertyMismatch
13
+ *
14
+ * @param {Object} params
15
+ * @param {string} params.propertyPath - Path to the property (e.g., 'Properties.BucketName')
16
+ * @param {*} params.expectedValue - Expected property value
17
+ * @param {*} params.actualValue - Actual property value
18
+ * @param {PropertyMutability} params.mutability - Property mutability
19
+ */
20
+ constructor({ propertyPath, expectedValue, actualValue, mutability }) {
21
+ // Validate required fields
22
+ if (!propertyPath) {
23
+ throw new Error('propertyPath is required');
24
+ }
25
+
26
+ if (expectedValue === undefined) {
27
+ throw new Error('expectedValue is required');
28
+ }
29
+
30
+ if (actualValue === undefined) {
31
+ throw new Error('actualValue is required');
32
+ }
33
+
34
+ if (!mutability) {
35
+ throw new Error('mutability is required');
36
+ }
37
+
38
+ if (!(mutability instanceof PropertyMutability)) {
39
+ throw new Error('mutability must be a PropertyMutability instance');
40
+ }
41
+
42
+ this.propertyPath = propertyPath;
43
+ this.expectedValue = expectedValue;
44
+ this.actualValue = actualValue;
45
+ this.mutability = mutability;
46
+ }
47
+
48
+ /**
49
+ * Check if fixing this mismatch requires resource replacement
50
+ *
51
+ * @returns {boolean}
52
+ */
53
+ requiresReplacement() {
54
+ return this.mutability.requiresReplacement();
55
+ }
56
+
57
+ /**
58
+ * Check if this mismatch can be automatically fixed
59
+ *
60
+ * @returns {boolean}
61
+ */
62
+ canAutoFix() {
63
+ return this.mutability.canChange();
64
+ }
65
+
66
+ /**
67
+ * Get severity level of this mismatch
68
+ *
69
+ * @returns {'critical' | 'warning'}
70
+ */
71
+ getSeverity() {
72
+ return this.mutability.isImmutable() ? 'critical' : 'warning';
73
+ }
74
+
75
+ /**
76
+ * Get string representation
77
+ *
78
+ * @returns {string}
79
+ */
80
+ toString() {
81
+ const expectedStr = this.expectedValue === null ? 'null' : this.expectedValue;
82
+ const actualStr = this.actualValue === null ? 'null' : this.actualValue;
83
+
84
+ return `PropertyMismatch: ${this.propertyPath} (expected: ${expectedStr}, actual: ${actualStr}, mutability: ${this.mutability.toString()})`;
85
+ }
86
+
87
+ /**
88
+ * Serialize to JSON
89
+ *
90
+ * @returns {Object}
91
+ */
92
+ toJSON() {
93
+ return {
94
+ propertyPath: this.propertyPath,
95
+ expectedValue: this.expectedValue,
96
+ actualValue: this.actualValue,
97
+ mutability: this.mutability.toString(),
98
+ severity: this.getSeverity(),
99
+ canAutoFix: this.canAutoFix(),
100
+ requiresReplacement: this.requiresReplacement(),
101
+ };
102
+ }
103
+ }
104
+
105
+ module.exports = PropertyMismatch;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Tests for PropertyMismatch Entity
3
+ */
4
+
5
+ const PropertyMismatch = require('./property-mismatch');
6
+ const PropertyMutability = require('../value-objects/property-mutability');
7
+
8
+ describe('PropertyMismatch', () => {
9
+ describe('constructor', () => {
10
+ it('should create property mismatch with all fields', () => {
11
+ const mismatch = new PropertyMismatch({
12
+ propertyPath: 'Properties.BucketName',
13
+ expectedValue: 'my-app-prod-bucket',
14
+ actualValue: 'my-app-dev-bucket',
15
+ mutability: PropertyMutability.IMMUTABLE,
16
+ });
17
+
18
+ expect(mismatch.propertyPath).toBe('Properties.BucketName');
19
+ expect(mismatch.expectedValue).toBe('my-app-prod-bucket');
20
+ expect(mismatch.actualValue).toBe('my-app-dev-bucket');
21
+ expect(mismatch.mutability.value).toBe('IMMUTABLE');
22
+ });
23
+
24
+ it('should require propertyPath', () => {
25
+ expect(() => {
26
+ new PropertyMismatch({
27
+ expectedValue: 'value1',
28
+ actualValue: 'value2',
29
+ mutability: PropertyMutability.MUTABLE,
30
+ });
31
+ }).toThrow('propertyPath is required');
32
+ });
33
+
34
+ it('should require expectedValue', () => {
35
+ expect(() => {
36
+ new PropertyMismatch({
37
+ propertyPath: 'Properties.Name',
38
+ actualValue: 'value2',
39
+ mutability: PropertyMutability.MUTABLE,
40
+ });
41
+ }).toThrow('expectedValue is required');
42
+ });
43
+
44
+ it('should require actualValue', () => {
45
+ expect(() => {
46
+ new PropertyMismatch({
47
+ propertyPath: 'Properties.Name',
48
+ expectedValue: 'value1',
49
+ mutability: PropertyMutability.MUTABLE,
50
+ });
51
+ }).toThrow('actualValue is required');
52
+ });
53
+
54
+ it('should require mutability', () => {
55
+ expect(() => {
56
+ new PropertyMismatch({
57
+ propertyPath: 'Properties.Name',
58
+ expectedValue: 'value1',
59
+ actualValue: 'value2',
60
+ });
61
+ }).toThrow('mutability is required');
62
+ });
63
+
64
+ it('should accept null as expectedValue', () => {
65
+ const mismatch = new PropertyMismatch({
66
+ propertyPath: 'Properties.Tags',
67
+ expectedValue: null,
68
+ actualValue: ['tag1', 'tag2'],
69
+ mutability: PropertyMutability.MUTABLE,
70
+ });
71
+
72
+ expect(mismatch.expectedValue).toBeNull();
73
+ });
74
+
75
+ it('should accept null as actualValue', () => {
76
+ const mismatch = new PropertyMismatch({
77
+ propertyPath: 'Properties.Tags',
78
+ expectedValue: ['tag1', 'tag2'],
79
+ actualValue: null,
80
+ mutability: PropertyMutability.MUTABLE,
81
+ });
82
+
83
+ expect(mismatch.actualValue).toBeNull();
84
+ });
85
+ });
86
+
87
+ describe('requiresReplacement', () => {
88
+ it('should return true for immutable property', () => {
89
+ const mismatch = new PropertyMismatch({
90
+ propertyPath: 'Properties.BucketName',
91
+ expectedValue: 'bucket1',
92
+ actualValue: 'bucket2',
93
+ mutability: PropertyMutability.IMMUTABLE,
94
+ });
95
+
96
+ expect(mismatch.requiresReplacement()).toBe(true);
97
+ });
98
+
99
+ it('should return false for mutable property', () => {
100
+ const mismatch = new PropertyMismatch({
101
+ propertyPath: 'Properties.Tags',
102
+ expectedValue: ['tag1'],
103
+ actualValue: ['tag2'],
104
+ mutability: PropertyMutability.MUTABLE,
105
+ });
106
+
107
+ expect(mismatch.requiresReplacement()).toBe(false);
108
+ });
109
+
110
+ it('should return false for conditional property', () => {
111
+ const mismatch = new PropertyMismatch({
112
+ propertyPath: 'Properties.EngineVersion',
113
+ expectedValue: '5.7',
114
+ actualValue: '5.6',
115
+ mutability: PropertyMutability.CONDITIONAL,
116
+ });
117
+
118
+ expect(mismatch.requiresReplacement()).toBe(false);
119
+ });
120
+ });
121
+
122
+ describe('canAutoFix', () => {
123
+ it('should return true for mutable property', () => {
124
+ const mismatch = new PropertyMismatch({
125
+ propertyPath: 'Properties.Tags',
126
+ expectedValue: ['tag1'],
127
+ actualValue: ['tag2'],
128
+ mutability: PropertyMutability.MUTABLE,
129
+ });
130
+
131
+ expect(mismatch.canAutoFix()).toBe(true);
132
+ });
133
+
134
+ it('should return false for immutable property', () => {
135
+ const mismatch = new PropertyMismatch({
136
+ propertyPath: 'Properties.BucketName',
137
+ expectedValue: 'bucket1',
138
+ actualValue: 'bucket2',
139
+ mutability: PropertyMutability.IMMUTABLE,
140
+ });
141
+
142
+ expect(mismatch.canAutoFix()).toBe(false);
143
+ });
144
+
145
+ it('should return false for conditional property', () => {
146
+ const mismatch = new PropertyMismatch({
147
+ propertyPath: 'Properties.EngineVersion',
148
+ expectedValue: '5.7',
149
+ actualValue: '5.6',
150
+ mutability: PropertyMutability.CONDITIONAL,
151
+ });
152
+
153
+ expect(mismatch.canAutoFix()).toBe(false);
154
+ });
155
+ });
156
+
157
+ describe('getSeverity', () => {
158
+ it('should return critical for immutable property mismatch', () => {
159
+ const mismatch = new PropertyMismatch({
160
+ propertyPath: 'Properties.BucketName',
161
+ expectedValue: 'bucket1',
162
+ actualValue: 'bucket2',
163
+ mutability: PropertyMutability.IMMUTABLE,
164
+ });
165
+
166
+ expect(mismatch.getSeverity()).toBe('critical');
167
+ });
168
+
169
+ it('should return warning for mutable property mismatch', () => {
170
+ const mismatch = new PropertyMismatch({
171
+ propertyPath: 'Properties.Tags',
172
+ expectedValue: ['tag1'],
173
+ actualValue: ['tag2'],
174
+ mutability: PropertyMutability.MUTABLE,
175
+ });
176
+
177
+ expect(mismatch.getSeverity()).toBe('warning');
178
+ });
179
+
180
+ it('should return warning for conditional property mismatch', () => {
181
+ const mismatch = new PropertyMismatch({
182
+ propertyPath: 'Properties.EngineVersion',
183
+ expectedValue: '5.7',
184
+ actualValue: '5.6',
185
+ mutability: PropertyMutability.CONDITIONAL,
186
+ });
187
+
188
+ expect(mismatch.getSeverity()).toBe('warning');
189
+ });
190
+ });
191
+
192
+ describe('toString', () => {
193
+ it('should return string representation', () => {
194
+ const mismatch = new PropertyMismatch({
195
+ propertyPath: 'Properties.BucketName',
196
+ expectedValue: 'bucket1',
197
+ actualValue: 'bucket2',
198
+ mutability: PropertyMutability.IMMUTABLE,
199
+ });
200
+
201
+ expect(mismatch.toString()).toBe(
202
+ 'PropertyMismatch: Properties.BucketName (expected: bucket1, actual: bucket2, mutability: IMMUTABLE)'
203
+ );
204
+ });
205
+
206
+ it('should handle null expected value', () => {
207
+ const mismatch = new PropertyMismatch({
208
+ propertyPath: 'Properties.Tags',
209
+ expectedValue: null,
210
+ actualValue: ['tag1'],
211
+ mutability: PropertyMutability.MUTABLE,
212
+ });
213
+
214
+ expect(mismatch.toString()).toContain('expected: null');
215
+ });
216
+
217
+ it('should handle null actual value', () => {
218
+ const mismatch = new PropertyMismatch({
219
+ propertyPath: 'Properties.Tags',
220
+ expectedValue: ['tag1'],
221
+ actualValue: null,
222
+ mutability: PropertyMutability.MUTABLE,
223
+ });
224
+
225
+ expect(mismatch.toString()).toContain('actual: null');
226
+ });
227
+ });
228
+
229
+ describe('toJSON', () => {
230
+ it('should serialize to JSON', () => {
231
+ const mismatch = new PropertyMismatch({
232
+ propertyPath: 'Properties.BucketName',
233
+ expectedValue: 'bucket1',
234
+ actualValue: 'bucket2',
235
+ mutability: PropertyMutability.IMMUTABLE,
236
+ });
237
+
238
+ const json = mismatch.toJSON();
239
+
240
+ expect(json).toEqual({
241
+ propertyPath: 'Properties.BucketName',
242
+ expectedValue: 'bucket1',
243
+ actualValue: 'bucket2',
244
+ mutability: 'IMMUTABLE',
245
+ severity: 'critical',
246
+ canAutoFix: false,
247
+ requiresReplacement: true,
248
+ });
249
+ });
250
+ });
251
+ });
@@ -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;