sf-forcekit 1.0.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 (32) hide show
  1. package/License +21 -0
  2. package/README.md +106 -0
  3. package/bin/cli.js +85 -0
  4. package/package.json +44 -0
  5. package/templates/ai-dir/README.md +155 -0
  6. package/templates/ai-dir/agentforce.md +213 -0
  7. package/templates/ai-dir/architecture.md +123 -0
  8. package/templates/ai-dir/commands.md +276 -0
  9. package/templates/ai-dir/context-snapshots/TEMPLATE.md +64 -0
  10. package/templates/ai-dir/conventions.md +242 -0
  11. package/templates/ai-dir/current-state.md +113 -0
  12. package/templates/ai-dir/debugging-notes.md +165 -0
  13. package/templates/ai-dir/deployment.md +161 -0
  14. package/templates/ai-dir/integrations.md +199 -0
  15. package/templates/ai-dir/inventory.md +209 -0
  16. package/templates/ai-dir/known-issues.md +124 -0
  17. package/templates/ai-dir/org-context.md +110 -0
  18. package/templates/ai-dir/performance.md +312 -0
  19. package/templates/ai-dir/prompts/agentforce.md +163 -0
  20. package/templates/ai-dir/prompts/apex.md +165 -0
  21. package/templates/ai-dir/prompts/flows.md +125 -0
  22. package/templates/ai-dir/prompts/lwc.md +230 -0
  23. package/templates/ai-dir/prompts/security.md +181 -0
  24. package/templates/ai-dir/prompts/testing.md +269 -0
  25. package/templates/ai-dir/rules.md +238 -0
  26. package/templates/ai-dir/scripts/update_state.py +1406 -0
  27. package/templates/ai-dir/source-of-truth.md +180 -0
  28. package/templates/ai-dir/templates/Selector.cls +113 -0
  29. package/templates/ai-dir/templates/Service.cls +132 -0
  30. package/templates/ai-dir/templates/TestClass.cls +143 -0
  31. package/templates/ai-dir/templates/TriggerHandler.cls +67 -0
  32. package/templates/ai-dir/testing-strategy.md +342 -0
