@sfqa/scar 0.0.0-stage → 0.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.
@@ -0,0 +1,82 @@
1
+ # Turn one incident into one rule
2
+
3
+ A fill-in-the-blanks prompt for writing a single `@sfqa/scar` rule from an
4
+ incident that already happened. Use this after a postmortem or a hotfix, when the
5
+ details are still fresh. If `sfqa-scar harvest` already produced a candidate,
6
+ paste that candidate as the incident block. For a bulk pass over your whole
7
+ history, use [`extract-rules.md`](extract-rules.md) instead.
8
+
9
+ The output is one JSON rule, plus a positive fixture and a clean fixture, so the
10
+ rule is proven before it merges.
11
+
12
+ ---
13
+
14
+ PROMPT (copy everything below this line, then fill in the incident block):
15
+
16
+ You are turning one real incident into one executable review rule for
17
+ `@sfqa/scar`. You are not writing a style guide. If the incident block does not
18
+ contain enough evidence for a detector, stop and say what is missing.
19
+
20
+ Incident block:
21
+
22
+ - What broke:
23
+ - When (date or sprint, commit or PR if known):
24
+ - The bad line or shape that caused it:
25
+ - The fix that worked:
26
+ - How much it cost (time, duplicate records, leaked secret, false green):
27
+ - Which files, folders or frameworks it applies to:
28
+
29
+ Produce exactly three things.
30
+
31
+ 1. One rule object with exactly these keys:
32
+
33
+ - `"id"`: your next free `ST-###` id.
34
+ - `"rule"`: one imperative sentence. What future code must or must not do.
35
+ - `"context"`: the objects, files or helpers this applies to, in plain words.
36
+ - `"origin"`: `{ "commit": "<hash or null>", "summary": "<1-2 sentences on
37
+ what went wrong and what it cost>" }`. Never invent a hash. If you do not
38
+ have one, use `null` and say the incident is a postmortem rather than a
39
+ commit.
40
+ - `"severity"`: `"high"` for data loss, leaked secrets, wrong records or false
41
+ green; `"med"` for flaky but recoverable; `"low"` for readability.
42
+ - `"detection_pattern"`: what a human reviewer should look for, in concrete
43
+ syntax.
44
+ - `"match"`: a detector using only these keys -
45
+ `pathInclude`, `pathExclude`, `fileNameRules`, `linePatterns`,
46
+ `numericRules`, `labelRules`, `fileRequiredSignals`, `fileExemptSignals`,
47
+ `fileExemptLines`. Regexes are JavaScript `RegExp` source. Scope the rule
48
+ with `pathInclude` so it cannot fire outside where the failure class lives.
49
+ Prefer one precise pattern over several loose ones.
50
+ - `"recommendation"`: the fix that worked, concrete enough to paste.
51
+
52
+ 2. A positive fixture: a minimal file, in the scoped path, containing the bad
53
+ line. The rule must fire on it exactly once.
54
+
55
+ 3. A clean fixture: a minimal file using the recommended fix. The rule must fire
56
+ zero times.
57
+
58
+ Then self-check and report each answer:
59
+
60
+ - Does every regex compile as a JavaScript `RegExp`?
61
+ - Does the positive fixture fire, and only once?
62
+ - Does the clean fixture stay silent?
63
+ - Is the rule scoped narrowly enough that unrelated files cannot trip it?
64
+ - Does the rule describe a repeat of *this* incident, rather than a generic best
65
+ practice you happen to like?
66
+ - Is any real secret, customer value, person's name or client label present? If
67
+ yes, remove it. Labels belong in `labelRules.canonical`, never in a
68
+ hardcoded bad-value list.
69
+
70
+ If the positive fixture cannot be made to fire, say so and mark the rule a
71
+ documented gap. A rule that can never fire is coverage theatre, and shipping one
72
+ is worse than shipping none.
73
+
74
+ ---
75
+
76
+ ## After the prompt
77
+
78
+ - Add the rule to `scar-tissue.json`, then run the two scans the README shows.
79
+ - Commit the rule, both fixtures and the incident summary in one commit.
80
+ - If the rule turns out noisy in the first week, tighten the regex or narrow
81
+ `pathInclude` before you delete it. A false positive teaches you where the
82
+ failure class actually lives.
@@ -0,0 +1,518 @@
1
+ {
2
+ "$schema_note": "A client-free scar-tissue rule set. The 'rule', 'context', 'origin', 'severity' and 'detection_pattern' keys are the human-readable contract; the 'match' object is the machine-executable detector. This file is a TEMPLATE: the example value lists (labelRules, inventory-code shape, local-only file patterns) must be replaced with your org's own canonical labels and conventions. Regenerate a rule set from your own git history with the extraction prompt shipped alongside the package.",
3
+ "version": 1,
4
+ "description": "Template scar-tissue rule set: past-failure patterns that a Salesforce test suite tends to repeat. No client vocabulary.",
5
+ "generated_from_repo": "template (client-free)",
6
+ "rules": [
7
+ {
8
+ "id": "ST-001",
9
+ "rule": "Never hardcode credentials, tokens or secrets in code, config fallbacks, or committed .env files.",
10
+ "context": "Environment config (test-lib/setup/env.ts), any auth or API client code, repository root. Treat any environment that handles personal or payment data accordingly.",
11
+ "origin": {
12
+ "commit": null,
13
+ "pr": null,
14
+ "summary": "Plaintext passwords sat as `|| 'literal'` fallbacks in the environment config, and a real .env containing live credentials was once committed. Removing the file did not remove it from history, forcing repeated rotation commits that re-exposed every new secret."
15
+ },
16
+ "severity": "high",
17
+ "detection_pattern": "Any assignment of a quoted string of 6+ chars to a key named password/passwd/pwd/secret/token/apikey; any tracked .env file. Exempt reads sourced from process.env / a secret manager with empty-string fallbacks. The literal must sit directly after the assignment or its `||` fallback - a key passed into a function call (`map.get(\"password\")`) is a lookup, not a secret.",
18
+ "match": {
19
+ "pathExclude": [
20
+ "**/.env.example",
21
+ "**/node_modules/**"
22
+ ],
23
+ "fileNameRules": [
24
+ {
25
+ "pattern": "^\\.env$"
26
+ }
27
+ ],
28
+ "linePatterns": [
29
+ "(?i)(?:password|passwd|pwd|secret|token|api[_-]?key)\\s*[:=][^'\"\\n(]*['\"](?!\\s*(?:role=|css=|xpath=|text=))[^'\"\\s]{6,}['\"]"
30
+ ],
31
+ "fileExemptLines": [
32
+ "process\\.env\\.[A-Z_]*(PASSWORD|TOKEN|SECRET|KEY)[A-Z_]*\\s*\\|\\|\\s*''"
33
+ ]
34
+ },
35
+ "recommendation": "Source credentials only from environment variables or a secret manager; keep .env gitignored and ship .env.example with empty values. If a secret already reached history, rotate it - deleting the file does not un-leak the value."
36
+ },
37
+ {
38
+ "id": "ST-002",
39
+ "rule": "Each environment config must reference its OWN environment's variables - never another environment's (sit2 must read SIT2_*, not SIT_*).",
40
+ "context": "Multi-environment config blocks (dev/sit/sit2/uat/prod) in test-lib/setup/env.ts.",
41
+ "origin": {
42
+ "commit": null,
43
+ "pr": null,
44
+ "summary": "The sit2 environment block was wired to SIT_* variables, so 'SIT2 runs' silently authenticated against the SIT org. Tests validated the wrong environment and the results were trusted until the wiring was fixed."
45
+ },
46
+ "severity": "high",
47
+ "detection_pattern": "An environment block (e.g. sit2:) whose body references variables prefixed with a DIFFERENT environment name within ~600 chars.",
48
+ "match": {
49
+ "pathInclude": [
50
+ "**/env.ts",
51
+ "**/env*.ts",
52
+ "**/*config*.ts"
53
+ ],
54
+ "pathExclude": [
55
+ "**/node_modules/**"
56
+ ],
57
+ "linePatterns": [
58
+ "sit2:\\s*[\\s\\S]{0,600}?process\\.env\\.SIT_(?!2)[A-Z_]"
59
+ ]
60
+ },
61
+ "recommendation": "For each environment block, assert every process.env reference carries that block's prefix. Add a startup guard that fails fast when a required per-environment variable is missing instead of falling back to a sibling environment."
62
+ },
63
+ {
64
+ "id": "ST-003",
65
+ "rule": "Picklist values must match the Salesforce label EXACTLY - casing, spaces and hyphens (Web not web; Preferred Contact not PreferredContact).",
66
+ "context": "Picklist-driven fields everywhere: features/*.feature tables, test-constants/*.ts, generated data profiles.",
67
+ "origin": {
68
+ "commit": null,
69
+ "pr": null,
70
+ "summary": "Constants shipped near-miss spellings; Salesforce rejected them, so records were created with wrong or blank data. The same drift recurs between constant files whenever the org's picklists change."
71
+ },
72
+ "severity": "high",
73
+ "detection_pattern": "A quoted literal that matches a configured canonical label case-insensitively, or after removing spaces and hyphens, but is not exactly equal to it. The canonical list is per-org input; the list below is an example and must be replaced.",
74
+ "match": {
75
+ "pathInclude": [
76
+ "features/**",
77
+ "test-constants/**",
78
+ "test-lib/**"
79
+ ],
80
+ "pathExclude": [
81
+ "**/node_modules/**"
82
+ ],
83
+ "labelRules": [
84
+ {
85
+ "canonical": [
86
+ "Web",
87
+ "Event",
88
+ "Referral",
89
+ "Phone",
90
+ "Email",
91
+ "SMS",
92
+ "First Name",
93
+ "Last Name",
94
+ "Lead Source",
95
+ "Preferred Contact"
96
+ ]
97
+ }
98
+ ]
99
+ },
100
+ "recommendation": "Copy labels from the org's UI or describe verbatim into ONE safety-net constants file and derive everything else from it. When the org changes picklists, diff against live metadata before editing values."
101
+ },
102
+ {
103
+ "id": "ST-004",
104
+ "rule": "Never pass options to isVisible() - it takes none and returns instantly. Use waitFor({ state: 'visible', timeout }) when render timing matters.",
105
+ "context": "All Playwright page objects (test-pages/web/**), field readers and verification helpers reading Lightning labels.",
106
+ "origin": {
107
+ "commit": null,
108
+ "pr": null,
109
+ "summary": "A field reader called isVisible({ timeout: 5000 }); Playwright ignores options there and returns immediately, so a Lightning label still rendering read as absent and the value was silently reported as empty. The same misuse tends to spread across page objects."
110
+ },
111
+ "severity": "high",
112
+ "detection_pattern": ".isVisible({ ... }) - any argument passed to isVisible, unless the call is .catch()-guarded (an explicit \"is it there right now\" check). Silent false negatives in assertions: no error, just a wrong verdict.",
113
+ "match": {
114
+ "pathInclude": [
115
+ "**/*.ts",
116
+ "**/*.js"
117
+ ],
118
+ "pathExclude": [
119
+ "**/node_modules/**",
120
+ "**/.features-gen/**"
121
+ ],
122
+ "linePatterns": [
123
+ "\\.isVisible\\(\\s*\\{[^{}]{0,200}?\\}\\)(?!\\s*\\.catch)"
124
+ ],
125
+ "fileExemptLines": [
126
+ "\\.catch\\("
127
+ ]
128
+ },
129
+ "recommendation": "Replace with `await locator.waitFor({ state: 'visible', timeout: N }).then(() => true).catch(() => false)` - a visible wait that cannot silently report a stale DOM."
130
+ },
131
+ {
132
+ "id": "ST-005",
133
+ "rule": "Do not treat success toasts as the sole success signal - they can be absent or auto-dismiss before capture on slower environments.",
134
+ "context": "Lightning toast assertions (.toastMessage / forceActionsText), save confirmations and conversion flows in test-pages/web/**.",
135
+ "origin": {
136
+ "commit": null,
137
+ "pr": null,
138
+ "summary": "A duplicate-resolution flow hard-waited for a success toast; on a slower environment the toast was absent or dismissed before capture and green conversions hard-failed. The fix races the toast against a durable path step."
139
+ },
140
+ "severity": "med",
141
+ "detection_pattern": "waitFor/expect on a toast selector without a Promise.race against a durable signal (record URL, path step, primary field render).",
142
+ "match": {
143
+ "pathInclude": [
144
+ "test-pages/**",
145
+ "test-lib/**"
146
+ ],
147
+ "pathExclude": [
148
+ "**/node_modules/**"
149
+ ],
150
+ "linePatterns": [
151
+ "toastMessage",
152
+ "hasText:\\s*'[^']*success"
153
+ ],
154
+ "fileExemptSignals": [
155
+ "Promise\\.race"
156
+ ]
157
+ },
158
+ "recommendation": "Race the toast against a durable post-condition (`Promise.race([toast.waitFor(...), durableSignal.waitFor(...)])`) so a dismissed toast cannot fail a successful save."
159
+ },
160
+ {
161
+ "id": "ST-006",
162
+ "rule": "Record-page navigation checks must tolerate Lightning URL variants (/lightning/r/<Object>/<id>/view AND canonical /lightning/r/<id>/view) and must not rely on a single waitForURL pattern.",
163
+ "context": "Record navigation assertions in test-pages/web/** and the steps that call them.",
164
+ "origin": {
165
+ "commit": null,
166
+ "pr": null,
167
+ "summary": "A save helper used one anchored record-URL regex; on another environment the suffix differed and saves exceeded the timeout. Separately, checks matching only the object-prefixed path broke because in-app navigation yields canonical /lightning/r/<id>/view URLs."
168
+ },
169
+ "severity": "med",
170
+ "detection_pattern": "waitForURL with a strict anchored regex, or URL assertions checking only the object-prefixed path shape.",
171
+ "match": {
172
+ "pathInclude": [
173
+ "test-pages/**",
174
+ "test-lib/**"
175
+ ],
176
+ "pathExclude": [
177
+ "**/node_modules/**"
178
+ ],
179
+ "linePatterns": [
180
+ "waitForURL\\("
181
+ ]
182
+ },
183
+ "recommendation": "Accept both URL shapes (regex like /lightning/r/(?:Object\\/)?<id>\\/view) and poll multiple success signals (URL, primary field rendered, path component) rather than one strict wait."
184
+ },
185
+ {
186
+ "id": "ST-007",
187
+ "rule": "Polling fields populated by async backend integrations must periodically reload the record - Lightning never live-updates server-side-rendered fields.",
188
+ "context": "Any cross-system field surfaced on a Lightning record page and polled in test-pages/web/**.",
189
+ "origin": {
190
+ "commit": null,
191
+ "pr": null,
192
+ "summary": "Integration-populated fields render server-side after record creation; polling the open page stalled forever because the DOM never updated. The fix reloads the record every few stalled polls within a fixed time budget."
193
+ },
194
+ "severity": "med",
195
+ "detection_pattern": "Deadline-based polling loops (Date.now() < deadline / MAX_WAIT) in page objects with NO reload/goto capability in the same file.",
196
+ "match": {
197
+ "pathInclude": [
198
+ "test-pages/**"
199
+ ],
200
+ "pathExclude": [
201
+ "**/node_modules/**"
202
+ ],
203
+ "linePatterns": [
204
+ "Date\\.now\\(\\)\\s*<\\s*deadline",
205
+ "MAX_WAIT"
206
+ ],
207
+ "fileExemptSignals": [
208
+ "reload\\("
209
+ ]
210
+ },
211
+ "recommendation": "Inside the poll loop, count stalled attempts and `await page.reload()` + re-open the Details tab every N attempts, bounded. A deadline alone cannot see server-side data."
212
+ },
213
+ {
214
+ "id": "ST-008",
215
+ "rule": "Never hardcode mutable business records (specific inventory units, named accounts or orders) in scenarios - use availability sentinels or search fallbacks.",
216
+ "context": "features/**/*.feature Examples tables, assignment steps, any fixture referencing consumable inventory.",
217
+ "origin": {
218
+ "commit": null,
219
+ "pr": null,
220
+ "summary": "Smoke scenarios pinned concrete inventory codes; each green run CONSUMED the record and the next run failed on 'not available'. The fix introduced a sentinel so the product's search picks the freshest eligible row at runtime."
221
+ },
222
+ "severity": "high",
223
+ "detection_pattern": "Concrete inventory codes in feature tables or step expectations. The shape below is an EXAMPLE (project-floor-unit); replace it with your org's identifier shape.",
224
+ "match": {
225
+ "pathInclude": [
226
+ "features/**",
227
+ "test-lib/bdd/**"
228
+ ],
229
+ "pathExclude": [
230
+ "**/node_modules/**"
231
+ ],
232
+ "linePatterns": [
233
+ "[A-Za-z][A-Za-z0-9]*-(?:B|T)\\d{1,2}-\\d{1,2}-\\d{1,2}"
234
+ ]
235
+ },
236
+ "recommendation": "Use a sentinel (e.g. AUTO) resolved by an availability-aware search with deterministic tie-breaking, logging what was actually selected."
237
+ },
238
+ {
239
+ "id": "ST-009",
240
+ "rule": "Per-test timeout must exceed the MEASURED duration of the longest legitimate journey - a timeout that fires mid-green-run causes retries that create duplicate records.",
241
+ "context": "playwright.config.ts global timeout vs the longest end-to-end journey (login + create/convert + booking + verification).",
242
+ "origin": {
243
+ "commit": null,
244
+ "pr": null,
245
+ "summary": "The timeout was bumped repeatedly (3min -> 5min -> 10min). At 300s the cap expired during the final steps of fully-green runs; Playwright retried, and each retry created ANOTHER business record in a shared environment."
246
+ },
247
+ "severity": "high",
248
+ "detection_pattern": "Top-level test timeout set below the measured journey length. The numeric ceiling below is an example; set it from your own measured duration.",
249
+ "match": {
250
+ "pathInclude": [
251
+ "playwright.config.*"
252
+ ],
253
+ "numericRules": [
254
+ {
255
+ "pattern": "^\\s*timeout:\\s*(\\d+)",
256
+ "flags": "gm",
257
+ "min": 0,
258
+ "max": 359999
259
+ }
260
+ ]
261
+ },
262
+ "recommendation": "Time the longest journey from real runs, add headroom (~2x), and make create/book flows idempotent or retry-safe - timeout expiry on a non-idempotent flow writes duplicate data."
263
+ },
264
+ {
265
+ "id": "ST-010",
266
+ "rule": "Keep the reporter-finalization window free of teardown work - no report zipping/moving in globalTeardown, no broad pkill.",
267
+ "context": "playwright.config.ts lifecycle hooks and CI artifact steps (.github/workflows/**).",
268
+ "origin": {
269
+ "commit": null,
270
+ "pr": null,
271
+ "summary": "GlobalTeardown zipped the HTML report, but teardown runs BEFORE reporter.onEnd, so it zipped a stale live report; its `pkill -f chrome` also matched any process with that substring system-wide. Teardown was deleted and CI uploads the results directory directly."
272
+ },
273
+ "severity": "med",
274
+ "detection_pattern": "globalTeardown wiring touching report artifacts, or pkill -f with a generic process name.",
275
+ "match": {
276
+ "pathInclude": [
277
+ "playwright.config.*",
278
+ ".github/**",
279
+ "*.config.*"
280
+ ],
281
+ "linePatterns": [
282
+ "globalTeardown",
283
+ "pkill\\s+-f"
284
+ ]
285
+ },
286
+ "recommendation": "Let Playwright close workers itself; do artifact packaging in CI AFTER the job, and never reap processes by fuzzy name."
287
+ },
288
+ {
289
+ "id": "ST-011",
290
+ "rule": "Authentication flows must go through the shared LoginPage flow - never hardcode standard-login selectors that break on SSO environments.",
291
+ "context": "Session bootstrap (test-lib/setup/**) and any direct login automation; SSO environments redirect away from the standard Salesforce login form.",
292
+ "origin": {
293
+ "commit": null,
294
+ "pr": null,
295
+ "summary": "A pre-auth step hardcoded the standard username input, which only exists on the login form; behind an SSO redirect the selector never matched and the browser sat on the login page forever. The fix delegates to the shared LoginPage flow."
296
+ },
297
+ "severity": "high",
298
+ "detection_pattern": "Direct selectors for username/password inputs outside LoginPage; session-validation timeouts under ~20s (false 'dead' verdicts cost a full MFA round trip).",
299
+ "match": {
300
+ "pathInclude": [
301
+ "test-lib/setup/**",
302
+ "test-pages/**"
303
+ ],
304
+ "pathExclude": [
305
+ "**/LoginPage.ts",
306
+ "**/node_modules/**"
307
+ ],
308
+ "linePatterns": [
309
+ "input\\[name=[\"']username[\"']\\]",
310
+ "input\\[name=[\"']password[\"']\\]"
311
+ ]
312
+ },
313
+ "recommendation": "Reuse LoginPage for every auth path; treat 'login form vanished' as possible success and verify the logged-in state with a generous timeout before failing."
314
+ },
315
+ {
316
+ "id": "ST-012",
317
+ "rule": "Shared-browser/human-in-the-loop logins need race tolerance: if credential fill fails because a human already logged in, adopt that session instead of throwing.",
318
+ "context": "The shared LoginPage and pre-auth session validation (a headful window reused by engineers).",
319
+ "origin": {
320
+ "commit": null,
321
+ "pr": null,
322
+ "summary": "When a human typed credentials or approved MFA faster than the script, credential fill timed out and the run failed even though the browser was fully logged in. Separately, a 15s logged-in check produced false 'dead session' verdicts costing an MFA round trip per attempt."
323
+ },
324
+ "severity": "med",
325
+ "detection_pattern": "The logged-in probe (#oneHeader or equivalent) waits shorter than 30s, or credential-fill failures that do not re-check logged-in state before throwing.",
326
+ "match": {
327
+ "pathInclude": [
328
+ "test-lib/setup/**",
329
+ "test-pages/web/LoginPage.ts"
330
+ ],
331
+ "numericRules": [
332
+ {
333
+ "pattern": "#oneHeader[\\s\\S]{0,200}?timeout:\\s*(\\d+)",
334
+ "flags": "gm",
335
+ "min": 0,
336
+ "max": 29999
337
+ }
338
+ ]
339
+ },
340
+ "recommendation": "Validate sessions with >=60s budgets on cold environments and wrap manual-fill steps in try/catch that re-probes logged-in state before failing."
341
+ },
342
+ {
343
+ "id": "ST-013",
344
+ "rule": "After impersonation (Login-As) or a popup/tab switch, rebuild page objects on the world-tracked ACTIVE page - the injected `page` fixture points at the wrong tab.",
345
+ "context": "BDD steps orchestrating a user switch plus booking flows (test-lib/bdd/steps/**) and HomePage.switchToUser* helpers.",
346
+ "origin": {
347
+ "commit": null,
348
+ "pr": null,
349
+ "summary": "Steps constructed page objects from the injected page fixture while the actual navigation happened on the impersonated tab. Locators hit a stale or blank page and flaked. The fix builds subsequent page objects from the world's active page."
350
+ },
351
+ "severity": "med",
352
+ "detection_pattern": "In steps: `new SomePage(page, ...)` using the raw fixture in files that also perform impersonation or tab switches.",
353
+ "match": {
354
+ "pathInclude": [
355
+ "test-lib/bdd/steps/**"
356
+ ],
357
+ "pathExclude": [
358
+ "**/node_modules/**"
359
+ ],
360
+ "linePatterns": [
361
+ "new\\s+(?:LeadPage|OpportunityPage|SalesOrderPage|HomePage|UnitPage)\\(\\s*page\\s*,"
362
+ ],
363
+ "fileExemptSignals": [
364
+ "activePage"
365
+ ]
366
+ },
367
+ "recommendation": "Track the active page in the BDD world after every switch or popup and construct page objects from that."
368
+ },
369
+ {
370
+ "id": "ST-014",
371
+ "rule": "Lead conversion has TWO runtime outcomes (fresh account vs existing-account duplicate). Verification must branch at runtime and read account identity from the record, never assume names from lead data.",
372
+ "context": "Lead conversion and duplicate-account resolution (LeadPage conversion methods, conversion steps).",
373
+ "origin": {
374
+ "commit": null,
375
+ "pr": null,
376
+ "summary": "Asserting that the converted account name contains the lead's name breaks on the duplicate path, where the converted account pre-exists under a different name - a false red, or a false green if skipped blindly. The fix gates the name check on the duplicate-detected branch and reads the real account name and id from the page."
377
+ },
378
+ "severity": "high",
379
+ "detection_pattern": "Assertions tying converted-account/opportunity names to lead-derived names without a duplicate-detected branch guard.",
380
+ "match": {
381
+ "pathInclude": [
382
+ "test-pages/**",
383
+ "test-lib/bdd/**"
384
+ ],
385
+ "pathExclude": [
386
+ "**/node_modules/**"
387
+ ],
388
+ "linePatterns": [
389
+ "(?:account|opportunity)Name[\\s\\S]{0,120}?toContain\\("
390
+ ],
391
+ "fileExemptSignals": [
392
+ "duplicateDetected"
393
+ ]
394
+ },
395
+ "recommendation": "Branch on the duplicate-account check; read Account/Opportunity ids and names from the record page, never from lead data."
396
+ },
397
+ {
398
+ "id": "ST-015",
399
+ "rule": "Field-dependency maps and picklist constants must be verified against the LIVE org before merge - they describe validation rules that change without notice.",
400
+ "context": "test-constants/*.ts, safety-net constants and generated-data dependency fixes.",
401
+ "origin": {
402
+ "commit": null,
403
+ "pr": null,
404
+ "summary": "Dependency constants encoded values that no longer existed in the org; generated records were rejected by Salesforce validation. Later dependency rules were only added after form rejections were debugged live."
405
+ },
406
+ "severity": "med",
407
+ "detection_pattern": "A constants/dependency file that declares picklist-like value lists but carries no provenance marker (verified / source: describe / probe:) in the same file.",
408
+ "match": {
409
+ "pathInclude": [
410
+ "test-constants/**"
411
+ ],
412
+ "pathExclude": [
413
+ "**/node_modules/**"
414
+ ],
415
+ "fileRequiredSignals": [
416
+ "(?i)verified|source:\\s*describe|probe:"
417
+ ]
418
+ },
419
+ "recommendation": "Before merging new picklist usage, dump the org's picklist dependencies and reconcile the maps. Record the source and date in the file so the next reader knows when it was last verified, and protect each field with a safety-net entry so drift fails at generation time."
420
+ },
421
+ {
422
+ "id": "ST-016",
423
+ "rule": "Correlated fields in generated data must be co-constrained (salutation <-> gender) - independent randomization produces impossible records.",
424
+ "context": "test-lib/utils/TestDataGenerator.ts and any generator producing semantically linked field pairs.",
425
+ "origin": {
426
+ "commit": null,
427
+ "pr": null,
428
+ "summary": "Salutation and gender were randomized independently, producing internally contradictory records that undermine trust in verification. The fix links them (Mr. -> Male, Ms./Mrs. -> Female, Dr./Prof. -> any) and makes overrides constrain each other."
429
+ },
430
+ "severity": "low",
431
+ "detection_pattern": "A generator file that randomizes correlated fields without a coupling function or coupling map in the same file.",
432
+ "match": {
433
+ "pathInclude": [
434
+ "test-lib/utils/TestDataGenerator.ts",
435
+ "**/*DataGenerator*"
436
+ ],
437
+ "fileRequiredSignals": [
438
+ "getGenderForSalutation|salutationFromGender|SALUTATION_GENDER"
439
+ ]
440
+ },
441
+ "recommendation": "Define explicit coupling maps for correlated pairs and let explicit overrides propagate BOTH ways."
442
+ },
443
+ {
444
+ "id": "ST-017",
445
+ "rule": "Before adding any helper or constants file, search for an existing one - duplicated utilities drift apart and half of them are dead (or would throw if ever called).",
446
+ "context": "Repo-wide: two locator readers (one reading a file that did not exist), two modules exporting the same lookup names, a duplicated field-display map, and a delay() defined twice with DIFFERENT UNITS (ms vs seconds).",
447
+ "origin": {
448
+ "commit": null,
449
+ "pr": null,
450
+ "summary": "An audit removed dead duplicates and unused imports, but a cleanup checklist documented remaining twins - including two delay helpers with colliding units, a silent bug waiting for a caller."
451
+ },
452
+ "severity": "med",
453
+ "detection_pattern": "Second definition of an existing helper name, the same export from two modules, or the same concept with different units/signatures.",
454
+ "match": {
455
+ "pathInclude": [
456
+ "test-lib/**",
457
+ "*.ts"
458
+ ],
459
+ "pathExclude": [
460
+ "**/node_modules/**"
461
+ ],
462
+ "linePatterns": [
463
+ "(?:export\\s+(?:async\\s+)?function|class)\\s+(?:delay|YamlReader|LocatorManager|locatorReader)\\b",
464
+ "function delay\\("
465
+ ]
466
+ },
467
+ "recommendation": "Reuse -> extend -> generalize -> create. If two near-duplicates exist, unify them BEFORE building a third thing on top."
468
+ },
469
+ {
470
+ "id": "ST-018",
471
+ "rule": "Local-only artifacts (personnel notes, diagnostics, reports, captured traffic) are gitignored from birth - never committed then purged later.",
472
+ "context": "Repository hygiene: personal feedback notes, report archives, captured diagnostics and stray results directories.",
473
+ "origin": {
474
+ "commit": null,
475
+ "pr": null,
476
+ "summary": "Personal notes naming colleagues and diagnostic scripts were committed and later had to be purged from tracking - content already cloned by teammates cannot be un-shared. The same class includes large report archives and stray results directories."
477
+ },
478
+ "severity": "low",
479
+ "detection_pattern": "Files matching known local-only patterns being added to git. This rule has no machine detector yet and is reported as a gap.",
480
+ "match": {
481
+ "pathInclude": [
482
+ "local-notes-*.md",
483
+ "*.zip",
484
+ "test-results/**",
485
+ "untitled folder/**"
486
+ ]
487
+ },
488
+ "recommendation": "Decide local-vs-repo at file creation; anything with names, credentials or machine-specific paths goes in .gitignore immediately."
489
+ },
490
+ {
491
+ "id": "ST-019",
492
+ "rule": "Read a record header from a scoped container, never a bare comma-list of element and class selectors plus .first().",
493
+ "context": "Lightning record page objects that read a record number, id or status from the page header.",
494
+ "origin": {
495
+ "commit": "79dafa3",
496
+ "pr": null,
497
+ "summary": "A header read used locator('lightning-formatted-text, .slds-page-header__title').first(); the first match was whichever node the DOM ordered first, so the journey read the wrong value. The fix replaced the list with a scoped header container, and the same list survived in another page object."
498
+ },
499
+ "severity": "med",
500
+ "detection_pattern": "A locator argument that is a bare comma-list mixing lightning-formatted-text with .slds-page-header__title, or .slds-form-element with a lightning-button selector, followed by .first(). The list matches containers, not the value.",
501
+ "match": {
502
+ "pathInclude": [
503
+ "**/test-pages/**/*.ts",
504
+ "**/pages/**/*.ts",
505
+ "**/page-objects/**/*.ts"
506
+ ],
507
+ "pathExclude": [
508
+ "**/node_modules/**"
509
+ ],
510
+ "linePatterns": [
511
+ "locator\\(\\s*['\"][^'\"]*lightning-formatted-text\\s*,\\s*\\.slds-page-header__title[^'\"]*['\"]\\)",
512
+ "locator\\(\\s*['\"][^'\"]*\\.slds-form-element\\s*,\\s*[^'\"]*lightning-button[^'\"]*['\"]\\)"
513
+ ]
514
+ },
515
+ "recommendation": "Scope the read to the record header (records-highlights2 or .slds-page-header) or to the field container, scan every candidate text, and accept only the text that matches the expected pattern. Never trust .first() on a list that mixes a container with a value element."
516
+ }
517
+ ]
518
+ }