@friggframework/devtools 2.0.0--canary.474.86c5119.0 → 2.0.0--canary.474.898a56c.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 (24) hide show
  1. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  2. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +762 -0
  3. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +154 -1
  4. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +20 -5
  5. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +3 -3
  6. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  7. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  8. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  9. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  10. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  11. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  12. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  13. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +645 -0
  14. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  15. package/infrastructure/domains/health/domain/services/health-score-calculator.js +174 -91
  16. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +332 -228
  17. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +330 -0
  18. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  19. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  20. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  21. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  22. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +108 -14
  23. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
  24. package/package.json +6 -6
@@ -0,0 +1,324 @@
1
+ # Build Template vs Deployed Template Analysis
2
+
3
+ **CRITICAL DISCOVERY**: The local build template and deployed CloudFormation template are DIFFERENT!
4
+
5
+ ## The Discrepancy
6
+
7
+ ### Local Build Template (.serverless/cloudformation-template-update-stack.json)
8
+
9
+ **Contains VPC Resources:**
10
+ ```json
11
+ {
12
+ "Resources": {
13
+ "FriggVPC": { "Type": "AWS::EC2::VPC" },
14
+ "FriggPrivateSubnet1": { "Type": "AWS::EC2::Subnet" },
15
+ "FriggPrivateSubnet2": { "Type": "AWS::EC2::Subnet" },
16
+ "FriggPublicSubnet": { "Type": "AWS::EC2::Subnet" },
17
+ "FriggPublicSubnet2": { "Type": "AWS::EC2::Subnet" },
18
+ "FriggLambdaSecurityGroup": { "Type": "AWS::EC2::SecurityGroup" },
19
+ "FriggVPCEndpointSecurityGroup": { "Type": "AWS::EC2::SecurityGroup" }
20
+ }
21
+ }
22
+ ```
23
+
24
+ **Lambda VPC Config uses Refs:**
25
+ ```json
26
+ {
27
+ "VpcConfig": {
28
+ "SecurityGroupIds": [{ "Ref": "FriggLambdaSecurityGroup" }],
29
+ "SubnetIds": [
30
+ { "Ref": "FriggPrivateSubnet1" },
31
+ { "Ref": "FriggPrivateSubnet2" }
32
+ ]
33
+ }
34
+ }
35
+ ```
36
+
37
+ ### Deployed CloudFormation Template (in AWS)
38
+
39
+ **Does NOT contain VPC Resources:**
40
+ - ✅ Has Lambda functions
41
+ - ✅ Has SQS queues
42
+ - ✅ Has IAM roles
43
+ - ❌ **NO VPC resources** (FriggVPC, FriggPrivateSubnet1, FriggPrivateSubnet2, etc.)
44
+
45
+ **Lambda VPC Config uses Hardcoded Physical IDs:**
46
+ ```json
47
+ {
48
+ "VpcConfig": {
49
+ "SecurityGroupIds": ["sg-07c01370e830b6ad6"], // Hardcoded physical ID
50
+ "SubnetIds": [
51
+ "subnet-00ab9e0502e66aac3", // Hardcoded physical ID
52
+ "subnet-00d085a52937aaf91" // Hardcoded physical ID
53
+ ]
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## What This Means
59
+
60
+ ### 1. VPC Resources Were Removed from Stack
61
+
62
+ At some point, the VPC resources were removed from CloudFormation management:
63
+ - VPC was created by CloudFormation originally
64
+ - VPC was later removed from the template/stack (but physical resources left in AWS)
65
+ - Lambda functions still reference the VPC subnets/SG by physical ID
66
+
67
+ ### 2. Local Build != Deployed State
68
+
69
+ **If you deploy the local build template now:**
70
+ - CloudFormation will try to CREATE new VPC resources (FriggVPC, subnets, SG)
71
+ - CloudFormation will FAIL because resources with those logical IDs already exist physically
72
+ - OR it will create NEW resources with different physical IDs
73
+ - Lambda functions will reference the NEW resources (via Ref)
74
+
75
+ ### 3. Import Operation is Complex
76
+
77
+ When you import `vpc-0eadd96976d29ede7`:
78
+ - You need to map it to the logical ID `FriggVPC` in the template
79
+ - You need to import ALL related resources:
80
+ - `FriggPrivateSubnet1` → `subnet-00ab9e0502e66aac3`
81
+ - `FriggPrivateSubnet2` → `subnet-00d085a52937aaf91`
82
+ - `FriggLambdaSecurityGroup` → `sg-07c01370e830b6ad6`
83
+ - etc.
84
+
85
+ ## CloudFormation Import Process
86
+
87
+ ### How Import Works
88
+
89
+ **Q: Will it know to grab the right VPC?**
90
+
91
+ **A:** ❌ **NO, you must explicitly tell it which physical ID maps to which logical ID**
92
+
93
+ CloudFormation import requires:
94
+ ```json
95
+ {
96
+ "Resources": [
97
+ {
98
+ "ResourceType": "AWS::EC2::VPC",
99
+ "LogicalResourceId": "FriggVPC",
100
+ "ResourceIdentifier": {
101
+ "VpcId": "vpc-0eadd96976d29ede7" // You specify this
102
+ }
103
+ },
104
+ {
105
+ "ResourceType": "AWS::EC2::Subnet",
106
+ "LogicalResourceId": "FriggPrivateSubnet1",
107
+ "ResourceIdentifier": {
108
+ "SubnetId": "subnet-00ab9e0502e66aac3" // You specify this
109
+ }
110
+ },
111
+ // ... more resources
112
+ ]
113
+ }
114
+ ```
115
+
116
+ **Q: Will it clear or delete the old resources?**
117
+
118
+ **A:** ❌ **NO, you must manually delete unused resources**
119
+
120
+ CloudFormation import:
121
+ - ✅ Adds existing resources to the stack (doesn't create or delete anything)
122
+ - ✅ Updates Lambda Refs to point to imported resources
123
+ - ❌ Does NOT delete the 2 unused VPCs
124
+ - ❌ Does NOT clean up orphaned resources
125
+
126
+ ## The Right Approach
127
+
128
+ ### Step 1: Update Local Build Template to Match Deployed State
129
+
130
+ **OPTION A: Remove VPC from local template (quick fix)**
131
+
132
+ Remove VPC resources from `serverless.yml`:
133
+ ```yaml
134
+ # Comment out or remove:
135
+ # resources:
136
+ # Resources:
137
+ # FriggVPC: ...
138
+ # FriggPrivateSubnet1: ...
139
+ ```
140
+
141
+ Then use hardcoded subnet/SG IDs:
142
+ ```yaml
143
+ provider:
144
+ vpc:
145
+ securityGroupIds:
146
+ - sg-07c01370e830b6ad6
147
+ subnetIds:
148
+ - subnet-00ab9e0502e66aac3
149
+ - subnet-00d085a52937aaf91
150
+ ```
151
+
152
+ **OPTION B: Import VPC resources to stack (proper fix)**
153
+
154
+ 1. Create import template with mappings
155
+ 2. Run CloudFormation import operation
156
+ 3. Redeploy with local template
157
+
158
+ ### Step 2: Decision Point
159
+
160
+ **CRITICAL QUESTION: Do you want CloudFormation to manage the VPC?**
161
+
162
+ **If YES (recommended for Frigg framework):**
163
+ - ✅ Import `vpc-0eadd96976d29ede7` and its resources
164
+ - ✅ CloudFormation will manage VPC lifecycle
165
+ - ✅ Template and reality stay in sync
166
+ - ✅ Proper infrastructure as code
167
+
168
+ **If NO (simpler but less controlled):**
169
+ - ✅ Remove VPC from local template
170
+ - ✅ Use hardcoded subnet/SG IDs in serverless.yml
171
+ - ✅ Manually manage VPC outside CloudFormation
172
+ - ⚠️ Template drift will always exist
173
+
174
+ ## Recommended Action (CloudFormation Import)
175
+
176
+ ### Phase 1: Prepare Import Template
177
+
178
+ ```bash
179
+ # 1. Get the local template
180
+ cd /Users/sean/Documents/GitHub/quo--frigg/backend
181
+
182
+ # 2. Create import-resources.json
183
+ cat > import-resources.json <<EOF
184
+ [
185
+ {
186
+ "ResourceType": "AWS::EC2::VPC",
187
+ "LogicalResourceId": "FriggVPC",
188
+ "ResourceIdentifier": { "VpcId": "vpc-0eadd96976d29ede7" }
189
+ },
190
+ {
191
+ "ResourceType": "AWS::EC2::Subnet",
192
+ "LogicalResourceId": "FriggPrivateSubnet1",
193
+ "ResourceIdentifier": { "SubnetId": "subnet-00ab9e0502e66aac3" }
194
+ },
195
+ {
196
+ "ResourceType": "AWS::EC2::Subnet",
197
+ "LogicalResourceId": "FriggPrivateSubnet2",
198
+ "ResourceIdentifier": { "SubnetId": "subnet-00d085a52937aaf91" }
199
+ },
200
+ {
201
+ "ResourceType": "AWS::EC2::SecurityGroup",
202
+ "LogicalResourceId": "FriggLambdaSecurityGroup",
203
+ "ResourceIdentifier": { "GroupId": "sg-07c01370e830b6ad6" }
204
+ }
205
+ // ... add other resources (public subnets, SGs, etc.)
206
+ ]
207
+ EOF
208
+ ```
209
+
210
+ ### Phase 2: Create Change Set for Import
211
+
212
+ ```bash
213
+ aws cloudformation create-change-set \
214
+ --stack-name quo-integrations-dev \
215
+ --change-set-name import-vpc-resources \
216
+ --change-set-type IMPORT \
217
+ --resources-to-import file://import-resources.json \
218
+ --template-body file://.serverless/cloudformation-template-update-stack.json \
219
+ --capabilities CAPABILITY_IAM \
220
+ --region us-east-1
221
+ ```
222
+
223
+ ### Phase 3: Review and Execute
224
+
225
+ ```bash
226
+ # Review the change set
227
+ aws cloudformation describe-change-set \
228
+ --stack-name quo-integrations-dev \
229
+ --change-set-name import-vpc-resources \
230
+ --region us-east-1
231
+
232
+ # Execute if looks good
233
+ aws cloudformation execute-change-set \
234
+ --stack-name quo-integrations-dev \
235
+ --change-set-name import-vpc-resources \
236
+ --region us-east-1
237
+ ```
238
+
239
+ ### Phase 4: Update Lambda VPC Configs
240
+
241
+ After import, CloudFormation will:
242
+ - ✅ Recognize VPC resources are in the stack
243
+ - ✅ Lambda Refs will resolve to imported physical IDs
244
+ - ✅ Next deploy will update Lambda VPC configs from default VPC back to Frigg VPC
245
+
246
+ ### Phase 5: Clean Up
247
+
248
+ ```bash
249
+ # Delete unused VPCs
250
+ aws ec2 delete-vpc --vpc-id vpc-0e2351eac99adcb83 --region us-east-1
251
+ aws ec2 delete-vpc --vpc-id vpc-020a0365610c05f0b --region us-east-1
252
+ ```
253
+
254
+ ## Alternative: Simpler Approach (Remove VPC from Template)
255
+
256
+ If you don't want CloudFormation to manage VPC:
257
+
258
+ ### Step 1: Update serverless.yml
259
+
260
+ ```yaml
261
+ provider:
262
+ name: aws
263
+ vpc:
264
+ # Hardcode the VPC resources
265
+ securityGroupIds:
266
+ - sg-07c01370e830b6ad6
267
+ subnetIds:
268
+ - subnet-00ab9e0502e66aac3
269
+ - subnet-00d085a52937aaf91
270
+
271
+ # Remove VPC resource definitions
272
+ # resources:
273
+ # Resources:
274
+ # FriggVPC: ...
275
+ ```
276
+
277
+ ### Step 2: Deploy
278
+
279
+ ```bash
280
+ serverless deploy --stage dev
281
+ ```
282
+
283
+ ### Step 3: Clean Up
284
+
285
+ ```bash
286
+ # Delete all 3 orphaned VPCs
287
+ aws ec2 delete-vpc --vpc-id vpc-0eadd96976d29ede7
288
+ aws ec2 delete-vpc --vpc-id vpc-0e2351eac99adcb83
289
+ aws ec2 delete-vpc --vpc-id vpc-020a0365610c05f0b
290
+ ```
291
+
292
+ ## Recommendation
293
+
294
+ **For Frigg Framework consistency: Import VPC resources**
295
+
296
+ Why:
297
+ - ✅ Maintains infrastructure as code principles
298
+ - ✅ VPC lifecycle managed by CloudFormation
299
+ - ✅ Consistent with Frigg framework design
300
+ - ✅ Template matches deployed state
301
+ - ✅ `frigg doctor` will show 100/100 health
302
+
303
+ **Trade-offs:**
304
+ - ⚠️ More complex import process
305
+ - ⚠️ Must map all VPC resources correctly
306
+ - ⚠️ One-time effort but proper long-term solution
307
+
308
+ ## Next Steps for Relationship Analysis
309
+
310
+ The relationship analysis should:
311
+
312
+ 1. **Parse local build template** (not just deployed template)
313
+ 2. **Detect CloudFormation Refs** in Lambda VPC configs
314
+ 3. **Resolve Refs to physical IDs** from deployed stack
315
+ 4. **Match orphaned resources** against resolved physical IDs
316
+ 5. **Identify import mapping**:
317
+ - `FriggVPC` (logical) → `vpc-0eadd96976d29ede7` (physical)
318
+ - `FriggPrivateSubnet1` → `subnet-00ab9e0502e66aac3`
319
+ - etc.
320
+
321
+ This will enable `frigg repair --import` to:
322
+ - Show correct VPC to import
323
+ - Generate import-resources.json automatically
324
+ - Handle CloudFormation import operation
@@ -0,0 +1,386 @@
1
+ # Orphan Detection: Relationship Analysis & Multiple Resource Warning System
2
+
3
+ ## Problem Statement
4
+
5
+ When `frigg doctor` detects multiple orphaned resources of the same type (e.g., 3 VPCs, 10 subnets), users cannot easily determine:
6
+ 1. Which orphaned resources are actually relevant to import
7
+ 2. Which orphaned resources are old/unused and should be deleted
8
+ 3. Whether any orphaned resources are being actively referenced by drifted resources
9
+
10
+ ## Real-World Example: acme-integrations-dev Stack
11
+
12
+ ### AWS Reality (Verified 2025-10-27)
13
+
14
+ **CloudFormation Stack State:**
15
+ - Stack Name: `acme-integrations-dev`
16
+ - VPCs in stack: **0** (stack doesn't manage VPC)
17
+ - Lambda functions: 16 (all with VPC configuration drift)
18
+
19
+ **Lambda Functions Configuration:**
20
+ ```json
21
+ {
22
+ "VpcId": "vpc-01f21101d4ed6db59", // AWS Default VPC (172.31.0.0/16)
23
+ "SubnetIds": [
24
+ "subnet-020d32e3ca398a041", // In default VPC
25
+ "subnet-0c186318804aba790" // In default VPC
26
+ ],
27
+ "SecurityGroupIds": [
28
+ "sg-0aca40438d17344c4" // In default VPC
29
+ ]
30
+ }
31
+ ```
32
+
33
+ **Orphaned Resources Detected:**
34
+
35
+ **3 VPCs (ALL with Frigg CloudFormation tags):**
36
+ 1. `vpc-0eadd96976d29ede7` (10.0.0.0/16)
37
+ - Tags: `aws:cloudformation:stack-name=acme-integrations-dev`, `aws:cloudformation:logical-id=FriggVPC`
38
+ - Status: **NOT in CloudFormation stack, NOT used by Lambdas**
39
+ - Conclusion: **Old unused VPC, should be deleted**
40
+
41
+ 2. `vpc-0e2351eac99adcb83` (10.0.0.0/16)
42
+ - Tags: `aws:cloudformation:stack-name=acme-integrations-dev`, `aws:cloudformation:logical-id=FriggVPC`
43
+ - Status: **NOT in CloudFormation stack, NOT used by Lambdas**
44
+ - Conclusion: **Old unused VPC, should be deleted**
45
+
46
+ 3. `vpc-020a0365610c05f0b` (10.0.0.0/16)
47
+ - Tags: `aws:cloudformation:stack-name=acme-integrations-dev`, `aws:cloudformation:logical-id=FriggVPC`
48
+ - Status: **NOT in CloudFormation stack, NOT used by Lambdas**
49
+ - Conclusion: **Old unused VPC, should be deleted**
50
+
51
+ **10 Subnets:** All belong to the 3 orphaned VPCs (not the default VPC being used)
52
+
53
+ **3 Security Groups:** All belong to the 3 orphaned VPCs (not the default VPC being used)
54
+
55
+ ### Key Insights
56
+
57
+ 1. **CloudFormation tags are misleading**: All 3 VPCs have identical CFN tags but NONE are in the stack
58
+ 2. **Lambda drift is expected**: Lambdas use default VPC which has NO Frigg tags
59
+ 3. **All orphaned resources are unused**: None of the 16 orphaned resources are being referenced
60
+ 4. **Old deployment artifacts**: The orphaned VPCs are likely from previous Frigg deployments that failed cleanup
61
+
62
+ ## Solution Design
63
+
64
+ ### Phase 1: Relationship Analysis
65
+
66
+ Analyze connections between:
67
+ - **Drift Issues** → Resources being referenced in actual values
68
+ - **Orphaned Resources** → Resources detected but not in stack
69
+ - **Resource Hierarchies** → VPCs contain subnets/SGs, subnets belong to VPCs
70
+
71
+ ### Phase 2: Extract Referenced Resource IDs
72
+
73
+ From property mismatch drift issues, extract resource IDs that are **actually being used**:
74
+
75
+ ```javascript
76
+ // Example drift issue:
77
+ {
78
+ propertyPath: "VpcConfig.SubnetIds",
79
+ expectedValue: "subnet-old-1,subnet-old-2",
80
+ actualValue: "subnet-020d32e3ca398a041,subnet-0c186318804aba790"
81
+ }
82
+
83
+ // Extract: ["subnet-020d32e3ca398a041", "subnet-0c186318804aba790"]
84
+ // These are the subnets actually being used (even if not orphaned)
85
+ ```
86
+
87
+ **Property Paths to Analyze:**
88
+ - `VpcConfig.SubnetIds` → Subnet IDs
89
+ - `VpcConfig.SecurityGroupIds` → Security Group IDs
90
+ - `VpcId` → VPC IDs
91
+ - `SubnetId` → Subnet IDs (for other resources)
92
+
93
+ ### Phase 3: Build Resource Relationship Graph
94
+
95
+ ```javascript
96
+ {
97
+ orphanedResources: [
98
+ {
99
+ physicalId: "vpc-0eadd96976d29ede7",
100
+ resourceType: "AWS::EC2::VPC",
101
+ metadata: {
102
+ isReferencedByDrift: false, // NOT referenced in any drift
103
+ containsReferencedResources: false, // No subnets/SGs in this VPC are referenced
104
+ relatedOrphans: [ // Other orphans in this VPC
105
+ "subnet-0ad31b5ee6814b8fa",
106
+ "sg-03abddb7fb50aeaff"
107
+ ],
108
+ priority: "LOW" // Unused, should delete not import
109
+ }
110
+ },
111
+ {
112
+ physicalId: "subnet-020d32e3ca398a041",
113
+ resourceType: "AWS::EC2::Subnet",
114
+ vpcId: "vpc-01f21101d4ed6db59", // In default VPC (not orphaned)
115
+ metadata: {
116
+ isReferencedByDrift: true, // ✅ Referenced by 16 Lambda functions
117
+ referencedBy: [ // Which resources reference this
118
+ "acme-integrations-dev-attio",
119
+ "acme-integrations-dev-auth",
120
+ // ... 14 more
121
+ ],
122
+ priority: "N/A" // Not orphaned, just drift reference
123
+ }
124
+ }
125
+ ]
126
+ }
127
+ ```
128
+
129
+ ### Phase 4: Multi-Resource Warning System
130
+
131
+ When multiple resources of the same type are detected, show contextual warnings:
132
+
133
+ ```
134
+ ⚠ WARNING: Multiple VPCs detected (3 orphaned)
135
+
136
+ Analysis:
137
+ • vpc-0eadd96976d29ede7 - No active references, contains 3 orphaned subnets [UNUSED]
138
+ • vpc-0e2351eac99adcb83 - No active references, contains 4 orphaned subnets [UNUSED]
139
+ • vpc-020a0365610c05f0b - No active references, contains 3 orphaned subnets [UNUSED]
140
+
141
+ Recommendation:
142
+ ❌ Do NOT import these VPCs - they are old deployment artifacts
143
+ ✅ Lambda functions use default VPC (vpc-01f21101d4ed6db59) - drift is expected
144
+ 🧹 Consider cleaning up orphaned VPCs to reduce clutter:
145
+ $ aws ec2 delete-vpc --vpc-id vpc-0eadd96976d29ede7
146
+ ```
147
+
148
+ ## Implementation Plan
149
+
150
+ ### 1. Add Relationship Analysis to AWSResourceDetector
151
+
152
+ ```javascript
153
+ class AWSResourceDetector {
154
+ /**
155
+ * Find orphaned resources with relationship metadata
156
+ */
157
+ async findOrphanedResourcesWithRelationships({
158
+ stackIdentifier,
159
+ stackResources,
160
+ driftIssues = []
161
+ }) {
162
+ // 1. Find orphaned resources (existing logic)
163
+ const orphans = await this.findOrphanedResources({
164
+ stackIdentifier,
165
+ stackResources
166
+ });
167
+
168
+ // 2. Extract referenced resource IDs from drift
169
+ const referencedIds = this._extractReferencedResourceIds(driftIssues);
170
+
171
+ // 3. Enrich orphans with relationship metadata
172
+ return this._enrichWithRelationshipMetadata(orphans, referencedIds);
173
+ }
174
+
175
+ /**
176
+ * Extract resource IDs being referenced in drift actualValue
177
+ */
178
+ _extractReferencedResourceIds(driftIssues) {
179
+ const referenced = {
180
+ vpcIds: new Set(),
181
+ subnetIds: new Set(),
182
+ securityGroupIds: new Set()
183
+ };
184
+
185
+ for (const issue of driftIssues) {
186
+ if (issue.type !== 'PROPERTY_MISMATCH') continue;
187
+
188
+ const { propertyPath, actualValue } = issue.mismatch;
189
+
190
+ // Extract subnet IDs from VpcConfig.SubnetIds
191
+ if (propertyPath === 'VpcConfig.SubnetIds') {
192
+ const subnetIds = actualValue.split(',');
193
+ subnetIds.forEach(id => referenced.subnetIds.add(id));
194
+ }
195
+
196
+ // Extract SG IDs from VpcConfig.SecurityGroupIds
197
+ if (propertyPath === 'VpcConfig.SecurityGroupIds') {
198
+ const sgIds = actualValue.split(',');
199
+ sgIds.forEach(id => referenced.securityGroupIds.add(id));
200
+ }
201
+ }
202
+
203
+ return {
204
+ vpcIds: Array.from(referenced.vpcIds),
205
+ subnetIds: Array.from(referenced.subnetIds),
206
+ securityGroupIds: Array.from(referenced.securityGroupIds)
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Add relationship metadata to orphaned resources
212
+ */
213
+ _enrichWithRelationshipMetadata(orphans, referencedIds) {
214
+ return orphans.map(orphan => {
215
+ const metadata = {
216
+ isReferencedByDrift: false,
217
+ referencedBy: [],
218
+ relatedOrphans: [],
219
+ priority: 'LOW'
220
+ };
221
+
222
+ // Check if this orphan is actually referenced
223
+ if (orphan.resourceType === 'AWS::EC2::Subnet') {
224
+ metadata.isReferencedByDrift = referencedIds.subnetIds.includes(orphan.physicalId);
225
+ } else if (orphan.resourceType === 'AWS::EC2::SecurityGroup') {
226
+ metadata.isReferencedByDrift = referencedIds.securityGroupIds.includes(orphan.physicalId);
227
+ }
228
+
229
+ if (metadata.isReferencedByDrift) {
230
+ metadata.priority = 'HIGH';
231
+ }
232
+
233
+ return { ...orphan, metadata };
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Analyze orphan summary for warnings
239
+ */
240
+ analyzeOrphanSummary(orphans) {
241
+ const summary = {
242
+ warnings: [],
243
+ multipleResourceTypes: []
244
+ };
245
+
246
+ // Group by resource type
247
+ const grouped = {};
248
+ for (const orphan of orphans) {
249
+ grouped[orphan.resourceType] = grouped[orphan.resourceType] || [];
250
+ grouped[orphan.resourceType].push(orphan);
251
+ }
252
+
253
+ // Check for multiples
254
+ for (const [type, resources] of Object.entries(grouped)) {
255
+ if (resources.length > 1) {
256
+ const shortType = type.replace('AWS::EC2::', '');
257
+ summary.warnings.push(
258
+ `Multiple ${shortType}s detected (${resources.length}). Review relationships before importing.`
259
+ );
260
+ summary.multipleResourceTypes.push(type);
261
+ }
262
+ }
263
+
264
+ return summary;
265
+ }
266
+ }
267
+ ```
268
+
269
+ ### 2. Update Health Check to Use Relationship Analysis
270
+
271
+ ```javascript
272
+ // domains/health/application/use-cases/check-stack-health-use-case.js
273
+
274
+ class CheckStackHealthUseCase {
275
+ async execute({ stackName, region }) {
276
+ // ... existing drift detection ...
277
+
278
+ // Enhanced orphan detection with relationships
279
+ const orphanedResources = await this.resourceDetector.findOrphanedResourcesWithRelationships({
280
+ stackIdentifier,
281
+ stackResources,
282
+ driftIssues: issues // Pass drift issues for analysis
283
+ });
284
+
285
+ // Analyze for warnings
286
+ const orphanSummary = this.resourceDetector.analyzeOrphanSummary(orphanedResources);
287
+
288
+ return {
289
+ // ... existing fields ...
290
+ orphanedResources,
291
+ orphanAnalysis: orphanSummary
292
+ };
293
+ }
294
+ }
295
+ ```
296
+
297
+ ### 3. Update Frigg Doctor Output
298
+
299
+ ```javascript
300
+ // Show relationship warnings
301
+ if (orphanAnalysis.warnings.length > 0) {
302
+ console.log('\n⚠ ORPHAN ANALYSIS WARNINGS:\n');
303
+ for (const warning of orphanAnalysis.warnings) {
304
+ console.log(` ${warning}`);
305
+ }
306
+ }
307
+
308
+ // Show orphan details with metadata
309
+ console.log('\n CRITICAL ISSUES:');
310
+ for (const orphan of orphanedResources) {
311
+ console.log(` [ORPHANED_RESOURCE] ${orphan.physicalId}`);
312
+ console.log(` Resource: ${orphan.resourceType}`);
313
+
314
+ if (orphan.metadata) {
315
+ if (orphan.metadata.isReferencedByDrift) {
316
+ console.log(` ✅ ACTIVELY USED - Referenced by ${orphan.metadata.referencedBy.length} drifted resources`);
317
+ console.log(` Fix: Import to CloudFormation stack`);
318
+ } else {
319
+ console.log(` ❌ UNUSED - Not referenced by any stack resources`);
320
+ console.log(` Fix: Consider deleting instead of importing`);
321
+ }
322
+ }
323
+ }
324
+ ```
325
+
326
+ ### 4. Update Frigg Repair to Handle Multiples
327
+
328
+ ```javascript
329
+ // frigg repair --import <stack-name>
330
+
331
+ if (orphanAnalysis.multipleResourceTypes.includes('AWS::EC2::VPC')) {
332
+ console.log('\n⚠ WARNING: Multiple VPCs detected');
333
+ console.log('Please review and select which VPC to import:\n');
334
+
335
+ const vpcs = orphanedResources.filter(o => o.resourceType === 'AWS::EC2::VPC');
336
+ for (let i = 0; i < vpcs.length; i++) {
337
+ const vpc = vpcs[i];
338
+ console.log(` ${i + 1}. ${vpc.physicalId} (${vpc.cidrBlock})`);
339
+ console.log(` Referenced: ${vpc.metadata.isReferencedByDrift ? 'Yes' : 'No'}`);
340
+ console.log(` Related Resources: ${vpc.metadata.relatedOrphans.length} subnets/SGs\n`);
341
+ }
342
+
343
+ // Prompt user to select or skip
344
+ const selection = await promptUser('Select VPC number to import (or "skip" to skip all): ');
345
+
346
+ if (selection === 'skip') {
347
+ console.log('Skipping VPC import');
348
+ // Remove VPCs from import list
349
+ }
350
+ }
351
+ ```
352
+
353
+ ## Testing Strategy
354
+
355
+ ### Test 1: Detect Unused Orphans
356
+ ✅ 3 VPCs with CFN tags but NOT in stack, NOT referenced → mark as unused
357
+
358
+ ### Test 2: Detect Referenced Non-Orphans
359
+ ✅ Subnets in default VPC referenced by Lambdas but NOT orphaned → show in analysis
360
+
361
+ ### Test 3: Multi-Resource Warning
362
+ ✅ Multiple VPCs detected → show warning + require user selection
363
+
364
+ ### Test 4: VPC Hierarchy Analysis
365
+ ✅ VPC contains subnets → link orphaned subnets to orphaned VPC
366
+
367
+ ## Benefits
368
+
369
+ 1. **Prevents Bad Imports**: Users won't accidentally import unused old VPCs
370
+ 2. **Clarifies Drift**: Explains why drift exists (using default VPC vs Frigg VPC)
371
+ 3. **Cleanup Guidance**: Identifies resources that should be deleted, not imported
372
+ 4. **Informed Decisions**: Shows relationships between resources before action
373
+ 5. **Reduces Errors**: Requires explicit selection when multiple resources exist
374
+
375
+ ## Next Steps
376
+
377
+ 1. ✅ Write TDD tests for relationship analysis
378
+ 2. ⬜ Implement `findOrphanedResourcesWithRelationships` method
379
+ 3. ⬜ Implement `_extractReferencedResourceIds` helper
380
+ 4. ⬜ Implement `_enrichWithRelationshipMetadata` helper
381
+ 5. ⬜ Implement `analyzeOrphanSummary` method
382
+ 6. ⬜ Update `CheckStackHealthUseCase` to use new method
383
+ 7. ⬜ Update `frigg doctor` output to show warnings
384
+ 8. ⬜ Update `frigg repair` to handle multiple resources
385
+ 9. ⬜ Test with real acme-integrations-dev data
386
+ 10. ⬜ Document relationship analysis in HEALTH.md