@@ -0,0 +1,312 @@
1
+ # Performance Guide
2
+
3
+ > Governor limits, SOQL optimization, heap management, and async patterns.
4
+ > Consult this before writing any code that touches large datasets or complex logic.
5
+
6
+ ---
7
+
8
+ ## Governor Limits — Quick Reference
9
+
10
+ | Limit | Sync | Async | Monitor With |
11
+ |-------|------|-------|--------------|
12
+ | SOQL queries | 100 | 200 | `Limits.getQueries()` |
13
+ | SOQL rows returned | 50,000 | 50,000 | `Limits.getQueryRows()` |
14
+ | DML statements | 150 | 150 | `Limits.getDmlStatements()` |
15
+ | DML rows | 10,000 | 10,000 | `Limits.getDmlRows()` |
16
+ | CPU time | 10,000 ms | 60,000 ms | `Limits.getCpuTime()` |
17
+ | Heap size | 6 MB | 12 MB | `Limits.getHeapSize()` |
18
+ | Callouts | 100 | 100 | `Limits.getCallouts()` |
19
+ | Future calls | 50 | 0 | `Limits.getFutureCalls()` |
20
+ | Queueable jobs | 50 | 1 | `Limits.getQueueableJobs()` |
21
+
22
+ ---
23
+
24
+ ## SOQL Optimization
25
+
26
+ ### Indexing Rules
27
+
28
+ Salesforce auto-indexes:
29
+ - `Id`, `Name`, `OwnerId`, `CreatedDate`, `LastModifiedDate`, `SystemModstamp`
30
+ - All lookup/master-detail fields
31
+ - `RecordTypeId`
32
+ - External ID fields (`__c` with External ID checked)
33
+
34
+ **Custom indexes** — Request via Salesforce Support for high-volume objects.
35
+
36
+ ### Query Performance Checklist
37
+
38
+ ```apex
39
+ // ✅ GOOD — Selective, uses indexed field
40
+ [SELECT Id, Name FROM Account
41
+ WHERE Id IN :accountIds
42
+ WITH USER_MODE]
43
+
44
+ // ✅ GOOD — Narrow date range on indexed field
45
+ [SELECT Id FROM Case
46
+ WHERE CreatedDate = LAST_N_DAYS:30
47
+ WITH USER_MODE]
48
+
49
+ // ❌ BAD — Leading wildcard forces table scan
50
+ [SELECT Id FROM Account
51
+ WHERE Name LIKE '%test%'
52
+ WITH USER_MODE]
53
+
54
+ // ❌ BAD — Negative filter, not selective
55
+ [SELECT Id FROM Account
56
+ WHERE Industry != 'Technology'
57
+ WITH USER_MODE]
58
+
59
+ // ❌ BAD — OR across non-indexed fields
60
+ [SELECT Id FROM Account
61
+ WHERE Custom_Field_1__c = 'X' OR Custom_Field_2__c = 'Y'
62
+ WITH USER_MODE]
63
+ ```
64
+
65
+ ### Query Plan Analysis
66
+
67
+ ```
68
+ /services/data/v66.0/query?explain=SELECT+Id+FROM+Account+WHERE+Name='Test'
69
+ ```
70
+
71
+ | Result | Meaning | Action |
72
+ |--------|---------|--------|
73
+ | `Index` | Uses index | ✅ Good |
74
+ | `TableScan` | Full table scan | ❌ Add index or restructure |
75
+ | `Selectivity: 0.xx` | % of table scanned | < 10% is good |
76
+
77
+ ### Reducing SOQL Count
78
+
79
+ ```apex
80
+ // ❌ BAD — Separate queries per object (wastes SOQL budget)
81
+ List<Account> accounts = [SELECT Id FROM Account WHERE Id IN :ids WITH USER_MODE];
82
+ List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId IN :ids WITH USER_MODE];
83
+ List<Case> cases = [SELECT Id FROM Case WHERE AccountId IN :ids WITH USER_MODE];
84
+
85
+ // ✅ GOOD — Single parent query with subqueries
86
+ List<Account> accounts = [
87
+ SELECT Id, Name,
88
+ (SELECT Id, Name FROM Contacts),
89
+ (SELECT Id, Subject FROM Cases)
90
+ FROM Account
91
+ WHERE Id IN :ids
92
+ WITH USER_MODE
93
+ ];
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Heap Management
99
+
100
+ ### When Heap Matters
101
+
102
+ | Scenario | Risk Level |
103
+ |----------|-----------|
104
+ | Processing 50K+ records | 🔴 High |
105
+ | JSON deserializing large payloads | 🔴 High |
106
+ | Building large Maps/Sets | 🟡 Medium |
107
+ | String concatenation in loops | 🟡 Medium |
108
+ | Normal CRUD operations | 🟢 Low |
109
+
110
+ ### Heap Reduction Patterns
111
+
112
+ ```apex
113
+ // ❌ BAD — Loads entire result set into memory
114
+ List<Account> allAccounts = [SELECT Id, Name, Description FROM Account WITH USER_MODE];
115
+
116
+ // ✅ GOOD — Use SOQL for filtering, only load what you need
117
+ List<Account> activeAccounts = [
118
+ SELECT Id, Name
119
+ FROM Account
120
+ WHERE IsActive__c = true
121
+ WITH USER_MODE
122
+ LIMIT 2000
123
+ ];
124
+
125
+ // ✅ BETTER — For 50K+ records, use Batch Apex with QueryLocator
126
+ global class LargeDataBatch implements Database.Batchable<SObject> {
127
+ global Database.QueryLocator start(Database.BatchableContext bc) {
128
+ return Database.getQueryLocator([
129
+ SELECT Id, Name FROM Account WITH USER_MODE
130
+ ]);
131
+ }
132
+ // QueryLocator bypasses 50K row limit — processes up to 50M rows
133
+ }
134
+ ```
135
+
136
+ ### Apex Cursors (v66.0+)
137
+
138
+ ```apex
139
+ // ✅ NEW — Process large result sets in chunks without heap pressure
140
+ Database.Cursor cursor = Database.getCursor(
141
+ 'SELECT Id, Name FROM Account WITH USER_MODE'
142
+ );
143
+ Integer total = cursor.getNumRecords();
144
+
145
+ // Fetch in chunks of 200
146
+ for (Integer offset = 0; offset < total; offset += 200) {
147
+ List<Account> chunk = (List<Account>) cursor.fetch(offset, 200);
148
+ processChunk(chunk);
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## CPU Time Optimization
155
+
156
+ ### Common CPU Hogs
157
+
158
+ | Pattern | Fix |
159
+ |---------|-----|
160
+ | Nested loops (`O(n²)`) | Use Map-based lookups |
161
+ | String concatenation in loops | Use `List<String>.join()` |
162
+ | `Schema.getGlobalDescribe()` in loops | Cache outside loop |
163
+ | Complex SOQL field references in loops | Pre-build maps |
164
+ | Regular expressions | Pre-compile Pattern |
165
+
166
+ ### Map-Based Lookup Pattern
167
+
168
+ ```apex
169
+ // ❌ O(n²) — Nested loop
170
+ for (Contact c : contacts) {
171
+ for (Account a : accounts) {
172
+ if (c.AccountId == a.Id) {
173
+ // process
174
+ }
175
+ }
176
+ }
177
+
178
+ // ✅ O(n) — Map lookup
179
+ Map<Id, Account> accountMap = new Map<Id, Account>(accounts);
180
+ for (Contact c : contacts) {
181
+ Account a = accountMap.get(c.AccountId);
182
+ if (a != null) {
183
+ // process
184
+ }
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Async Pattern Selection
191
+
192
+ | Need | Pattern | Why |
193
+ |------|---------|-----|
194
+ | Simple fire-and-forget | `@future` | Lowest overhead |
195
+ | Complex types / chaining | `Queueable` | **Preferred default** |
196
+ | 50K+ records | `Batch Apex` | QueryLocator = 50M rows |
197
+ | Time-based processing | `Schedulable` → Batch | Cron trigger |
198
+ | Cross-system / decoupled | `Platform Events` | Event-driven |
199
+ | Mixed DML | `@future` or `Queueable` | Separate transactions |
200
+
201
+ ### Queueable Pattern (Preferred)
202
+
203
+ ```apex
204
+ public class ProcessAccountsQueueable implements Queueable {
205
+ private List<Id> accountIds;
206
+ private Integer retryCount;
207
+
208
+ public ProcessAccountsQueueable(List<Id> accountIds) {
209
+ this(accountIds, 0);
210
+ }
211
+
212
+ public ProcessAccountsQueueable(List<Id> accountIds, Integer retryCount) {
213
+ this.accountIds = accountIds;
214
+ this.retryCount = retryCount;
215
+ }
216
+
217
+ public void execute(QueueableContext context) {
218
+ try {
219
+ AccountService.heavyProcessing(accountIds);
220
+ } catch (Exception e) {
221
+ if (retryCount < 3) {
222
+ System.enqueueJob(new ProcessAccountsQueueable(accountIds, retryCount + 1));
223
+ } else {
224
+ Logger.error('Max retries exceeded', e);
225
+ }
226
+ }
227
+ }
228
+ }
229
+ ```
230
+
231
+ ---
232
+
233
+ ## DML Optimization
234
+
235
+ ### Partial DML for Fault Tolerance
236
+
237
+ ```apex
238
+ // ✅ Use Database methods with allOrNone=false for bulk tolerance
239
+ Database.SaveResult[] results = Database.insert(records, false);
240
+ for (Database.SaveResult sr : results) {
241
+ if (!sr.isSuccess()) {
242
+ for (Database.Error err : sr.getErrors()) {
243
+ Logger.error('Insert failed for record: ' + err.getMessage());
244
+ }
245
+ }
246
+ }
247
+ ```
248
+
249
+ ### Batch DML at End of Transaction
250
+
251
+ ```apex
252
+ // ❌ BAD — Scattered DML throughout method
253
+ public static void processOrder(Order__c order) {
254
+ update order; // DML 1
255
+ insert lineItems; // DML 2
256
+ update relatedAccount; // DML 3
257
+ EventBus.publish(statusEvent); // DML 4
258
+ }
259
+
260
+ // ✅ GOOD — Collect and batch
261
+ public static void processOrder(Order__c order) {
262
+ List<SObject> toUpdate = new List<SObject>();
263
+ List<SObject> toInsert = new List<SObject>();
264
+
265
+ toUpdate.add(order);
266
+ toInsert.addAll(lineItems);
267
+ toUpdate.add(relatedAccount);
268
+
269
+ insert toInsert;
270
+ update toUpdate;
271
+ EventBus.publish(statusEvent);
272
+ }
273
+ ```
274
+
275
+ ---
276
+
277
+ ## In-Code Monitoring
278
+
279
+ Add this to performance-critical methods:
280
+
281
+ ```apex
282
+ public static void criticalMethod(List<Account> accounts) {
283
+ Long startCpu = Limits.getCpuTime();
284
+ Integer startQueries = Limits.getQueries();
285
+ Integer startHeap = Limits.getHeapSize();
286
+
287
+ // ... method logic ...
288
+
289
+ System.debug(LoggingLevel.INFO, String.format(
290
+ '>>> Performance: CPU={0}ms, SOQL={1}, Heap={2}KB',
291
+ new List<Object>{
292
+ Limits.getCpuTime() - startCpu,
293
+ Limits.getQueries() - startQueries,
294
+ (Limits.getHeapSize() - startHeap) / 1024
295
+ }
296
+ ));
297
+ }
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Red Flags (Performance Code Smells)
303
+
304
+ | Smell | Risk | Fix |
305
+ |-------|------|-----|
306
+ | SOQL inside `for` loop | 🔴 101 limit | Move query before loop, use Map |
307
+ | DML inside `for` loop | 🔴 151 limit | Collect, single DML after loop |
308
+ | `Schema.getGlobalDescribe()` per record | 🔴 CPU timeout | Cache once, reuse |
309
+ | `String += ` in loop with 1000+ iterations | 🟡 Heap + CPU | Use `List<String>` + `.join()` |
310
+ | Loading all fields (`SELECT *` equivalent) | 🟡 Heap | Select only needed fields |
311
+ | Recursive trigger without guard | 🔴 Stack overflow | Static `Set<Id>` guard |
312
+ | `for (SObject s : [SELECT ...])` inside another loop | 🔴 101 limit | Pre-query, use Map |
@@ -0,0 +1,163 @@
1
+ # Agentforce Development Rules
2
+
3
+ > Instructions for AI agents when building Agentforce Topics, Actions, and Prompt Templates.
4
+
5
+ ---
6
+
7
+ ## Core Principle
8
+
9
+ **Declarative First, Code Second.**
10
+
11
+ ```
12
+ Can a Standard Action do it? → Use Standard Action
13
+ Can a Flow do it? → Use Flow Action
14
+ Need complex logic? → Use @InvocableMethod Apex Action
15
+ Need AI-generated text? → Use Prompt Template Action
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Action Design Rules
21
+
22
+ ### 1. Descriptions Are Everything
23
+
24
+ The Atlas Reasoning Engine chooses actions based on natural language descriptions. Write them like you're explaining to a smart colleague what this action does and WHEN to use it.
25
+
26
+ ```apex
27
+ // ✅ GOOD
28
+ @InvocableMethod(
29
+ label='Get Account Health Score'
30
+ description='Calculates a health score (0-100) for an Account based on recent activity, '
31
+ + 'open cases, and engagement metrics. Use when the user asks about account health, '
32
+ + 'risk level, engagement, or churn probability.'
33
+ )
34
+
35
+ // ❌ BAD
36
+ @InvocableMethod(
37
+ label='Process Account'
38
+ description='Does account stuff'
39
+ )
40
+ ```
41
+
42
+ ### 2. Input/Output Descriptions
43
+
44
+ Every `@InvocableVariable` needs a clear description:
45
+
46
+ ```apex
47
+ public class AccountHealthRequest {
48
+ @InvocableVariable(
49
+ label='Account ID'
50
+ description='The 18-character Salesforce ID of the Account to analyze'
51
+ required=true
52
+ )
53
+ public Id accountId;
54
+
55
+ @InvocableVariable(
56
+ label='Include Forecast'
57
+ description='Set to true to include a 90-day churn forecast in the response'
58
+ required=false
59
+ )
60
+ public Boolean includeForecast;
61
+ }
62
+ ```
63
+
64
+ ### 3. Return Meaningful Responses
65
+
66
+ Actions should return structured data that the agent can reason about:
67
+
68
+ ```apex
69
+ public class AccountHealthResponse {
70
+ @InvocableVariable(label='Health Score' description='Score from 0-100, higher is healthier')
71
+ public Integer healthScore;
72
+
73
+ @InvocableVariable(label='Risk Level' description='LOW, MEDIUM, or HIGH')
74
+ public String riskLevel;
75
+
76
+ @InvocableVariable(label='Summary' description='Human-readable summary of account health')
77
+ public String summary;
78
+
79
+ @InvocableVariable(label='Recommendations' description='List of recommended actions')
80
+ public List<String> recommendations;
81
+ }
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Topic Design
87
+
88
+ ### Naming
89
+ - Use clear, business-domain names: "Order Management", "Account Health", "Case Resolution"
90
+ - Avoid technical names: "API Handler", "Data Processor"
91
+
92
+ ### Scope
93
+ - Each Topic should cover a coherent set of related tasks
94
+ - Don't make Topics too broad (entire CRM) or too narrow (single field update)
95
+
96
+ ### Instructions
97
+ Write Topic instructions as if briefing a new team member:
98
+ ```
99
+ You help customers with their orders. You can look up order status, modify shipping
100
+ addresses for orders that haven't shipped yet, and process return requests. Always
101
+ verify the customer's identity before making changes. If an order has already shipped,
102
+ direct the customer to the returns process instead.
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Prompt Template Rules
108
+
109
+ 1. **Ground in data** — Use merge fields from Data Cloud or CRM objects
110
+ 2. **Be specific** — Tell the LLM exactly what format you want
111
+ 3. **Set guardrails** — Include what the agent should NOT do
112
+ 4. **Test extensively** — Different inputs, edge cases, adversarial prompts
113
+
114
+ ### Template Structure
115
+ ```
116
+ You are a {role} helping with {domain}.
117
+
118
+ ## Context
119
+ {Merge fields from Salesforce data}
120
+
121
+ ## Task
122
+ {What to generate/analyze}
123
+
124
+ ## Format
125
+ {Expected output format}
126
+
127
+ ## Guardrails
128
+ - Do NOT {restrictions}
129
+ - Always {requirements}
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Guardrails & Safety
135
+
136
+ | Mechanism | Purpose |
137
+ |-----------|---------|
138
+ | **Agent Script** | Enforce deterministic if/then workflows for critical tasks |
139
+ | **Content Safety Policies** | Filter inappropriate output |
140
+ | **Einstein Trust Layer** | PII masking before LLM, audit logging |
141
+ | **Topic Scoping** | Limit what each agent can access |
142
+ | **Command Center** | Monitor performance, audit trails, policy enforcement |
143
+
144
+ ---
145
+
146
+ ## Testing Agentforce Components
147
+
148
+ 1. **Unit test** all `@InvocableMethod` classes (standard Apex testing)
149
+ 2. **Preview** in Agent Builder with sample conversations
150
+ 3. **Validate action selection** — Does Atlas pick the right action for the right query?
151
+ 4. **Edge cases** — Empty results, invalid input, error responses
152
+ 5. **Multi-turn** — Test conversation flows, not just single queries
153
+
154
+ ---
155
+
156
+ ## Checklist
157
+
158
+ - [ ] Action descriptions are clear, specific, and include "use when" guidance
159
+ - [ ] All input/output variables have labels and descriptions
160
+ - [ ] Apex actions follow all standard Apex conventions (bulkified, secure, tested)
161
+ - [ ] Prompt templates are grounded in live data (not hallucination-prone)
162
+ - [ ] Guardrails configured for sensitive operations
163
+ - [ ] Tested in Agent Builder preview before deployment
@@ -0,0 +1,165 @@
1
+ # Apex Generation Rules
2
+
3
+ > Instructions for AI agents when generating Apex code in this project.
4
+
5
+ ---
6
+
7
+ ## Before Writing Any Apex
8
+
9
+ 1. **Check `conventions.md`** for naming, architecture, and security rules.
10
+ 2. **Check `architecture.md`** for existing classes and patterns in this project.
11
+ 3. **Identify the correct layer** — is this a Trigger, Handler, Service, or Selector?
12
+
13
+ ---
14
+
15
+ ## Mandatory Rules
16
+
17
+ ### Architecture
18
+
19
+ - **One trigger per object** → delegates to `{Object}TriggerHandler`
20
+ - **Handler** routes events → calls `{Object}Service` methods
21
+ - **Service** contains all business logic → calls `{Object}Selector` for data
22
+ - **Selector** contains all SOQL → returns typed collections
23
+ - **Zero business logic in triggers or handlers**
24
+
25
+ ### Security
26
+
27
+ ```apex
28
+ // EVERY class starts with:
29
+ public with sharing class ClassName {
30
+
31
+ // EVERY SOQL query uses:
32
+ [SELECT ... FROM ... WITH USER_MODE]
33
+
34
+ // Use stripInaccessible when handling external data:
35
+ SObjectAccessDecision decision = Security.stripInaccessible(AccessType.CREATABLE, records);
36
+ insert decision.getRecords();
37
+ ```
38
+
39
+ ### Bulkification
40
+
41
+ ```apex
42
+ // ✅ ALWAYS — Collections and maps
43
+ public static void processContacts(List<Contact> contacts) {
44
+ Set<Id> accountIds = new Set<Id>();
45
+ for (Contact c : contacts) {
46
+ accountIds.add(c.AccountId);
47
+ }
48
+ Map<Id, Account> accountMap = new Map<Id, Account>(
49
+ AccountSelector.getByIds(accountIds)
50
+ );
51
+ // Process using map lookups, not queries
52
+ }
53
+
54
+ // ❌ NEVER
55
+ for (Contact c : contacts) {
56
+ Account a = [SELECT Id FROM Account WHERE Id = :c.AccountId]; // SOQL IN LOOP!
57
+ }
58
+ ```
59
+
60
+ ### Error Handling
61
+
62
+ ```apex
63
+ // Custom exceptions for domain errors
64
+ public class OrderProcessingException extends Exception {}
65
+
66
+ // Structured try/catch with context
67
+ try {
68
+ // risky operation
69
+ } catch (SpecificException e) {
70
+ Logger.error('Context: what was happening', e);
71
+ throw new DomainException('User-friendly message', e);
72
+ }
73
+ // NEVER: catch (Exception e) { } — empty catch
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Templates
79
+
80
+ When generating a new class, use these skeletons:
81
+
82
+ ### Trigger
83
+ ```apex
84
+ trigger {Object}Trigger on {Object__c} (
85
+ before insert, before update, before delete,
86
+ after insert, after update, after delete, after undelete
87
+ ) {
88
+ new {Object}TriggerHandler().run();
89
+ }
90
+ ```
91
+
92
+ ### Trigger Handler
93
+ ```apex
94
+ public with sharing class {Object}TriggerHandler extends TriggerHandler {
95
+ public override void beforeInsert() {
96
+ {Object}Service.onBeforeInsert((List<{Object__c}>) Trigger.new);
97
+ }
98
+ public override void afterUpdate() {
99
+ {Object}Service.onAfterUpdate(
100
+ (List<{Object__c}>) Trigger.new,
101
+ (Map<Id, {Object__c}>) Trigger.oldMap
102
+ );
103
+ }
104
+ // Add only the events you need
105
+ }
106
+ ```
107
+
108
+ ### Service
109
+ ```apex
110
+ public with sharing class {Object}Service {
111
+ public static void onBeforeInsert(List<{Object__c}> newRecords) {
112
+ // Business logic here
113
+ }
114
+
115
+ public static void onAfterUpdate(List<{Object__c}> newRecords, Map<Id, {Object__c}> oldMap) {
116
+ // Change detection
117
+ List<{Object__c}> changed = new List<{Object__c}>();
118
+ for ({Object__c} record : newRecords) {
119
+ if (record.Status__c != oldMap.get(record.Id).Status__c) {
120
+ changed.add(record);
121
+ }
122
+ }
123
+ if (!changed.isEmpty()) {
124
+ processStatusChange(changed);
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Selector
131
+ ```apex
132
+ public with sharing class {Object}Selector {
133
+ public static List<{Object__c}> getByIds(Set<Id> ids) {
134
+ return [
135
+ SELECT Id, Name, Status__c, Account__c
136
+ FROM {Object__c}
137
+ WHERE Id IN :ids
138
+ WITH USER_MODE
139
+ ];
140
+ }
141
+
142
+ public static List<{Object__c}> getByAccountIds(Set<Id> accountIds) {
143
+ return [
144
+ SELECT Id, Name, Status__c, Account__c
145
+ FROM {Object__c}
146
+ WHERE Account__c IN :accountIds
147
+ WITH USER_MODE
148
+ ORDER BY CreatedDate DESC
149
+ ];
150
+ }
151
+ }
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Checklist Before Returning Apex Code
157
+
158
+ - [ ] Uses `with sharing`
159
+ - [ ] All SOQL has `WITH USER_MODE`
160
+ - [ ] No SOQL or DML in loops
161
+ - [ ] Handles 200+ records
162
+ - [ ] Custom exception for domain errors
163
+ - [ ] try/catch on callouts with logging
164
+ - [ ] Follows naming conventions from `conventions.md`
165
+ - [ ] Methods have Javadoc comments