lemmaly 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.
Files changed (97) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +238 -0
  3. package/cli/gen-agents-md.js +60 -0
  4. package/cli/gen-rule-docs.js +885 -0
  5. package/cli/lemmaly.js +162 -0
  6. package/commands/benchmark.md +40 -0
  7. package/commands/budget.md +53 -0
  8. package/commands/complexity.md +26 -0
  9. package/commands/cut.md +27 -0
  10. package/commands/hotpath.md +22 -0
  11. package/commands/invariant.md +22 -0
  12. package/commands/n-plus-one.md +20 -0
  13. package/commands/profile.md +34 -0
  14. package/commands/regress.md +43 -0
  15. package/commands/scale-check.md +37 -0
  16. package/commands/ship-check.md +26 -0
  17. package/package.json +48 -0
  18. package/rules/cpp.json +46 -0
  19. package/rules/csharp.json +38 -0
  20. package/rules/go.json +46 -0
  21. package/rules/java.json +38 -0
  22. package/rules/javascript.json +102 -0
  23. package/rules/php.json +38 -0
  24. package/rules/python.json +62 -0
  25. package/rules/ruby.json +38 -0
  26. package/rules/rust.json +38 -0
  27. package/rules/shell.json +38 -0
  28. package/rules/sql.json +54 -0
  29. package/skills/complexity-cuts/SKILL.md +259 -0
  30. package/skills/invariant-guard/SKILL.md +310 -0
  31. package/skills/lemmaly/AGENTS.md +1869 -0
  32. package/skills/lemmaly/SKILL.md +365 -0
  33. package/skills/lemmaly/references/async.md +135 -0
  34. package/skills/lemmaly/references/complexity.md +66 -0
  35. package/skills/lemmaly/references/hot-paths.md +87 -0
  36. package/skills/lemmaly/references/memory.md +118 -0
  37. package/skills/lemmaly/references/n-plus-one.md +139 -0
  38. package/skills/lemmaly/rules/cpp-map-double-lookup.md +38 -0
  39. package/skills/lemmaly/rules/cpp-range-loop-copy.md +33 -0
  40. package/skills/lemmaly/rules/cpp-raw-new.md +36 -0
  41. package/skills/lemmaly/rules/cpp-string-concat-in-loop.md +45 -0
  42. package/skills/lemmaly/rules/cpp-vector-push-no-reserve.md +40 -0
  43. package/skills/lemmaly/rules/cs-async-void.md +45 -0
  44. package/skills/lemmaly/rules/cs-disposable-no-using.md +32 -0
  45. package/skills/lemmaly/rules/cs-list-contains-in-loop.md +36 -0
  46. package/skills/lemmaly/rules/cs-string-concat-in-loop.md +42 -0
  47. package/skills/lemmaly/rules/go-defer-in-loop.md +39 -0
  48. package/skills/lemmaly/rules/go-err-not-checked.md +38 -0
  49. package/skills/lemmaly/rules/go-loop-var-capture.md +47 -0
  50. package/skills/lemmaly/rules/go-slice-append-no-cap.md +39 -0
  51. package/skills/lemmaly/rules/go-string-concat-in-loop.md +44 -0
  52. package/skills/lemmaly/rules/java-arraylist-remove-in-for-i.md +44 -0
  53. package/skills/lemmaly/rules/java-bare-catch-exception.md +42 -0
  54. package/skills/lemmaly/rules/java-list-contains-in-loop.md +40 -0
  55. package/skills/lemmaly/rules/java-string-concat-in-loop.md +42 -0
  56. package/skills/lemmaly/rules/js-anonymous-handler-jsx.md +31 -0
  57. package/skills/lemmaly/rules/js-array-key-index.md +29 -0
  58. package/skills/lemmaly/rules/js-async-in-foreach.md +43 -0
  59. package/skills/lemmaly/rules/js-await-in-for-loop.md +41 -0
  60. package/skills/lemmaly/rules/js-deep-clone-via-json.md +33 -0
  61. package/skills/lemmaly/rules/js-helper-call-in-iterator.md +41 -0
  62. package/skills/lemmaly/rules/js-includes-in-iterator.md +37 -0
  63. package/skills/lemmaly/rules/js-inline-object-jsx-prop.md +35 -0
  64. package/skills/lemmaly/rules/js-nested-for-loops.md +45 -0
  65. package/skills/lemmaly/rules/js-spread-in-reduce.md +38 -0
  66. package/skills/lemmaly/rules/js-unique-via-indexof.md +35 -0
  67. package/skills/lemmaly/rules/js-useeffect-missing-deps.md +33 -0
  68. package/skills/lemmaly/rules/php-count-in-for-condition.md +45 -0
  69. package/skills/lemmaly/rules/php-in-array-in-loop.md +42 -0
  70. package/skills/lemmaly/rules/php-loose-equality.md +35 -0
  71. package/skills/lemmaly/rules/php-query-in-loop.md +47 -0
  72. package/skills/lemmaly/rules/py-bare-except.md +39 -0
  73. package/skills/lemmaly/rules/py-django-loop-without-eager.md +42 -0
  74. package/skills/lemmaly/rules/py-in-list-literal.md +37 -0
  75. package/skills/lemmaly/rules/py-mutable-default-arg.md +39 -0
  76. package/skills/lemmaly/rules/py-open-without-with.md +33 -0
  77. package/skills/lemmaly/rules/py-range-len.md +35 -0
  78. package/skills/lemmaly/rules/py-string-concat-in-loop.md +43 -0
  79. package/skills/lemmaly/rules/rb-bare-rescue.md +41 -0
  80. package/skills/lemmaly/rules/rb-include-in-iterator.md +37 -0
  81. package/skills/lemmaly/rules/rb-n-plus-one-activerecord.md +39 -0
  82. package/skills/lemmaly/rules/rb-string-concat-in-loop.md +39 -0
  83. package/skills/lemmaly/rules/rs-clone-in-loop.md +38 -0
  84. package/skills/lemmaly/rules/rs-string-push-no-capacity.md +43 -0
  85. package/skills/lemmaly/rules/rs-unwrap-in-prod.md +36 -0
  86. package/skills/lemmaly/rules/rs-vec-push-no-capacity.md +42 -0
  87. package/skills/lemmaly/rules/sh-for-ls.md +41 -0
  88. package/skills/lemmaly/rules/sh-set-e-no-pipefail.md +37 -0
  89. package/skills/lemmaly/rules/sh-unquoted-var.md +35 -0
  90. package/skills/lemmaly/rules/sh-useless-cat-pipe.md +32 -0
  91. package/skills/lemmaly/rules/sql-leading-wildcard-like.md +34 -0
  92. package/skills/lemmaly/rules/sql-not-in-subquery.md +38 -0
  93. package/skills/lemmaly/rules/sql-or-in-where.md +35 -0
  94. package/skills/lemmaly/rules/sql-select-no-limit.md +37 -0
  95. package/skills/lemmaly/rules/sql-select-star.md +29 -0
  96. package/skills/lemmaly/rules/sql-update-no-where.md +35 -0
  97. package/skills/mathguard/SKILL.md +277 -0
