@salesforce/afv-skills 1.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 (47) hide show
  1. package/LICENSE.txt +330 -0
  2. package/README.md +466 -0
  3. package/package.json +23 -0
  4. package/skills/apex-class/SKILL.md +253 -0
  5. package/skills/apex-class/examples/AccountDeduplicationBatch.cls +148 -0
  6. package/skills/apex-class/examples/AccountSelector.cls +193 -0
  7. package/skills/apex-class/examples/AccountService.cls +201 -0
  8. package/skills/apex-class/templates/abstract.cls +128 -0
  9. package/skills/apex-class/templates/batch.cls +125 -0
  10. package/skills/apex-class/templates/domain.cls +102 -0
  11. package/skills/apex-class/templates/dto.cls +108 -0
  12. package/skills/apex-class/templates/exception.cls +51 -0
  13. package/skills/apex-class/templates/interface.cls +25 -0
  14. package/skills/apex-class/templates/queueable.cls +92 -0
  15. package/skills/apex-class/templates/schedulable.cls +75 -0
  16. package/skills/apex-class/templates/selector.cls +92 -0
  17. package/skills/apex-class/templates/service.cls +69 -0
  18. package/skills/apex-class/templates/utility.cls +97 -0
  19. package/skills/apex-test-class/SKILL.md +101 -0
  20. package/skills/apex-test-class/references/assertion-patterns.md +209 -0
  21. package/skills/apex-test-class/references/async-testing.md +276 -0
  22. package/skills/apex-test-class/references/mocking-patterns.md +219 -0
  23. package/skills/apex-test-class/references/test-data-factory.md +176 -0
  24. package/skills/deployment-readiness-check/SKILL.md +257 -0
  25. package/skills/deployment-readiness-check/assets/deployment_checklist.md +286 -0
  26. package/skills/deployment-readiness-check/references/rollback_procedures.md +308 -0
  27. package/skills/deployment-readiness-check/scripts/check_metadata.sh +207 -0
  28. package/skills/salesforce-custom-application/SKILL.md +211 -0
  29. package/skills/salesforce-custom-field/SKILL.md +505 -0
  30. package/skills/salesforce-custom-lightning-type/SKILL.md +157 -0
  31. package/skills/salesforce-custom-object/SKILL.md +238 -0
  32. package/skills/salesforce-custom-tab/SKILL.md +78 -0
  33. package/skills/salesforce-experience-site/SKILL.md +178 -0
  34. package/skills/salesforce-flexipage/SKILL.md +445 -0
  35. package/skills/salesforce-flow/SKILL.md +368 -0
  36. package/skills/salesforce-fragment/SKILL.md +42 -0
  37. package/skills/salesforce-lightning-app-build/SKILL.md +254 -0
  38. package/skills/salesforce-list-view/SKILL.md +216 -0
  39. package/skills/salesforce-validation-rule/SKILL.md +72 -0
  40. package/skills/salesforce-web-app-creating-records/SKILL.md +84 -0
  41. package/skills/salesforce-web-app-feature/SKILL.md +70 -0
  42. package/skills/salesforce-web-app-list-and-create-records/SKILL.md +36 -0
  43. package/skills/salesforce-web-application/SKILL.md +34 -0
  44. package/skills/trigger-refactor-pipeline/SKILL.md +191 -0
  45. package/skills/trigger-refactor-pipeline/assets/test_template.apex +321 -0
  46. package/skills/trigger-refactor-pipeline/references/handler_patterns.md +442 -0
  47. package/skills/trigger-refactor-pipeline/scripts/analyze_trigger.py +258 -0
@@ -0,0 +1,442 @@
1
+ # Trigger Handler Patterns Reference
2
+
3
+ This guide covers common patterns for refactoring Salesforce triggers into handler classes with bulk-safe operations.
4
+
5
+ ## Pattern 1: Simple Handler Class
6
+
7
+ **Best for**: Triggers with 1-3 contexts and straightforward logic.
8
+
9
+ ### Structure
10
+
11
+ ```apex
12
+ public class OpportunityTriggerHandler {
13
+
14
+ public void beforeInsert(List<Opportunity> newRecords) {
15
+ validateClosedWonAmount(newRecords);
16
+ }
17
+
18
+ public void beforeUpdate(List<Opportunity> newRecords, Map<Id, Opportunity> oldMap) {
19
+ updateDescriptionOnStageChange(newRecords, oldMap);
20
+ }
21
+
22
+ public void afterUpdate(List<Opportunity> newRecords, Map<Id, Opportunity> oldMap) {
23
+ createTasksForClosedWon(newRecords, oldMap);
24
+ }
25
+
26
+ // Private helper methods below
27
+ private void validateClosedWonAmount(List<Opportunity> opportunities) {
28
+ for (Opportunity opp : opportunities) {
29
+ if (opp.StageName == 'Closed Won' &&
30
+ (opp.Amount == null || opp.Amount < 1000)) {
31
+ opp.addError('Closed Won opportunities must have Amount ≥ 1000.');
32
+ }
33
+ }
34
+ }
35
+
36
+ private void updateDescriptionOnStageChange(
37
+ List<Opportunity> newRecords,
38
+ Map<Id, Opportunity> oldMap
39
+ ) {
40
+ for (Opportunity opp : newRecords) {
41
+ Opportunity oldOpp = oldMap.get(opp.Id);
42
+ if (opp.StageName != oldOpp.StageName) {
43
+ opp.Description = 'Stage changed from ' +
44
+ oldOpp.StageName + ' to ' + opp.StageName;
45
+ }
46
+ }
47
+ }
48
+
49
+ private void createTasksForClosedWon(
50
+ List<Opportunity> newRecords,
51
+ Map<Id, Opportunity> oldMap
52
+ ) {
53
+ List<Task> tasksToInsert = new List<Task>();
54
+
55
+ for (Opportunity opp : newRecords) {
56
+ Opportunity oldOpp = oldMap.get(opp.Id);
57
+
58
+ // Check if stage changed to Closed Won
59
+ if (opp.StageName == 'Closed Won' &&
60
+ oldOpp.StageName != 'Closed Won') {
61
+
62
+ tasksToInsert.add(new Task(
63
+ WhatId = opp.Id,
64
+ OwnerId = opp.OwnerId,
65
+ Subject = 'Send thank-you',
66
+ Status = 'Not Started',
67
+ Priority = 'Normal',
68
+ ActivityDate = Date.today()
69
+ ));
70
+ }
71
+ }
72
+
73
+ // Bulk DML outside loop
74
+ if (!tasksToInsert.isEmpty()) {
75
+ insert tasksToInsert;
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### Trigger Delegation
82
+
83
+ ```apex
84
+ trigger OpportunityTrigger on Opportunity (
85
+ before insert, before update, after update
86
+ ) {
87
+ OpportunityTriggerHandler handler = new OpportunityTriggerHandler();
88
+
89
+ if (Trigger.isBefore) {
90
+ if (Trigger.isInsert) {
91
+ handler.beforeInsert(Trigger.new);
92
+ } else if (Trigger.isUpdate) {
93
+ handler.beforeUpdate(Trigger.new, Trigger.oldMap);
94
+ }
95
+ }
96
+
97
+ if (Trigger.isAfter && Trigger.isUpdate) {
98
+ handler.afterUpdate(Trigger.new, Trigger.oldMap);
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## Pattern 2: Handler with Database Methods
104
+
105
+ **Best for**: When you need granular error handling and partial success.
106
+
107
+ ### Key Features
108
+
109
+ - Uses `Database.insert()` instead of `insert` for partial saves
110
+ - Returns `Database.SaveResult` for error handling
111
+ - Logs errors without stopping execution
112
+
113
+ ### Example
114
+
115
+ ```apex
116
+ private void createTasksForClosedWon(
117
+ List<Opportunity> newRecords,
118
+ Map<Id, Opportunity> oldMap
119
+ ) {
120
+ List<Task> tasksToInsert = new List<Task>();
121
+
122
+ for (Opportunity opp : newRecords) {
123
+ Opportunity oldOpp = oldMap.get(opp.Id);
124
+
125
+ if (opp.StageName == 'Closed Won' &&
126
+ oldOpp.StageName != 'Closed Won') {
127
+
128
+ tasksToInsert.add(new Task(
129
+ WhatId = opp.Id,
130
+ OwnerId = opp.OwnerId,
131
+ Subject = 'Send thank-you',
132
+ Status = 'Not Started',
133
+ Priority = 'Normal',
134
+ ActivityDate = Date.today()
135
+ ));
136
+ }
137
+ }
138
+
139
+ if (!tasksToInsert.isEmpty()) {
140
+ Database.SaveResult[] results = Database.insert(tasksToInsert, false);
141
+
142
+ // Log errors without stopping execution
143
+ for (Integer i = 0; i < results.size(); i++) {
144
+ if (!results[i].isSuccess()) {
145
+ System.debug('Failed to create task: ' + results[i].getErrors());
146
+ }
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ ## Pattern 3: Handler with Maps for Lookups
153
+
154
+ **Best for**: When you need to query related records for processing.
155
+
156
+ ### Key Features
157
+
158
+ - Pre-queries related records using Sets
159
+ - Uses Maps for O(1) lookups instead of nested loops
160
+ - Avoids SOQL in loops
161
+
162
+ ### Example
163
+
164
+ ```apex
165
+ private void enrichOpportunitiesWithAccountData(List<Opportunity> opportunities) {
166
+ // Collect Account IDs
167
+ Set<Id> accountIds = new Set<Id>();
168
+ for (Opportunity opp : opportunities) {
169
+ if (opp.AccountId != null) {
170
+ accountIds.add(opp.AccountId);
171
+ }
172
+ }
173
+
174
+ // Single SOQL query outside loop
175
+ Map<Id, Account> accountMap = new Map<Id, Account>([
176
+ SELECT Id, Name, Industry, AnnualRevenue
177
+ FROM Account
178
+ WHERE Id IN :accountIds
179
+ ]);
180
+
181
+ // Use Map for O(1) lookup
182
+ for (Opportunity opp : opportunities) {
183
+ if (opp.AccountId != null && accountMap.containsKey(opp.AccountId)) {
184
+ Account acc = accountMap.get(opp.AccountId);
185
+ // Process with account data
186
+ opp.Description = 'Account Industry: ' + acc.Industry;
187
+ }
188
+ }
189
+ }
190
+ ```
191
+
192
+ ## Pattern 4: Unified Handler Framework
193
+
194
+ **Best for**: Complex triggers with many contexts and cross-cutting concerns.
195
+
196
+ ### Structure
197
+
198
+ ```apex
199
+ public abstract class TriggerHandler {
200
+
201
+ protected Boolean isBefore;
202
+ protected Boolean isAfter;
203
+ protected Boolean isInsert;
204
+ protected Boolean isUpdate;
205
+ protected Boolean isDelete;
206
+ protected Boolean isUndelete;
207
+
208
+ public void run() {
209
+ isBefore = Trigger.isBefore;
210
+ isAfter = Trigger.isAfter;
211
+ isInsert = Trigger.isInsert;
212
+ isUpdate = Trigger.isUpdate;
213
+ isDelete = Trigger.isDelete;
214
+ isUndelete = Trigger.isUndelete;
215
+
216
+ if (isBefore) {
217
+ if (isInsert) beforeInsert();
218
+ if (isUpdate) beforeUpdate();
219
+ if (isDelete) beforeDelete();
220
+ }
221
+
222
+ if (isAfter) {
223
+ if (isInsert) afterInsert();
224
+ if (isUpdate) afterUpdate();
225
+ if (isDelete) afterDelete();
226
+ if (isUndelete) afterUndelete();
227
+ }
228
+ }
229
+
230
+ protected virtual void beforeInsert() {}
231
+ protected virtual void beforeUpdate() {}
232
+ protected virtual void beforeDelete() {}
233
+ protected virtual void afterInsert() {}
234
+ protected virtual void afterUpdate() {}
235
+ protected virtual void afterDelete() {}
236
+ protected virtual void afterUndelete() {}
237
+ }
238
+ ```
239
+
240
+ ### Concrete Handler
241
+
242
+ ```apex
243
+ public class OpportunityTriggerHandler extends TriggerHandler {
244
+
245
+ private List<Opportunity> newRecords;
246
+ private List<Opportunity> oldRecords;
247
+ private Map<Id, Opportunity> newMap;
248
+ private Map<Id, Opportunity> oldMap;
249
+
250
+ public OpportunityTriggerHandler() {
251
+ this.newRecords = (List<Opportunity>) Trigger.new;
252
+ this.oldRecords = (List<Opportunity>) Trigger.old;
253
+ this.newMap = (Map<Id, Opportunity>) Trigger.newMap;
254
+ this.oldMap = (Map<Id, Opportunity>) Trigger.oldMap;
255
+ }
256
+
257
+ protected override void beforeInsert() {
258
+ validateClosedWonAmount();
259
+ }
260
+
261
+ protected override void beforeUpdate() {
262
+ updateDescriptionOnStageChange();
263
+ }
264
+
265
+ protected override void afterUpdate() {
266
+ createTasksForClosedWon();
267
+ }
268
+
269
+ // Private helper methods omitted for brevity
270
+ }
271
+ ```
272
+
273
+ ### Trigger Delegation
274
+
275
+ ```apex
276
+ trigger OpportunityTrigger on Opportunity (
277
+ before insert, before update, after update
278
+ ) {
279
+ new OpportunityTriggerHandler().run();
280
+ }
281
+ ```
282
+
283
+ ## Best Practices
284
+
285
+ ### 1. Bulkification
286
+
287
+ Always process records in collections:
288
+
289
+ ```apex
290
+ // ✓ Good: Collect DML outside loop
291
+ List<Task> tasksToInsert = new List<Task>();
292
+ for (Opportunity opp : opportunities) {
293
+ tasksToInsert.add(new Task(...));
294
+ }
295
+ if (!tasksToInsert.isEmpty()) {
296
+ insert tasksToInsert;
297
+ }
298
+
299
+ // ✗ Bad: DML inside loop
300
+ for (Opportunity opp : opportunities) {
301
+ insert new Task(...); // SOQL/DML in loop!
302
+ }
303
+ ```
304
+
305
+ ### 2. Defensive Null Checks
306
+
307
+ ```apex
308
+ // ✓ Good: Check for null before accessing
309
+ if (opp.AccountId != null && accountMap.containsKey(opp.AccountId)) {
310
+ Account acc = accountMap.get(opp.AccountId);
311
+ // Safe to use acc
312
+ }
313
+
314
+ // ✗ Bad: Assumes data exists
315
+ Account acc = accountMap.get(opp.AccountId);
316
+ String industry = acc.Industry; // NullPointerException risk
317
+ ```
318
+
319
+ ### 3. Clear Method Names
320
+
321
+ ```apex
322
+ // ✓ Good: Descriptive, verb-noun pattern
323
+ private void validateClosedWonAmount(List<Opportunity> opportunities)
324
+ private void createTasksForClosedWon(List<Opportunity> opportunities)
325
+
326
+ // ✗ Bad: Vague or unclear
327
+ private void validate(List<Opportunity> opportunities)
328
+ private void doStuff(List<Opportunity> opportunities)
329
+ ```
330
+
331
+ ### 4. Single Responsibility
332
+
333
+ Each handler method should do one thing:
334
+
335
+ ```apex
336
+ // ✓ Good: Separate concerns
337
+ private void validateClosedWonAmount(List<Opportunity> opportunities)
338
+ private void validateRequiredFields(List<Opportunity> opportunities)
339
+ private void calculateDiscounts(List<Opportunity> opportunities)
340
+
341
+ // ✗ Bad: One method does everything
342
+ private void processOpportunities(List<Opportunity> opportunities)
343
+ ```
344
+
345
+ ### 5. Test Boundaries
346
+
347
+ Structure code to make testing easier:
348
+
349
+ ```apex
350
+ // ✓ Good: Public method for testing, private for implementation
351
+ @TestVisible
352
+ private void createTasksForClosedWon(
353
+ List<Opportunity> newRecords,
354
+ Map<Id, Opportunity> oldMap
355
+ ) {
356
+ // Implementation
357
+ }
358
+ ```
359
+
360
+ ## Deployment Order
361
+
362
+ When deploying refactored triggers:
363
+
364
+ 1. Deploy handler class(es) first
365
+ 2. Update trigger to use handler
366
+ 3. Deploy test class
367
+ 4. Run all tests before production deployment
368
+ 5. Monitor debug logs for 24-48 hours after production deployment
369
+
370
+ ## Rollback Strategy
371
+
372
+ Keep the old trigger code commented out or in version control:
373
+
374
+ ```apex
375
+ trigger OpportunityTrigger on Opportunity (...) {
376
+ // New handler approach
377
+ new OpportunityTriggerHandler().run();
378
+
379
+ /* OLD CODE - REMOVE AFTER 1 WEEK IF NO ISSUES
380
+ if (Trigger.isBefore && Trigger.isInsert) {
381
+ for (Opportunity o : Trigger.new) {
382
+ // old logic
383
+ }
384
+ }
385
+ */
386
+ }
387
+ ```
388
+
389
+ ## Common Pitfalls
390
+
391
+ ### Pitfall 1: Recursive Triggers
392
+
393
+ **Problem**: Handler calls DML which triggers the same trigger again.
394
+
395
+ **Solution**: Use static flag to prevent recursion:
396
+
397
+ ```apex
398
+ public class OpportunityTriggerHandler {
399
+ private static Boolean isExecuting = false;
400
+
401
+ public void beforeUpdate(List<Opportunity> newRecords, Map<Id, Opportunity> oldMap) {
402
+ if (isExecuting) return;
403
+
404
+ isExecuting = true;
405
+ try {
406
+ // Your logic here
407
+ } finally {
408
+ isExecuting = false;
409
+ }
410
+ }
411
+ }
412
+ ```
413
+
414
+ ### Pitfall 2: Mixed Context Logic
415
+
416
+ **Problem**: Before-context logic mixed with after-context logic.
417
+
418
+ **Solution**: Keep context methods separate and focused:
419
+
420
+ ```apex
421
+ // ✓ Good: Separate methods per context
422
+ public void beforeUpdate(List<Opportunity> newRecords, Map<Id, Opportunity> oldMap)
423
+ public void afterUpdate(List<Opportunity> newRecords, Map<Id, Opportunity> oldMap)
424
+
425
+ // ✗ Bad: Mixed logic in one method
426
+ public void handleUpdate(List<Opportunity> newRecords, Map<Id, Opportunity> oldMap)
427
+ ```
428
+
429
+ ### Pitfall 3: Over-Engineering
430
+
431
+ **Problem**: Using complex framework for simple triggers.
432
+
433
+ **Solution**: Choose the right pattern for your complexity:
434
+ - 1-3 contexts with simple logic → Simple Handler (Pattern 1)
435
+ - 3-5 contexts with moderate complexity → Handler with Database Methods (Pattern 2)
436
+ - 5+ contexts with cross-cutting concerns → Unified Framework (Pattern 4)
437
+
438
+ ## Additional Resources
439
+
440
+ - [Apex Developer Guide: Trigger and Bulk Request Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_bulk_requests.htm)
441
+ - [Apex Enterprise Patterns](https://github.com/apex-enterprise-patterns)
442
+ - [Trigger Framework Comparison](https://github.com/kevinohara80/sfdc-trigger-framework)
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Analyze Salesforce Apex triggers for common anti-patterns.
4
+
5
+ Usage:
6
+ python analyze_trigger.py <TriggerName>
7
+
8
+ Requirements:
9
+ - Salesforce CLI authenticated to target org
10
+ - Python 3.9+
11
+ """
12
+
13
+ import sys
14
+ import subprocess
15
+ import json
16
+ import re
17
+ from typing import List, Dict, Tuple
18
+
19
+ class TriggerAnalyzer:
20
+ def __init__(self, trigger_name: str):
21
+ self.trigger_name = trigger_name
22
+ self.trigger_body = ""
23
+ self.issues = {
24
+ 'dml_in_loops': [],
25
+ 'soql_in_loops': [],
26
+ 'missing_bulk': [],
27
+ }
28
+ self.complexity_score = 0
29
+
30
+ def retrieve_trigger(self) -> bool:
31
+ """Retrieve trigger source code from Salesforce org."""
32
+ try:
33
+ cmd = [
34
+ 'sf', 'apex', 'get', 'trigger',
35
+ '--trigger-name', self.trigger_name,
36
+ '--json'
37
+ ]
38
+ result = subprocess.run(cmd, capture_output=True, text=True)
39
+
40
+ if result.returncode != 0:
41
+ print(f"Error retrieving trigger: {result.stderr}")
42
+ return False
43
+
44
+ data = json.loads(result.stdout)
45
+ self.trigger_body = data.get('result', {}).get('body', '')
46
+ return True
47
+
48
+ except Exception as e:
49
+ print(f"Failed to retrieve trigger: {e}")
50
+ return False
51
+
52
+ def analyze_dml_in_loops(self) -> None:
53
+ """Detect DML operations inside for loops."""
54
+ lines = self.trigger_body.split('\n')
55
+ in_loop = False
56
+ loop_start = 0
57
+
58
+ dml_patterns = [
59
+ r'\binsert\s+', r'\bupdate\s+', r'\bdelete\s+',
60
+ r'\bundelete\s+', r'\bupsert\s+'
61
+ ]
62
+
63
+ for i, line in enumerate(lines, 1):
64
+ # Track loop entry
65
+ if re.search(r'\bfor\s*\(', line):
66
+ in_loop = True
67
+ loop_start = i
68
+
69
+ # Track loop exit
70
+ if in_loop and line.strip() == '}':
71
+ # Check if this closes the loop (simplified heuristic)
72
+ in_loop = False
73
+
74
+ # Check for DML in loop
75
+ if in_loop:
76
+ for pattern in dml_patterns:
77
+ if re.search(pattern, line):
78
+ self.issues['dml_in_loops'].append({
79
+ 'line': i,
80
+ 'code': line.strip(),
81
+ 'loop_start': loop_start
82
+ })
83
+
84
+ def analyze_soql_in_loops(self) -> None:
85
+ """Detect SOQL queries inside for loops."""
86
+ lines = self.trigger_body.split('\n')
87
+ in_loop = False
88
+ loop_start = 0
89
+
90
+ soql_pattern = r'\[SELECT\s+'
91
+
92
+ for i, line in enumerate(lines, 1):
93
+ if re.search(r'\bfor\s*\(', line):
94
+ in_loop = True
95
+ loop_start = i
96
+
97
+ if in_loop and line.strip() == '}':
98
+ in_loop = False
99
+
100
+ if in_loop and re.search(soql_pattern, line, re.IGNORECASE):
101
+ self.issues['soql_in_loops'].append({
102
+ 'line': i,
103
+ 'code': line.strip(),
104
+ 'loop_start': loop_start
105
+ })
106
+
107
+ def analyze_bulkification(self) -> None:
108
+ """Check for proper bulkification patterns."""
109
+ # Simple heuristic: if we have DML in loops, bulkification is missing
110
+ if self.issues['dml_in_loops']:
111
+ self.issues['missing_bulk'].append({
112
+ 'message': 'DML operations should be collected and executed outside loops',
113
+ 'affected_lines': [issue['line'] for issue in self.issues['dml_in_loops']]
114
+ })
115
+
116
+ if self.issues['soql_in_loops']:
117
+ self.issues['missing_bulk'].append({
118
+ 'message': 'SOQL queries should be moved outside loops or use Maps for lookups',
119
+ 'affected_lines': [issue['line'] for issue in self.issues['soql_in_loops']]
120
+ })
121
+
122
+ def calculate_complexity(self) -> None:
123
+ """Calculate overall complexity score (1-10)."""
124
+ score = 1
125
+
126
+ # Add points for each issue type
127
+ score += len(self.issues['dml_in_loops']) * 2
128
+ score += len(self.issues['soql_in_loops']) * 2
129
+ score += len(self.issues['missing_bulk']) * 1
130
+
131
+ # Count trigger contexts
132
+ contexts = len(re.findall(r'Trigger\.(isBefore|isAfter)', self.trigger_body))
133
+ score += contexts
134
+
135
+ # Count lines of code (normalized)
136
+ loc = len([l for l in self.trigger_body.split('\n') if l.strip()])
137
+ score += min(loc // 10, 3)
138
+
139
+ self.complexity_score = min(score, 10)
140
+
141
+ def recommend_approach(self) -> str:
142
+ """Recommend refactoring approach based on analysis."""
143
+ if self.complexity_score <= 3:
144
+ return "Simple handler class with separate methods for each trigger context"
145
+ elif self.complexity_score <= 6:
146
+ return "Handler class with bulkified collections and helper methods"
147
+ else:
148
+ return "Unified handler framework with separate concern classes (validation, DML, etc.)"
149
+
150
+ def generate_report(self) -> None:
151
+ """Print analysis report."""
152
+ print("\n" + "="*70)
153
+ print(f"TRIGGER ANALYSIS REPORT: {self.trigger_name}")
154
+ print("="*70 + "\n")
155
+
156
+ print(f"Complexity Score: {self.complexity_score}/10")
157
+ print(f"Recommended Approach: {self.recommend_approach()}\n")
158
+
159
+ # DML in loops
160
+ if self.issues['dml_in_loops']:
161
+ print("⚠️ DML OPERATIONS IN LOOPS:")
162
+ for issue in self.issues['dml_in_loops']:
163
+ print(f" Line {issue['line']}: {issue['code']}")
164
+ print(f" └─ Loop started at line {issue['loop_start']}")
165
+ print()
166
+ else:
167
+ print("✓ No DML operations found in loops\n")
168
+
169
+ # SOQL in loops
170
+ if self.issues['soql_in_loops']:
171
+ print("⚠️ SOQL QUERIES IN LOOPS:")
172
+ for issue in self.issues['soql_in_loops']:
173
+ print(f" Line {issue['line']}: {issue['code']}")
174
+ print(f" └─ Loop started at line {issue['loop_start']}")
175
+ print()
176
+ else:
177
+ print("✓ No SOQL queries found in loops\n")
178
+
179
+ # Bulkification
180
+ if self.issues['missing_bulk']:
181
+ print("⚠️ BULKIFICATION RECOMMENDATIONS:")
182
+ for issue in self.issues['missing_bulk']:
183
+ print(f" • {issue['message']}")
184
+ print(f" Affected lines: {', '.join(map(str, issue['affected_lines']))}")
185
+ print()
186
+ else:
187
+ print("✓ Bulkification patterns look good\n")
188
+
189
+ print("="*70)
190
+ print("NEXT STEPS:")
191
+ print("1. Review the handler patterns reference guide")
192
+ print("2. Create handler class with bulk-safe collections")
193
+ print("3. Extract trigger logic into handler methods")
194
+ print("4. Generate comprehensive tests using the template")
195
+ print("="*70 + "\n")
196
+
197
+
198
+ def main():
199
+ if len(sys.argv) < 2:
200
+ print("Usage: python analyze_trigger.py <TriggerName>")
201
+ sys.exit(1)
202
+
203
+ trigger_name = sys.argv[1]
204
+
205
+ print(f"Analyzing trigger: {trigger_name}...")
206
+
207
+ analyzer = TriggerAnalyzer(trigger_name)
208
+
209
+ # For demo purposes, use inline example if retrieval fails
210
+ if not analyzer.retrieve_trigger():
211
+ print("⚠️ Could not retrieve from org. Using example trigger for demonstration.\n")
212
+ # Use the example from the SKILL.md
213
+ analyzer.trigger_body = """trigger OpportunityTrigger on Opportunity (before insert, before update, after update) {
214
+ if (Trigger.isBefore && Trigger.isInsert) {
215
+ for (Opportunity o : Trigger.new) {
216
+ if (o.StageName == 'Closed Won' && (o.Amount == null || o.Amount < 1000)) {
217
+ o.addError('Closed Won opportunities must have Amount ≥ 1000.');
218
+ }
219
+ }
220
+ }
221
+ if (Trigger.isBefore && Trigger.isUpdate) {
222
+ for (Opportunity o : Trigger.new) {
223
+ Opportunity oldO = Trigger.oldMap.get(o.Id);
224
+ if (o.StageName != oldO.StageName) {
225
+ o.Description = 'Stage changed from ' + oldO.StageName + ' to ' + o.StageName;
226
+ }
227
+ }
228
+ }
229
+ if (Trigger.isAfter && Trigger.isUpdate) {
230
+ for (Opportunity o : Trigger.new) {
231
+ Opportunity oldO = Trigger.oldMap.get(o.Id);
232
+ if (o.StageName == 'Closed Won' && oldO.StageName != 'Closed Won') {
233
+ Task t = new Task(
234
+ WhatId = o.Id,
235
+ OwnerId = o.OwnerId,
236
+ Subject = 'Send thank-you',
237
+ Status = 'Not Started',
238
+ Priority = 'Normal',
239
+ ActivityDate = Date.today()
240
+ );
241
+ insert t;
242
+ }
243
+ }
244
+ }
245
+ }"""
246
+
247
+ # Run analysis
248
+ analyzer.analyze_dml_in_loops()
249
+ analyzer.analyze_soql_in_loops()
250
+ analyzer.analyze_bulkification()
251
+ analyzer.calculate_complexity()
252
+
253
+ # Generate report
254
+ analyzer.generate_report()
255
+
256
+
257
+ if __name__ == '__main__':
258
+ main()