@@ -0,0 +1,1869 @@
1
+ # Lemmaly Rule Catalog (compiled)
2
+
3
+ Full compiled view of every CLI rule with hand-authored Incorrect/Correct examples and escalation path. Generated from `rules/*.json` and `skills/lemmaly/rules/*.md`.
4
+
5
+ For per-rule documents, load `skills/lemmaly/rules/<rule-id>.md`. For the deterministic CLI, run `node cli/lemmaly.js scan <path>`.
6
+
7
+
8
+ ## CRITICAL — will break or scale-fail in production
9
+
10
+ ### `cs-async-void` — csharp
11
+
12
+ # async void — exceptions are unobserved and crash the process
13
+
14
+ `async void` cannot be awaited. Exceptions raised inside are unobserved and crash the process — they bypass `try/catch` at the call site. The only legitimate use is true event handlers (e.g. `OnClick`).
15
+
16
+ ## Fix
17
+
18
+ Return Task. async void is only acceptable for true event handlers (OnClick, etc.).
19
+
20
+ ## Incorrect
21
+
22
+ ```csharp
23
+ public async void DoWork() {
24
+ await Task.Delay(100);
25
+ throw new Exception("boom"); // crashes the process
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```csharp
32
+ public async Task DoWork() {
33
+ await Task.Delay(100);
34
+ throw new Exception("boom"); // caller can await + catch
35
+ }
36
+
37
+ // Event handlers stay async void with a try/catch at the boundary:
38
+ private async void OnClick(object s, EventArgs e) {
39
+ try { await DoWorkAsync(); }
40
+ catch (Exception ex) { _log.LogError(ex, "OnClick failed"); }
41
+ }
42
+ ```
43
+
44
+ ## Escalate to
45
+
46
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
47
+
48
+ ### `go-loop-var-capture` — go
49
+
50
+ # Loop variable captured by goroutine — pre-1.22 races on the last value
51
+
52
+ Before Go 1.22, the loop variable in `for ... := range` was reused across iterations. Goroutines that closed over it all read the *same* (final) value. Go 1.22+ fixes this at the language level, but the pattern still appears in libraries that target older versions.
53
+
54
+ ## Fix
55
+
56
+ Pin the variable inside the loop: `i := i` before `go func() { use(i) }()`, or pass as argument: `go func(i int) { ... }(i)`. (Fixed automatically in Go 1.22+.)
57
+
58
+ ## Incorrect
59
+
60
+ ```go
61
+ // Pre-1.22: all goroutines print the last item
62
+ for _, v := range items {
63
+ go func() {
64
+ fmt.Println(v)
65
+ }()
66
+ }
67
+ ```
68
+
69
+ ## Correct
70
+
71
+ ```go
72
+ // Pin the variable explicitly
73
+ for _, v := range items {
74
+ v := v
75
+ go func() { fmt.Println(v) }()
76
+ }
77
+
78
+ // Or pass as a parameter
79
+ for _, v := range items {
80
+ go func(v string) { fmt.Println(v) }(v)
81
+ }
82
+ ```
83
+
84
+ ## Escalate to
85
+
86
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
87
+
88
+ ### `java-arraylist-remove-in-for-i` — java
89
+
90
+ # list.remove(i) inside for-i — index shifts; ConcurrentModification risk
91
+
92
+ Removing from an `ArrayList` inside a `for (int i = 0; i < list.size(); i++)` shifts indices and skips elements — and on a `Collections.synchronizedList` or in concurrent code, it throws `ConcurrentModificationException`.
93
+
94
+ ## Fix
95
+
96
+ Iterate backwards, use Iterator.remove(), or collect indexes and remove in one removeIf().
97
+
98
+ ## Incorrect
99
+
100
+ ```java
101
+ for (int i = 0; i < list.size(); i++) {
102
+ if (shouldRemove(list.get(i))) {
103
+ list.remove(i); // shifts everything; next element is skipped
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## Correct
109
+
110
+ ```java
111
+ // Idiomatic and correct
112
+ list.removeIf(this::shouldRemove);
113
+
114
+ // Or, with an explicit iterator:
115
+ var it = list.iterator();
116
+ while (it.hasNext()) {
117
+ if (shouldRemove(it.next())) it.remove();
118
+ }
119
+ ```
120
+
121
+ ## Escalate to
122
+
123
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
124
+
125
+ ### `js-async-in-foreach` — javascript
126
+
127
+ # async passed to .forEach — returned promises are dropped
128
+
129
+ `Array.prototype.forEach` ignores return values. Passing an `async` function returns promises that are dropped — errors are swallowed and the caller continues before the work finishes.
130
+
131
+ ## Fix
132
+
133
+ Use `for (const x of arr) await fn(x)` or `await Promise.all(arr.map(fn))`.
134
+
135
+ ## Incorrect
136
+
137
+ ```ts
138
+ // Promises dropped; errors silently swallowed
139
+ items.forEach(async (item) => {
140
+ await save(item);
141
+ });
142
+ console.log('done'); // logs before any save completes
143
+ ```
144
+
145
+ ## Correct
146
+
147
+ ```ts
148
+ // Sequential, with errors propagated
149
+ for (const item of items) {
150
+ await save(item);
151
+ }
152
+
153
+ // Parallel
154
+ await Promise.all(items.map((item) => save(item)));
155
+ ```
156
+
157
+ ## Escalate to
158
+
159
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
160
+
161
+ ### `js-await-in-for-loop` — javascript
162
+
163
+ # await inside for-loop — likely N+1 / serialized I/O
164
+
165
+ Awaiting inside a `for` over independent items serializes wall-clock work into O(n × latency). For network or DB calls this is the N+1 problem.
166
+
167
+ ## Fix
168
+
169
+ Collect promises into an array and use Promise.all(...), or batch with a bulk query (WHERE id IN (...)).
170
+
171
+ ## Incorrect
172
+
173
+ ```ts
174
+ // Wall-clock: O(n × latency)
175
+ for (const id of ids) {
176
+ const user = await db.users.findById(id);
177
+ results.push(user);
178
+ }
179
+ ```
180
+
181
+ ## Correct
182
+
183
+ ```ts
184
+ // Wall-clock: O(latency); one bulk query
185
+ const users = await db.users.findMany({ where: { id: { in: ids } } });
186
+
187
+ // Or, if calls are truly independent:
188
+ const results = await Promise.all(ids.map(id => db.users.findById(id)));
189
+ ```
190
+
191
+ ## Escalate to
192
+
193
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
194
+
195
+ ### `php-query-in-loop` — php
196
+
197
+ # DB query inside loop — N+1
198
+
199
+ A SQL query inside a loop is the textbook N+1 problem. 1000 orders, 1000 round-trips to the DB. Batch with `WHERE id IN (...)` or eager-load in your ORM.
200
+
201
+ ## Fix
202
+
203
+ Collect the ids, run ONE `WHERE id IN (...)` query, index by id, then loop. In Eloquent use eager loading: `Model::with('relation')`.
204
+
205
+ ## Incorrect
206
+
207
+ ```php
208
+ <?php
209
+ foreach ($orderIds as $id) {
210
+ $row = $db->query("SELECT * FROM orders WHERE id = $id"); // 1 query per id
211
+ process($row);
212
+ }
213
+ ```
214
+
215
+ ## Correct
216
+
217
+ ```php
218
+ <?php
219
+ // One bulk query
220
+ $placeholders = implode(',', array_fill(0, count($orderIds), '?'));
221
+ $stmt = $db->prepare("SELECT * FROM orders WHERE id IN ($placeholders)");
222
+ $stmt->execute($orderIds);
223
+ foreach ($stmt->fetchAll() as $row) {
224
+ process($row);
225
+ }
226
+
227
+ // Eloquent / Doctrine: use eager loading
228
+ $orders = Order::with('items')->whereIn('id', $orderIds)->get();
229
+ ```
230
+
231
+ ## Escalate to
232
+
233
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
234
+
235
+ ### `py-mutable-default-arg` — python
236
+
237
+ # Mutable default argument — shared across calls
238
+
239
+ Python evaluates default arguments once, at function definition. A mutable default (list, dict, set) is **shared across every call** — calls accumulate state from previous calls.
240
+
241
+ ## Fix
242
+
243
+ Default to None; create inside: `def f(x=None): x = x or []`.
244
+
245
+ ## Incorrect
246
+
247
+ ```python
248
+ def collect(item, bucket=[]): # shared across all calls!
249
+ bucket.append(item)
250
+ return bucket
251
+ ```
252
+
253
+ ## Correct
254
+
255
+ ```python
256
+ def collect(item, bucket=None):
257
+ if bucket is None:
258
+ bucket = []
259
+ bucket.append(item)
260
+ return bucket
261
+ ```
262
+
263
+ ## Escalate to
264
+
265
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
266
+
267
+ ### `rb-n-plus-one-activerecord` — ruby
268
+
269
+ # ActiveRecord iteration touching an association — N+1 without includes
270
+
271
+ Iterating an ActiveRecord scope and touching an association fires one extra query per row. `includes` (preload or eager_load) fetches everything in a constant number of queries.
272
+
273
+ ## Fix
274
+
275
+ Use `Model.includes(:assoc)` or `:preload` / `:eager_load` before iterating.
276
+
277
+ ## Incorrect
278
+
279
+ ```ruby
280
+ # N+1 queries
281
+ Post.all.each do |p|
282
+ puts p.author.name # 1 extra query per post
283
+ end
284
+ ```
285
+
286
+ ## Correct
287
+
288
+ ```ruby
289
+ # 2 queries total (or 1 with eager_load)
290
+ Post.includes(:author).each do |p|
291
+ puts p.author.name
292
+ end
293
+ ```
294
+
295
+ ## Escalate to
296
+
297
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
298
+
299
+ ### `sql-update-no-where` — sql
300
+
301
+ # UPDATE / DELETE without WHERE — touches every row
302
+
303
+ An `UPDATE` or `DELETE` without a `WHERE` clause rewrites every row in the table. In production this is an incident, not a bug.
304
+
305
+ ## Fix
306
+
307
+ Add a WHERE clause, or confirm in a comment that touching all rows is intentional.
308
+
309
+ ## Incorrect
310
+
311
+ ```sql
312
+ UPDATE users SET active = false;
313
+ DELETE FROM sessions;
314
+ ```
315
+
316
+ ## Correct
317
+
318
+ ```sql
319
+ UPDATE users SET active = false WHERE last_seen_at < NOW() - INTERVAL '90 days';
320
+ DELETE FROM sessions WHERE expires_at < NOW();
321
+ ```
322
+
323
+ ## Escalate to
324
+
325
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
326
+
327
+
328
+ ## HIGH — hot-path or correctness risk at realistic n
329
+
330
+ ### `cpp-raw-new` — cpp
331
+
332
+ # Raw `new` outside of smart-pointer ctor — manual delete, exception-unsafe
333
+
334
+ Raw `new` requires a matching `delete` on every exit path. If an exception fires between `new` and `delete`, the memory leaks. Smart pointers (`unique_ptr`, `shared_ptr`) destruct automatically.
335
+
336
+ ## Fix
337
+
338
+ Use `std::make_unique<T>(...)` or `std::make_shared<T>(...)`. Reserve raw `new` for placement-new or interop with C APIs.
339
+
340
+ ## Incorrect
341
+
342
+ ```cpp
343
+ Widget* w = new Widget(42);
344
+ configure(w); // if this throws, w leaks
345
+ delete w;
346
+ ```
347
+
348
+ ## Correct
349
+
350
+ ```cpp
351
+ auto w = std::make_unique<Widget>(42);
352
+ configure(w.get()); // destructs on every exit path, including throw
353
+ ```
354
+
355
+ ## Escalate to
356
+
357
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
358
+
359
+ ### `cpp-string-concat-in-loop` — cpp
360
+
361
+ # std::string += inside loop — O(n^2) without reserve()
362
+
363
+ `std::string::operator+=` past capacity reallocates. Without a `reserve`, repeated concatenation is O(n²). `std::ostringstream` or one `reserve` fixes it.
364
+
365
+ ## Fix
366
+
367
+ Call `s.reserve(total)` once if size known, or accumulate into a `std::ostringstream` and call `.str()` at the end.
368
+
369
+ ## Incorrect
370
+
371
+ ```cpp
372
+ std::string s;
373
+ for (const auto& part : parts) {
374
+ s += part;
375
+ }
376
+ ```
377
+
378
+ ## Correct
379
+
380
+ ```cpp
381
+ std::ostringstream os;
382
+ for (const auto& part : parts) {
383
+ os << part;
384
+ }
385
+ auto s = os.str();
386
+
387
+ // Or, if size known:
388
+ std::string s;
389
+ s.reserve(total_size);
390
+ for (const auto& part : parts) s += part;
391
+ ```
392
+
393
+ ## Escalate to
394
+
395
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
396
+
397
+ ### `cs-disposable-no-using` — csharp
398
+
399
+ # IDisposable allocated without using — leak on exception
400
+
401
+ `IDisposable` resources allocated without `using` leak if an exception fires before `Dispose()`. `using var` ensures cleanup even on throw.
402
+
403
+ ## Fix
404
+
405
+ Wrap in `using var x = new ...;` or `using (var x = ...) { }`.
406
+
407
+ ## Incorrect
408
+
409
+ ```csharp
410
+ var stream = new FileStream(path, FileMode.Open);
411
+ var data = ReadAll(stream); // if this throws, stream is never disposed
412
+ stream.Dispose();
413
+ ```
414
+
415
+ ## Correct
416
+
417
+ ```csharp
418
+ using var stream = new FileStream(path, FileMode.Open);
419
+ var data = ReadAll(stream); // disposed on every exit path
420
+ ```
421
+
422
+ ### `cs-list-contains-in-loop` — csharp
423
+
424
+ # List.Contains inside LINQ/loop — O(n*m); use HashSet<T>
425
+
426
+ `List<T>.Contains` is O(n). Inside LINQ over m items it is O(n·m). `HashSet<T>` is O(1).
427
+
428
+ ## Fix
429
+
430
+ Convert to HashSet once: `var s = new HashSet<T>(list); s.Contains(x);`
431
+
432
+ ## Incorrect
433
+
434
+ ```csharp
435
+ // O(n·m)
436
+ var active = users.Where(u => !banned.Contains(u.Id)).ToList();
437
+ ```
438
+
439
+ ## Correct
440
+
441
+ ```csharp
442
+ // O(n+m)
443
+ var bannedSet = new HashSet<int>(banned);
444
+ var active = users.Where(u => !bannedSet.Contains(u.Id)).ToList();
445
+ ```
446
+
447
+ ## Escalate to
448
+
449
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
450
+
451
+ ### `cs-string-concat-in-loop` — csharp
452
+
453
+ # string += inside loop — O(n^2) on immutable string
454
+
455
+ C# `string` is immutable. `s += x` allocates a new string each iteration — O(n²). `StringBuilder` reuses one buffer; or use `string.Concat` / `string.Join` for known parts.
456
+
457
+ ## Fix
458
+
459
+ Use StringBuilder: `var sb = new StringBuilder(); foreach (...) sb.Append(x); sb.ToString();`
460
+
461
+ ## Incorrect
462
+
463
+ ```csharp
464
+ // O(n²)
465
+ string s = "";
466
+ foreach (var line in lines) {
467
+ s += line + "\n";
468
+ }
469
+ ```
470
+
471
+ ## Correct
472
+
473
+ ```csharp
474
+ // O(n)
475
+ var sb = new StringBuilder(lines.Count * 80);
476
+ foreach (var line in lines) {
477
+ sb.Append(line).Append('\n');
478
+ }
479
+ var s = sb.ToString();
480
+ ```
481
+
482
+ ## Escalate to
483
+
484
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
485
+
486
+ ### `go-defer-in-loop` — go
487
+
488
+ # defer inside loop — defers accumulate until function returns
489
+
490
+ `defer` fires when the enclosing *function* returns, not when the loop body ends. Deferring inside a loop over N files holds N open handles until the function exits — easy way to exhaust file descriptors.
491
+
492
+ ## Fix
493
+
494
+ Wrap the body in a function so each iteration's defer fires at iteration end, or close resources explicitly.
495
+
496
+ ## Incorrect
497
+
498
+ ```go
499
+ for _, f := range files {
500
+ fp, _ := os.Open(f)
501
+ defer fp.Close() // accumulates; nothing closes until the outer function returns
502
+ process(fp)
503
+ }
504
+ ```
505
+
506
+ ## Correct
507
+
508
+ ```go
509
+ for _, f := range files {
510
+ func() {
511
+ fp, _ := os.Open(f)
512
+ defer fp.Close() // closes at end of this iteration
513
+ process(fp)
514
+ }()
515
+ }
516
+ ```
517
+
518
+ ### `go-err-not-checked` — go
519
+
520
+ # Error return value discarded — silent failures
521
+
522
+ Discarding the error return with `_` is silent failure. The function looks like it succeeded; downstream code sees zero values and behaves unpredictably.
523
+
524
+ ## Fix
525
+
526
+ Handle the error or comment why it is safe to ignore (`_ = err // <reason>`).
527
+
528
+ ## Incorrect
529
+
530
+ ```go
531
+ data, _ := os.ReadFile(path)
532
+ return parse(data) // parse on empty buffer if file missing
533
+ ```
534
+
535
+ ## Correct
536
+
537
+ ```go
538
+ data, err := os.ReadFile(path)
539
+ if err != nil {
540
+ return nil, fmt.Errorf("read %s: %w", path, err)
541
+ }
542
+ return parse(data), nil
543
+ ```
544
+
545
+ ## Escalate to
546
+
547
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
548
+
549
+ ### `go-string-concat-in-loop` — go
550
+
551
+ # string += inside loop — O(n^2); use strings.Builder
552
+
553
+ Go strings are immutable. `s += x` allocates each iteration — O(n²). `strings.Builder` reuses one backing array.
554
+
555
+ ## Fix
556
+
557
+ Use strings.Builder: `var sb strings.Builder; for ... { sb.WriteString(x) }; sb.String()`.
558
+
559
+ ## Incorrect
560
+
561
+ ```go
562
+ // O(n²)
563
+ var s string
564
+ for _, line := range lines {
565
+ s += line + "\n"
566
+ }
567
+ ```
568
+
569
+ ## Correct
570
+
571
+ ```go
572
+ // O(n)
573
+ var sb strings.Builder
574
+ sb.Grow(len(lines) * 80)
575
+ for _, line := range lines {
576
+ sb.WriteString(line)
577
+ sb.WriteByte('\n')
578
+ }
579
+ s := sb.String()
580
+ ```
581
+
582
+ ## Escalate to
583
+
584
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
585
+
586
+ ### `java-bare-catch-exception` — java
587
+
588
+ # catch (Exception) with empty body or log-only — swallows root cause
589
+
590
+ `catch (Exception e)` with an empty body or just `printStackTrace()` swallows the root cause. The bug lives on, the stack trace evaporates, the production incident has no breadcrumbs.
591
+
592
+ ## Fix
593
+
594
+ Catch the narrowest exception, rethrow as a domain exception, or handle with a documented fallback. Never swallow silently.
595
+
596
+ ## Incorrect
597
+
598
+ ```java
599
+ try {
600
+ riskyCall();
601
+ } catch (Exception e) {
602
+ e.printStackTrace(); // swallowed; caller thinks everything is fine
603
+ }
604
+ ```
605
+
606
+ ## Correct
607
+
608
+ ```java
609
+ try {
610
+ riskyCall();
611
+ } catch (IOException e) {
612
+ // narrow exception, rethrow as domain error with cause preserved
613
+ throw new ReadFailedException("riskyCall failed for " + ctx, e);
614
+ }
615
+ ```
616
+
617
+ ## Escalate to
618
+
619
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
620
+
621
+ ### `java-list-contains-in-loop` — java
622
+
623
+ # List.contains inside iterator — O(n*m); use HashSet
624
+
625
+ `List.contains` is O(n). Used inside a stream/iterator over m items it is O(n·m). A `HashSet` lookup is O(1).
626
+
627
+ ## Fix
628
+
629
+ Build a HashSet outside: `Set<T> s = new HashSet<>(list); s.contains(x);`
630
+
631
+ ## Incorrect
632
+
633
+ ```java
634
+ // O(n·m)
635
+ var active = users.stream()
636
+ .filter(u -> !banned.contains(u.id))
637
+ .toList();
638
+ ```
639
+
640
+ ## Correct
641
+
642
+ ```java
643
+ // O(n+m)
644
+ var bannedSet = new HashSet<>(banned);
645
+ var active = users.stream()
646
+ .filter(u -> !bannedSet.contains(u.id))
647
+ .toList();
648
+ ```
649
+
650
+ ## Escalate to
651
+
652
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
653
+
654
+ ### `java-string-concat-in-loop` — java
655
+
656
+ # String += inside loop — O(n^2) on immutable String
657
+
658
+ Java `String` is immutable. `s += x` allocates a fresh `String` (and an underlying `char[]`) each iteration — O(n²) total work. `StringBuilder` reuses one buffer.
659
+
660
+ ## Fix
661
+
662
+ Use StringBuilder: `var sb = new StringBuilder(); for (...) sb.append(x); sb.toString();`
663
+
664
+ ## Incorrect
665
+
666
+ ```java
667
+ // O(n²)
668
+ String s = "";
669
+ for (var line : lines) {
670
+ s += line + "\n";
671
+ }
672
+ ```
673
+
674
+ ## Correct
675
+
676
+ ```java
677
+ // O(n)
678
+ var sb = new StringBuilder(lines.size() * 80);
679
+ for (var line : lines) {
680
+ sb.append(line).append('\n');
681
+ }
682
+ String s = sb.toString();
683
+ ```
684
+
685
+ ## Escalate to
686
+
687
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
688
+
689
+ ### `js-anonymous-handler-jsx` — javascript
690
+
691
+ # Anonymous arrow handler in JSX — breaks memoized-child equality
692
+
693
+ An anonymous arrow handler is a new function every render. For an ordinary child, this is fine. For a `React.memo` child, it breaks equality and forces a re-render every time.
694
+
695
+ ## Fix
696
+
697
+ Wrap with useCallback if the child is memoized. Otherwise ignore.
698
+
699
+ ## Incorrect
700
+
701
+ ```tsx
702
+ // Child is React.memo — this defeats it
703
+ <MemoButton onClick={() => save(id)} />
704
+ ```
705
+
706
+ ## Correct
707
+
708
+ ```tsx
709
+ const onClick = useCallback(() => save(id), [id]);
710
+ <MemoButton onClick={onClick} />
711
+ ```
712
+
713
+ ### `js-deep-clone-via-json` — javascript
714
+
715
+ # JSON.parse(JSON.stringify(x)) — slow clone; loses Dates/Maps/undefined
716
+
717
+ `JSON.parse(JSON.stringify(x))` is slow, allocates twice, and silently loses `Date`, `Map`, `Set`, `undefined`, `BigInt`, `RegExp`, and any non-enumerable property. It is also unsafe on cyclic structures.
718
+
719
+ ## Fix
720
+
721
+ Use structuredClone(x), or copy only the fields you actually need.
722
+
723
+ ## Incorrect
724
+
725
+ ```ts
726
+ const copy = JSON.parse(JSON.stringify(state));
727
+ // state.createdAt was a Date — now a string
728
+ ```
729
+
730
+ ## Correct
731
+
732
+ ```ts
733
+ const copy = structuredClone(state); // preserves Date, Map, Set, cycles
734
+
735
+ // Or, when you only need a few fields:
736
+ const copy = { id: state.id, name: state.name };
737
+ ```
738
+
739
+ ### `js-helper-call-in-iterator` — javascript
740
+
741
+ # get*/find*/fetch* helper called inside iterator — likely N round-trips
742
+
743
+ A `get*`/`find*`/`fetch*` helper inside an iterator usually means N independent lookups. If the helper hits a DB or network, this is N+1. If it scans an array, it is O(n × m).
744
+
745
+ ## Fix
746
+
747
+ Hoist the helper out of the loop and pass a precomputed lookup (Map/Set) into the iterator, or batch with a single bulk query.
748
+
749
+ ## Incorrect
750
+
751
+ ```ts
752
+ // N round trips
753
+ const enriched = orders.map((o) => ({
754
+ ...o,
755
+ user: getUserById(o.userId),
756
+ }));
757
+ ```
758
+
759
+ ## Correct
760
+
761
+ ```ts
762
+ // 1 round trip
763
+ const userIds = [...new Set(orders.map((o) => o.userId))];
764
+ const users = await db.users.findMany({ where: { id: { in: userIds } } });
765
+ const userById = new Map(users.map((u) => [u.id, u]));
766
+ const enriched = orders.map((o) => ({ ...o, user: userById.get(o.userId) }));
767
+ ```
768
+
769
+ ## Escalate to
770
+
771
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
772
+
773
+ ### `js-inline-object-jsx-prop` — javascript
774
+
775
+ # Inline object literal as JSX prop — new reference every render
776
+
777
+ An inline object literal in JSX creates a new reference every render. If the child is `React.memo`, this defeats the memoization. If it is a dependency of a hook in the child, it re-fires that hook every render.
778
+
779
+ ## Fix
780
+
781
+ Hoist outside render, or wrap in useMemo if the child is React.memo.
782
+
783
+ ## Incorrect
784
+
785
+ ```tsx
786
+ <Chart options={{ animated: true, color: 'red' }} />
787
+ ```
788
+
789
+ ## Correct
790
+
791
+ ```tsx
792
+ // Hoist if static
793
+ const CHART_OPTIONS = { animated: true, color: 'red' };
794
+ <Chart options={CHART_OPTIONS} />
795
+
796
+ // Memoize if derived
797
+ const options = useMemo(() => ({ animated, color }), [animated, color]);
798
+ <Chart options={options} />
799
+ ```
800
+
801
+ ### `js-spread-in-reduce` — javascript
802
+
803
+ # Object spread inside reduce — O(n^2)
804
+
805
+ Object-spread in a reducer copies the accumulator on every iteration — O(n²) work and O(n²) allocation. The result is the same as mutating once.
806
+
807
+ ## Fix
808
+
809
+ Mutate the accumulator (`acc[k] = v; return acc`) or use Object.fromEntries(arr.map(...)).
810
+
811
+ ## Incorrect
812
+
813
+ ```ts
814
+ // O(n²)
815
+ const byId = items.reduce((acc, x) => ({ ...acc, [x.id]: x }), {});
816
+ ```
817
+
818
+ ## Correct
819
+
820
+ ```ts
821
+ // O(n)
822
+ const byId = Object.fromEntries(items.map((x) => [x.id, x]));
823
+
824
+ // Or mutate the accumulator
825
+ const byId = items.reduce((acc, x) => { acc[x.id] = x; return acc; }, {});
826
+ ```
827
+
828
+ ## Escalate to
829
+
830
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
831
+
832
+ ### `js-unique-via-indexof` — javascript
833
+
834
+ # Unique-by-indexOf — O(n^2) dedupe; use `Array.from(new Set(arr))`
835
+
836
+ `.filter((x, i, a) => a.indexOf(x) === i)` is the textbook O(n²) dedupe. A `Set` does it in O(n).
837
+
838
+ ## Fix
839
+
840
+ Use `Array.from(new Set(arr))`, or build a Set inline. O(n) instead of O(n^2).
841
+
842
+ ## Incorrect
843
+
844
+ ```ts
845
+ // O(n²)
846
+ const unique = arr.filter((x, i, a) => a.indexOf(x) === i);
847
+ ```
848
+
849
+ ## Correct
850
+
851
+ ```ts
852
+ // O(n)
853
+ const unique = Array.from(new Set(arr));
854
+ ```
855
+
856
+ ## Escalate to
857
+
858
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
859
+
860
+ ### `js-useeffect-missing-deps` — javascript
861
+
862
+ # useEffect without a deps array — runs after every render
863
+
864
+ A `useEffect` with no dependency array runs after every render. If the effect updates state, you can get a render loop or wasted work each frame.
865
+
866
+ ## Fix
867
+
868
+ Add a deps array — `[]` for mount-only, `[a, b]` to track changes.
869
+
870
+ ## Incorrect
871
+
872
+ ```tsx
873
+ useEffect(() => {
874
+ setUser(fetchUser(id));
875
+ }); // no deps — runs every render
876
+ ```
877
+
878
+ ## Correct
879
+
880
+ ```tsx
881
+ useEffect(() => {
882
+ setUser(fetchUser(id));
883
+ }, [id]); // runs when id changes
884
+ ```
885
+
886
+ ### `php-count-in-for-condition` — php
887
+
888
+ # count() in for-condition — recomputed every iteration
889
+
890
+ PHP recomputes the loop condition every iteration. `count($a)` on a 100k-element array, called 100k times, is 10B element traversals. Hoist it.
891
+
892
+ ## Fix
893
+
894
+ Hoist: `for ($i = 0, $n = count($a); $i < $n; $i++)`. Or use `foreach`.
895
+
896
+ ## Incorrect
897
+
898
+ ```php
899
+ <?php
900
+ for ($i = 0; $i < count($a); $i++) {
901
+ echo $a[$i];
902
+ }
903
+ ```
904
+
905
+ ## Correct
906
+
907
+ ```php
908
+ <?php
909
+ // Hoist the length
910
+ for ($i = 0, $n = count($a); $i < $n; $i++) {
911
+ echo $a[$i];
912
+ }
913
+
914
+ // Or, idiomatic
915
+ foreach ($a as $x) {
916
+ echo $x;
917
+ }
918
+ ```
919
+
920
+ ## Escalate to
921
+
922
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
923
+
924
+ ### `php-in-array-in-loop` — php
925
+
926
+ # in_array inside loop — O(n*m); use array_flip + isset
927
+
928
+ `in_array` is a linear scan. Used inside a loop over m items it is O(n·m). `array_flip + isset` is O(1) per check.
929
+
930
+ ## Fix
931
+
932
+ `$set = array_flip($haystack); ... isset($set[$x])` is O(1) per check.
933
+
934
+ ## Incorrect
935
+
936
+ ```php
937
+ <?php
938
+ foreach ($users as $u) {
939
+ if (in_array($u->id, $banned)) continue;
940
+ ship($u);
941
+ }
942
+ ```
943
+
944
+ ## Correct
945
+
946
+ ```php
947
+ <?php
948
+ $bannedSet = array_flip($banned); // O(n) once
949
+ foreach ($users as $u) {
950
+ if (isset($bannedSet[$u->id])) continue; // O(1)
951
+ ship($u);
952
+ }
953
+ ```
954
+
955
+ ## Escalate to
956
+
957
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
958
+
959
+ ### `py-django-loop-without-eager` — python
960
+
961
+ # Django ORM iteration — verify select_related/prefetch_related to avoid N+1
962
+
963
+ Iterating a Django QuerySet that touches a related model triggers one extra query per row — classic N+1. `select_related` (foreign key, one query with JOIN) or `prefetch_related` (reverse / many-to-many, two queries) fixes it.
964
+
965
+ ## Fix
966
+
967
+ Add .select_related('fk') for FK/OneToOne, .prefetch_related('rel') for reverse FK / M2M.
968
+
969
+ ## Incorrect
970
+
971
+ ```python
972
+ # N+1 queries
973
+ for order in Order.objects.all():
974
+ print(order.user.email) # extra query per order
975
+ ```
976
+
977
+ ## Correct
978
+
979
+ ```python
980
+ # 1 query with JOIN
981
+ for order in Order.objects.select_related("user"):
982
+ print(order.user.email)
983
+
984
+ # For reverse FK / M2M:
985
+ for user in User.objects.prefetch_related("orders"):
986
+ for order in user.orders.all():
987
+ ...
988
+ ```
989
+
990
+ ## Escalate to
991
+
992
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
993
+
994
+ ### `py-string-concat-in-loop` — python
995
+
996
+ # String += inside loop — O(n^2)
997
+
998
+ Python strings are immutable. `s += x` in a loop allocates and copies the whole `s` each iteration — O(n²) total work. A list join is O(n).
999
+
1000
+ ## Fix
1001
+
1002
+ Append to a list, then ''.join(list). Or use io.StringIO.
1003
+
1004
+ ## Incorrect
1005
+
1006
+ ```python
1007
+ # O(n²)
1008
+ s = ""
1009
+ for line in lines:
1010
+ s += line + "\n"
1011
+ ```
1012
+
1013
+ ## Correct
1014
+
1015
+ ```python
1016
+ # O(n)
1017
+ s = "\n".join(lines) + "\n"
1018
+
1019
+ # Or, for incremental building:
1020
+ parts = []
1021
+ for line in lines:
1022
+ parts.append(line)
1023
+ s = "\n".join(parts)
1024
+ ```
1025
+
1026
+ ## Escalate to
1027
+
1028
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1029
+
1030
+ ### `rb-bare-rescue` — ruby
1031
+
1032
+ # Bare rescue — catches StandardError, hides bugs
1033
+
1034
+ A bare `rescue` (without a class) catches `StandardError` — including `NoMethodError`, `ArgumentError`, and other bugs you almost always want to surface. Catch the specific class.
1035
+
1036
+ ## Fix
1037
+
1038
+ Catch a specific class: `rescue Net::ReadTimeout` etc. Bare rescue swallows too much.
1039
+
1040
+ ## Incorrect
1041
+
1042
+ ```ruby
1043
+ begin
1044
+ fetch_remote
1045
+ rescue
1046
+ retry # also catches typos and bugs in fetch_remote
1047
+ end
1048
+ ```
1049
+
1050
+ ## Correct
1051
+
1052
+ ```ruby
1053
+ begin
1054
+ fetch_remote
1055
+ rescue Net::ReadTimeout, Net::OpenTimeout => e
1056
+ retry
1057
+ end
1058
+ ```
1059
+
1060
+ ## Escalate to
1061
+
1062
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
1063
+
1064
+ ### `rb-include-in-iterator` — ruby
1065
+
1066
+ # Array#include? inside iterator — O(n*m); use Set
1067
+
1068
+ `Array#include?` is O(n). Used inside an iterator over m items it is O(n·m). `Set#include?` is O(1).
1069
+
1070
+ ## Fix
1071
+
1072
+ `require 'set'; allowed = Set.new(list); ... allowed.include?(x)`.
1073
+
1074
+ ## Incorrect
1075
+
1076
+ ```ruby
1077
+ # O(n·m)
1078
+ users.select { |u| banned.include?(u.id) }
1079
+ ```
1080
+
1081
+ ## Correct
1082
+
1083
+ ```ruby
1084
+ # O(n+m)
1085
+ require 'set'
1086
+ banned_set = Set.new(banned)
1087
+ users.select { |u| banned_set.include?(u.id) }
1088
+ ```
1089
+
1090
+ ## Escalate to
1091
+
1092
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1093
+
1094
+ ### `rs-unwrap-in-prod` — rust
1095
+
1096
+ # .unwrap() / .expect() panics on None/Err — surface the error
1097
+
1098
+ `.unwrap()` and `.expect()` panic on `None`/`Err`. In production code they crash the process and lose the structured error. Rust gives you `?`, `match`, and `ok_or` to surface the error to the caller.
1099
+
1100
+ ## Fix
1101
+
1102
+ Use `?`, `match`, `unwrap_or`, `unwrap_or_else`, or `ok_or(...)?`. Reserve unwrap for tests and provably infallible paths.
1103
+
1104
+ ## Incorrect
1105
+
1106
+ ```rust
1107
+ let value = map.get(&key).unwrap(); // panics if key missing
1108
+ ```
1109
+
1110
+ ## Correct
1111
+
1112
+ ```rust
1113
+ let value = map.get(&key).ok_or(Error::MissingKey)?;
1114
+
1115
+ // Or, when None has a meaningful default:
1116
+ let value = map.get(&key).copied().unwrap_or_default();
1117
+ ```
1118
+
1119
+ ## Escalate to
1120
+
1121
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
1122
+
1123
+ ### `sh-for-ls` — shell
1124
+
1125
+ # for f in $(ls ...) — breaks on filenames with spaces / newlines
1126
+
1127
+ `for f in $(ls ...)` breaks on filenames with spaces, tabs, newlines, or globs. The output of `ls` is meant for humans, not programs. Use a glob or `find -print0 | xargs -0`.
1128
+
1129
+ ## Fix
1130
+
1131
+ Use glob `for f in *.txt` or `find ... -print0 | xargs -0`. Never parse `ls`.
1132
+
1133
+ ## Incorrect
1134
+
1135
+ ```shell
1136
+ for f in $(ls *.txt); do
1137
+ process "$f" # breaks on "my file.txt"
1138
+ done
1139
+ ```
1140
+
1141
+ ## Correct
1142
+
1143
+ ```shell
1144
+ # Glob directly
1145
+ for f in *.txt; do
1146
+ process "$f"
1147
+ done
1148
+
1149
+ # Or, when find is necessary
1150
+ find . -name '*.txt' -print0 | xargs -0 -n1 process
1151
+ ```
1152
+
1153
+ ## Escalate to
1154
+
1155
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
1156
+
1157
+ ### `sh-set-e-no-pipefail` — shell
1158
+
1159
+ # set -e without set -o pipefail — failures inside pipes are masked
1160
+
1161
+ `set -e` exits on a failed command, but a failure in the middle of a pipe is masked — only the *last* command's exit status counts. `set -o pipefail` fixes it. `set -u` catches unset variables.
1162
+
1163
+ ## Fix
1164
+
1165
+ Use `set -euo pipefail` so the script also fails when an earlier stage in a pipe fails.
1166
+
1167
+ ## Incorrect
1168
+
1169
+ ```shell
1170
+ #!/bin/bash
1171
+ set -e
1172
+ some_failing_step | grep needle # if some_failing_step fails, script keeps going
1173
+ ```
1174
+
1175
+ ## Correct
1176
+
1177
+ ```shell
1178
+ #!/bin/bash
1179
+ set -euo pipefail
1180
+ some_failing_step | grep needle # any failure in the pipe aborts the script
1181
+ ```
1182
+
1183
+ ## Escalate to
1184
+
1185
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
1186
+
1187
+ ### `sh-unquoted-var` — shell
1188
+
1189
+ # Unquoted $var — word splitting and glob expansion
1190
+
1191
+ An unquoted `$var` is subject to word splitting (on `$IFS`) and glob expansion. A path with a space, a tab, or a `*` will silently do the wrong thing.
1192
+
1193
+ ## Fix
1194
+
1195
+ Quote: `"$var"`. Use `"${arr[@]}"` for arrays. Required even for paths you trust.
1196
+
1197
+ ## Incorrect
1198
+
1199
+ ```shell
1200
+ if [ -d $dir ]; then echo yes; fi
1201
+ # If $dir = "/tmp/with space", expands to: [ -d /tmp/with space ] — syntax error
1202
+ ```
1203
+
1204
+ ## Correct
1205
+
1206
+ ```shell
1207
+ if [ -d "$dir" ]; then echo yes; fi
1208
+ # Always quote. For arrays: "${arr[@]}".
1209
+ ```
1210
+
1211
+ ## Escalate to
1212
+
1213
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
1214
+
1215
+ ### `sql-leading-wildcard-like` — sql
1216
+
1217
+ # LIKE with leading wildcard — cannot use a B-tree index
1218
+
1219
+ A B-tree index sorts by prefix. `LIKE '%foo'` cannot use it — the query scans the table. Use a trigram index (Postgres `pg_trgm`), reverse the column for suffix search, or a full-text search index.
1220
+
1221
+ ## Fix
1222
+
1223
+ Use a trigram index (pg_trgm), full-text search, or a reversed-column index.
1224
+
1225
+ ## Incorrect
1226
+
1227
+ ```sql
1228
+ SELECT * FROM products WHERE name LIKE '%phone';
1229
+ ```
1230
+
1231
+ ## Correct
1232
+
1233
+ ```sql
1234
+ -- Postgres: GIN index on trigrams
1235
+ CREATE INDEX products_name_trgm ON products USING gin (name gin_trgm_ops);
1236
+ SELECT * FROM products WHERE name ILIKE '%phone%';
1237
+
1238
+ -- Or full-text:
1239
+ SELECT * FROM products WHERE to_tsvector(name) @@ to_tsquery('phone');
1240
+ ```
1241
+
1242
+ ### `sql-not-in-subquery` — sql
1243
+
1244
+ # NOT IN (subquery) — null-unsafe; usually slower than NOT EXISTS
1245
+
1246
+ `NOT IN (subquery)` is null-unsafe: if any row in the subquery is NULL, the whole predicate is NULL (not TRUE), and the outer row is dropped. `NOT EXISTS` is null-safe and usually has a better plan.
1247
+
1248
+ ## Fix
1249
+
1250
+ Rewrite as `WHERE NOT EXISTS (SELECT 1 FROM ... WHERE ...)`.
1251
+
1252
+ ## Incorrect
1253
+
1254
+ ```sql
1255
+ SELECT * FROM orders
1256
+ WHERE user_id NOT IN (SELECT id FROM banned_users);
1257
+ -- One NULL in banned_users.id → returns zero rows
1258
+ ```
1259
+
1260
+ ## Correct
1261
+
1262
+ ```sql
1263
+ SELECT o.* FROM orders o
1264
+ WHERE NOT EXISTS (
1265
+ SELECT 1 FROM banned_users b WHERE b.id = o.user_id
1266
+ );
1267
+ ```
1268
+
1269
+ ## Escalate to
1270
+
1271
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
1272
+
1273
+ ### `sql-select-star` — sql
1274
+
1275
+ # SELECT * — fetches unused columns; blocks index-only scans
1276
+
1277
+ `SELECT *` fetches every column, defeating index-only scans, inflating wire traffic, and breaking downstream code when a column is added/renamed. Project only what you need.
1278
+
1279
+ ## Fix
1280
+
1281
+ Name the columns. Smaller payload and more covering-index opportunities.
1282
+
1283
+ ## Incorrect
1284
+
1285
+ ```sql
1286
+ SELECT * FROM users WHERE id = $1;
1287
+ ```
1288
+
1289
+ ## Correct
1290
+
1291
+ ```sql
1292
+ SELECT id, email, created_at FROM users WHERE id = $1;
1293
+ ```
1294
+
1295
+
1296
+ ## MEDIUM — suboptimal; flag when n is large or hot path
1297
+
1298
+ ### `cpp-map-double-lookup` — cpp
1299
+
1300
+ # map.count(k) then map[k] — two lookups; use find()
1301
+
1302
+ `m.count(k)` then `m[k]` does two hash lookups (and for `std::map`, two binary searches). `find` returns an iterator that gives both presence and value in one lookup.
1303
+
1304
+ ## Fix
1305
+
1306
+ Use `auto it = m.find(k); if (it != m.end()) use(it->second);` — one lookup.
1307
+
1308
+ ## Incorrect
1309
+
1310
+ ```cpp
1311
+ if (m.count(k)) {
1312
+ use(m[k]); // second lookup
1313
+ }
1314
+ ```
1315
+
1316
+ ## Correct
1317
+
1318
+ ```cpp
1319
+ auto it = m.find(k);
1320
+ if (it != m.end()) {
1321
+ use(it->second);
1322
+ }
1323
+ ```
1324
+
1325
+ ## Escalate to
1326
+
1327
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1328
+
1329
+ ### `cpp-range-loop-copy` — cpp
1330
+
1331
+ # Range-for `auto x` copies each element — use `const auto&` for non-trivial types
1332
+
1333
+ `for (auto x : container)` copies each element into `x`. For non-trivial types (`std::string`, `std::vector<…>`, custom types) this is expensive. `const auto&` borrows, `auto&` mutates in place.
1334
+
1335
+ ## Fix
1336
+
1337
+ Prefer `for (const auto& x : container)` unless you intentionally need a copy.
1338
+
1339
+ ## Incorrect
1340
+
1341
+ ```cpp
1342
+ for (auto s : large_strings) {
1343
+ process(s); // s is a fresh copy each iteration
1344
+ }
1345
+ ```
1346
+
1347
+ ## Correct
1348
+
1349
+ ```cpp
1350
+ for (const auto& s : large_strings) {
1351
+ process(s); // reference, no copy
1352
+ }
1353
+ ```
1354
+
1355
+ ### `cpp-vector-push-no-reserve` — cpp
1356
+
1357
+ # vector push_back in loop without reserve() — log-amortized reallocation
1358
+
1359
+ `std::vector::push_back` past the current capacity doubles the backing array and copies/moves every element. Calling `reserve(n)` first does one allocation.
1360
+
1361
+ ## Fix
1362
+
1363
+ Call `v.reserve(n)` before the loop when n is known.
1364
+
1365
+ ## Incorrect
1366
+
1367
+ ```cpp
1368
+ std::vector<int> v;
1369
+ for (int i = 0; i < n; ++i) {
1370
+ v.push_back(compute(i)); // reallocates log(n) times
1371
+ }
1372
+ ```
1373
+
1374
+ ## Correct
1375
+
1376
+ ```cpp
1377
+ std::vector<int> v;
1378
+ v.reserve(n);
1379
+ for (int i = 0; i < n; ++i) {
1380
+ v.push_back(compute(i));
1381
+ }
1382
+ ```
1383
+
1384
+ ## Escalate to
1385
+
1386
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1387
+
1388
+ ### `go-slice-append-no-cap` — go
1389
+
1390
+ # append in loop without preallocated capacity — repeated reallocation
1391
+
1392
+ A slice grown by repeated `append` reallocates and copies its backing array each time it crosses a capacity boundary. Preallocating with `make([]T, 0, n)` does one allocation total.
1393
+
1394
+ ## Fix
1395
+
1396
+ Preallocate: `out := make([]T, 0, len(in))` then `append`.
1397
+
1398
+ ## Incorrect
1399
+
1400
+ ```go
1401
+ var out []int
1402
+ for _, x := range in {
1403
+ out = append(out, x*2) // grows; reallocates log(n) times
1404
+ }
1405
+ ```
1406
+
1407
+ ## Correct
1408
+
1409
+ ```go
1410
+ out := make([]int, 0, len(in))
1411
+ for _, x := range in {
1412
+ out = append(out, x*2)
1413
+ }
1414
+ ```
1415
+
1416
+ ## Escalate to
1417
+
1418
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1419
+
1420
+ ### `js-array-key-index` — javascript
1421
+
1422
+ # key={index} on a list — breaks identity for reorderable items
1423
+
1424
+ `key={index}` is fine for a static list. For a list that reorders, inserts, or deletes in the middle, it forces React to mismatch component state with the wrong row — losing input focus, animation, and local state.
1425
+
1426
+ ## Fix
1427
+
1428
+ Use a stable id when the list can reorder, insert, or delete in the middle.
1429
+
1430
+ ## Incorrect
1431
+
1432
+ ```tsx
1433
+ {items.map((item, i) => <Row key={i} item={item} />)}
1434
+ ```
1435
+
1436
+ ## Correct
1437
+
1438
+ ```tsx
1439
+ {items.map((item) => <Row key={item.id} item={item} />)}
1440
+ ```
1441
+
1442
+ ### `js-includes-in-iterator` — javascript
1443
+
1444
+ # Array.includes inside .map/.filter/.forEach — O(n*m); use a Set
1445
+
1446
+ `.includes` on an array is O(n). Calling it inside `.map`/`.filter`/`.forEach` over m items is O(n × m). A `Set` lookup is O(1).
1447
+
1448
+ ## Fix
1449
+
1450
+ Build a Set once outside the iterator, then `set.has(x)` inside.
1451
+
1452
+ ## Incorrect
1453
+
1454
+ ```ts
1455
+ // O(n × m)
1456
+ const allowed = ['admin', 'editor', 'owner'];
1457
+ const result = users.filter((u) => allowed.includes(u.role));
1458
+ ```
1459
+
1460
+ ## Correct
1461
+
1462
+ ```ts
1463
+ // O(n + m)
1464
+ const allowed = new Set(['admin', 'editor', 'owner']);
1465
+ const result = users.filter((u) => allowed.has(u.role));
1466
+ ```
1467
+
1468
+ ## Escalate to
1469
+
1470
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1471
+
1472
+ ### `js-nested-for-loops` — javascript
1473
+
1474
+ # Nested for-loops — O(n*m); consider hashing one side
1475
+
1476
+ Two `for` loops that check membership between arrays are O(n × m). Hashing one side into a `Set` makes it O(n + m).
1477
+
1478
+ ## Fix
1479
+
1480
+ If looking up between the two arrays, put one into a Set/Map first → O(n+m).
1481
+
1482
+ ## Incorrect
1483
+
1484
+ ```ts
1485
+ // O(n × m)
1486
+ const matches = [];
1487
+ for (const a of left) {
1488
+ for (const b of right) {
1489
+ if (a.id === b.id) matches.push([a, b]);
1490
+ }
1491
+ }
1492
+ ```
1493
+
1494
+ ## Correct
1495
+
1496
+ ```ts
1497
+ // O(n + m)
1498
+ const rightById = new Map(right.map((b) => [b.id, b]));
1499
+ const matches = [];
1500
+ for (const a of left) {
1501
+ const b = rightById.get(a.id);
1502
+ if (b) matches.push([a, b]);
1503
+ }
1504
+ ```
1505
+
1506
+ ## Escalate to
1507
+
1508
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1509
+
1510
+ ### `php-loose-equality` — php
1511
+
1512
+ # == / != — loose equality has surprising coercions
1513
+
1514
+ PHP `==` does type coercion in surprising ways (`"0" == false` is `true`, `"abc" == 0` was `true` before PHP 8). `===` compares value AND type — no surprises.
1515
+
1516
+ ## Fix
1517
+
1518
+ Use `===` / `!==` unless type-juggling is explicitly intended (with a comment).
1519
+
1520
+ ## Incorrect
1521
+
1522
+ ```php
1523
+ <?php
1524
+ if ($status == 0) { /* matches "", "0", false, null, 0, "abc" pre-PHP 8 */ }
1525
+ ```
1526
+
1527
+ ## Correct
1528
+
1529
+ ```php
1530
+ <?php
1531
+ if ($status === 0) { /* matches only int 0 */ }
1532
+ ```
1533
+
1534
+ ## Escalate to
1535
+
1536
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
1537
+
1538
+ ### `py-bare-except` — python
1539
+
1540
+ # Bare except — hides timeouts, OOM, KeyboardInterrupt
1541
+
1542
+ Bare `except:` catches `SystemExit`, `KeyboardInterrupt`, `MemoryError`, and `GeneratorExit` — things you almost never want to swallow. It hides timeouts, OOM, and Ctrl-C.
1543
+
1544
+ ## Fix
1545
+
1546
+ Catch specific exceptions: `except (TimeoutError, ConnectionError):`.
1547
+
1548
+ ## Incorrect
1549
+
1550
+ ```python
1551
+ try:
1552
+ do_work()
1553
+ except:
1554
+ log("failed") # also swallows Ctrl-C, OOM, timeouts
1555
+ ```
1556
+
1557
+ ## Correct
1558
+
1559
+ ```python
1560
+ try:
1561
+ do_work()
1562
+ except Exception as e: # excludes SystemExit, KeyboardInterrupt
1563
+ log(f"failed: {e}")
1564
+ ```
1565
+
1566
+ ## Escalate to
1567
+
1568
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
1569
+
1570
+ ### `py-in-list-literal` — python
1571
+
1572
+ # Membership against a list literal — O(n) per check; use a set
1573
+
1574
+ `x in [a, b, c, ...]` is an O(n) linear scan. Inside a loop over m items, this is O(n × m). A `set` (or a literal `{a, b, c}` for membership) is O(1).
1575
+
1576
+ ## Fix
1577
+
1578
+ Hoist to a module-level frozenset({...}).
1579
+
1580
+ ## Incorrect
1581
+
1582
+ ```python
1583
+ # O(n × m)
1584
+ roles = ["admin", "editor", "owner"]
1585
+ result = [u for u in users if u.role in roles]
1586
+ ```
1587
+
1588
+ ## Correct
1589
+
1590
+ ```python
1591
+ # O(n + m)
1592
+ roles = {"admin", "editor", "owner"}
1593
+ result = [u for u in users if u.role in roles]
1594
+ ```
1595
+
1596
+ ## Escalate to
1597
+
1598
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1599
+
1600
+ ### `py-open-without-with` — python
1601
+
1602
+ # open() without `with` — risk of leaked file descriptors
1603
+
1604
+ `open()` returns a file object. Without `with`, an exception between open and close leaks the file descriptor. Long-running processes (servers, workers) eventually exhaust the OS limit.
1605
+
1606
+ ## Fix
1607
+
1608
+ Use `with open(path) as f:` to ensure the handle is released.
1609
+
1610
+ ## Incorrect
1611
+
1612
+ ```python
1613
+ f = open(path)
1614
+ data = f.read()
1615
+ f.close() # skipped if read() raises
1616
+ ```
1617
+
1618
+ ## Correct
1619
+
1620
+ ```python
1621
+ with open(path) as f:
1622
+ data = f.read()
1623
+ # f is closed even if read() raises
1624
+ ```
1625
+
1626
+ ### `py-range-len` — python
1627
+
1628
+ # range(len(x)) — un-Pythonic; use enumerate(x)
1629
+
1630
+ `for i in range(len(xs))` then indexing `xs[i]` is un-Pythonic, slower, and forces you to think about indices when you usually want the items. `enumerate` is idiomatic and faster.
1631
+
1632
+ ## Fix
1633
+
1634
+ Use `for i, item in enumerate(x):` when you need the index too.
1635
+
1636
+ ## Incorrect
1637
+
1638
+ ```python
1639
+ for i in range(len(xs)):
1640
+ process(xs[i])
1641
+ ```
1642
+
1643
+ ## Correct
1644
+
1645
+ ```python
1646
+ for x in xs:
1647
+ process(x)
1648
+
1649
+ # When you actually need the index
1650
+ for i, x in enumerate(xs):
1651
+ process(i, x)
1652
+ ```
1653
+
1654
+ ### `rb-string-concat-in-loop` — ruby
1655
+
1656
+ # String += in loop — O(n^2) since += creates a new string each iteration
1657
+
1658
+ `s += x` creates a new string each iteration — O(n²). `<<` mutates in place (O(n) amortized). `Array#join` is O(n) and idiomatic.
1659
+
1660
+ ## Fix
1661
+
1662
+ Use `<<` (mutates) or `parts.join` if building from an array.
1663
+
1664
+ ## Incorrect
1665
+
1666
+ ```ruby
1667
+ s = ""
1668
+ parts.each { |p| s += p } # O(n²)
1669
+ ```
1670
+
1671
+ ## Correct
1672
+
1673
+ ```ruby
1674
+ # Mutating concat
1675
+ s = String.new
1676
+ parts.each { |p| s << p }
1677
+
1678
+ # Idiomatic
1679
+ s = parts.join
1680
+ ```
1681
+
1682
+ ## Escalate to
1683
+
1684
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1685
+
1686
+ ### `rs-clone-in-loop` — rust
1687
+
1688
+ # .clone() inside iterator — avoid if a borrow works
1689
+
1690
+ A `.clone()` inside an iterator allocates a fresh copy per element. If a borrow (`&x`) or `Rc::clone` (cheap atomic increment for shared ownership) would do, the deep clone is wasted work.
1691
+
1692
+ ## Fix
1693
+
1694
+ Borrow with &, use Rc/Arc for shared ownership, or move once outside the loop.
1695
+
1696
+ ## Incorrect
1697
+
1698
+ ```rust
1699
+ // Deep clones every element
1700
+ let names: Vec<String> = users.iter().map(|u| u.name.clone()).collect();
1701
+ ```
1702
+
1703
+ ## Correct
1704
+
1705
+ ```rust
1706
+ // Borrow when possible
1707
+ let names: Vec<&str> = users.iter().map(|u| u.name.as_str()).collect();
1708
+
1709
+ // Cheap reference-counted clone when shared ownership is needed
1710
+ let shared: Vec<Rc<String>> = users.iter().map(|u| Rc::clone(&u.name)).collect();
1711
+ ```
1712
+
1713
+ ## Escalate to
1714
+
1715
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1716
+
1717
+ ### `rs-string-push-no-capacity` — rust
1718
+
1719
+ # String::new() + push_str in loop — repeated reallocation
1720
+
1721
+ `String::new()` starts at capacity 0. Each `push_str` past capacity reallocates. `with_capacity` or `join` avoids it.
1722
+
1723
+ ## Fix
1724
+
1725
+ Preallocate: `String::with_capacity(n)`, or `parts.join(sep)` for known parts.
1726
+
1727
+ ## Incorrect
1728
+
1729
+ ```rust
1730
+ let mut s = String::new();
1731
+ for part in parts.iter() {
1732
+ s.push_str(part); // reallocates as it grows
1733
+ }
1734
+ ```
1735
+
1736
+ ## Correct
1737
+
1738
+ ```rust
1739
+ let total: usize = parts.iter().map(|p| p.len()).sum();
1740
+ let mut s = String::with_capacity(total);
1741
+ for part in parts.iter() {
1742
+ s.push_str(part);
1743
+ }
1744
+
1745
+ // Or, simplest:
1746
+ let s = parts.join("");
1747
+ ```
1748
+
1749
+ ## Escalate to
1750
+
1751
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1752
+
1753
+ ### `rs-vec-push-no-capacity` — rust
1754
+
1755
+ # Vec::new() + push in loop — repeated reallocation
1756
+
1757
+ `Vec::new()` starts with capacity 0; each `push` past the current capacity reallocates and copies. Preallocating with `Vec::with_capacity(n)` does one allocation.
1758
+
1759
+ ## Fix
1760
+
1761
+ Preallocate: `Vec::with_capacity(n)`, or use `.collect::<Vec<_>>()` over an iterator that has a size hint.
1762
+
1763
+ ## Incorrect
1764
+
1765
+ ```rust
1766
+ let mut out = Vec::new();
1767
+ for x in input.iter() {
1768
+ out.push(transform(x)); // grows; reallocates log(n) times
1769
+ }
1770
+ ```
1771
+
1772
+ ## Correct
1773
+
1774
+ ```rust
1775
+ let mut out = Vec::with_capacity(input.len());
1776
+ for x in input.iter() {
1777
+ out.push(transform(x));
1778
+ }
1779
+
1780
+ // Or, when transform is pure:
1781
+ let out: Vec<_> = input.iter().map(transform).collect();
1782
+ ```
1783
+
1784
+ ## Escalate to
1785
+
1786
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
1787
+
1788
+ ### `sh-useless-cat-pipe` — shell
1789
+
1790
+ # cat file | cmd — useless use of cat (UUOC)
1791
+
1792
+ `cat file | cmd` reads the file and pipes through `cat` just to feed `cmd`. Every command that takes a file argument can read it directly — one fewer process, clearer intent.
1793
+
1794
+ ## Fix
1795
+
1796
+ Run the command on the file directly: `grep ... file` instead of `cat file | grep ...`.
1797
+
1798
+ ## Incorrect
1799
+
1800
+ ```shell
1801
+ cat access.log | grep "500"
1802
+ ```
1803
+
1804
+ ## Correct
1805
+
1806
+ ```shell
1807
+ grep "500" access.log
1808
+
1809
+ # Or stdin redirection if the command takes only stdin:
1810
+ cmd < access.log
1811
+ ```
1812
+
1813
+ ### `sql-or-in-where` — sql
1814
+
1815
+ # OR in WHERE — can prevent index use
1816
+
1817
+ A query planner can use an index on one side of an `OR` but often not both, falling back to a sequential scan. `UNION ALL` or `IN (...)` (when both sides are equality on the same column) usually wins.
1818
+
1819
+ ## Fix
1820
+
1821
+ Equality on same column → `WHERE col IN (...)`. Otherwise consider UNION ALL.
1822
+
1823
+ ## Incorrect
1824
+
1825
+ ```sql
1826
+ SELECT * FROM events
1827
+ WHERE user_id = $1 OR account_id = $1;
1828
+ ```
1829
+
1830
+ ## Correct
1831
+
1832
+ ```sql
1833
+ SELECT * FROM events WHERE user_id = $1
1834
+ UNION ALL
1835
+ SELECT * FROM events WHERE account_id = $1;
1836
+
1837
+ -- Or, when both sides are the same column:
1838
+ SELECT * FROM events WHERE user_id IN ($1, $2, $3);
1839
+ ```
1840
+
1841
+ ### `sql-select-no-limit` — sql
1842
+
1843
+ # SELECT without LIMIT — unbounded result set
1844
+
1845
+ A query with no `LIMIT` returns however many rows match. On a small table this is fine; on a growing one it eventually OOMs the client or the page. Add a bound, or paginate.
1846
+
1847
+ ## Fix
1848
+
1849
+ Add LIMIT, or paginate by id range. Unbounded reads OOM under growth.
1850
+
1851
+ ## Incorrect
1852
+
1853
+ ```sql
1854
+ SELECT id, email FROM users ORDER BY created_at DESC;
1855
+ ```
1856
+
1857
+ ## Correct
1858
+
1859
+ ```sql
1860
+ SELECT id, email FROM users
1861
+ ORDER BY created_at DESC
1862
+ LIMIT 100;
1863
+
1864
+ -- Keyset pagination for next page:
1865
+ SELECT id, email FROM users
1866
+ WHERE created_at < $1
1867
+ ORDER BY created_at DESC
1868
+ LIMIT 100;
1869
+ ```