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,35 @@
1
+ ---
2
+ id: js-unique-via-indexof
3
+ title: Unique-by-indexOf — O(n^2) dedupe; use `Array.from(new Set(arr))`
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: javascript
8
+ tags: javascript, warning, complexity-cuts
9
+ ---
10
+
11
+ # Unique-by-indexOf — O(n^2) dedupe; use `Array.from(new Set(arr))`
12
+
13
+ `.filter((x, i, a) => a.indexOf(x) === i)` is the textbook O(n²) dedupe. A `Set` does it in O(n).
14
+
15
+ ## Fix
16
+
17
+ Use `Array.from(new Set(arr))`, or build a Set inline. O(n) instead of O(n^2).
18
+
19
+ ## Incorrect
20
+
21
+ ```ts
22
+ // O(n²)
23
+ const unique = arr.filter((x, i, a) => a.indexOf(x) === i);
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```ts
29
+ // O(n)
30
+ const unique = Array.from(new Set(arr));
31
+ ```
32
+
33
+ ## Escalate to
34
+
35
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,33 @@
1
+ ---
2
+ id: js-useeffect-missing-deps
3
+ title: useEffect without a deps array — runs after every render
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: javascript
8
+ tags: javascript, warning
9
+ ---
10
+
11
+ # useEffect without a deps array — runs after every render
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Add a deps array — `[]` for mount-only, `[a, b]` to track changes.
18
+
19
+ ## Incorrect
20
+
21
+ ```tsx
22
+ useEffect(() => {
23
+ setUser(fetchUser(id));
24
+ }); // no deps — runs every render
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```tsx
30
+ useEffect(() => {
31
+ setUser(fetchUser(id));
32
+ }, [id]); // runs when id changes
33
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ id: php-count-in-for-condition
3
+ title: count() in for-condition — recomputed every iteration
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: php
8
+ tags: php, warning, complexity-cuts
9
+ ---
10
+
11
+ # count() in for-condition — recomputed every iteration
12
+
13
+ PHP recomputes the loop condition every iteration. `count($a)` on a 100k-element array, called 100k times, is 10B element traversals. Hoist it.
14
+
15
+ ## Fix
16
+
17
+ Hoist: `for ($i = 0, $n = count($a); $i < $n; $i++)`. Or use `foreach`.
18
+
19
+ ## Incorrect
20
+
21
+ ```php
22
+ <?php
23
+ for ($i = 0; $i < count($a); $i++) {
24
+ echo $a[$i];
25
+ }
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```php
31
+ <?php
32
+ // Hoist the length
33
+ for ($i = 0, $n = count($a); $i < $n; $i++) {
34
+ echo $a[$i];
35
+ }
36
+
37
+ // Or, idiomatic
38
+ foreach ($a as $x) {
39
+ echo $x;
40
+ }
41
+ ```
42
+
43
+ ## Escalate to
44
+
45
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,42 @@
1
+ ---
2
+ id: php-in-array-in-loop
3
+ title: in_array inside loop — O(n*m); use array_flip + isset
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: php
8
+ tags: php, warning, complexity-cuts
9
+ ---
10
+
11
+ # in_array inside loop — O(n*m); use array_flip + isset
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ `$set = array_flip($haystack); ... isset($set[$x])` is O(1) per check.
18
+
19
+ ## Incorrect
20
+
21
+ ```php
22
+ <?php
23
+ foreach ($users as $u) {
24
+ if (in_array($u->id, $banned)) continue;
25
+ ship($u);
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```php
32
+ <?php
33
+ $bannedSet = array_flip($banned); // O(n) once
34
+ foreach ($users as $u) {
35
+ if (isset($bannedSet[$u->id])) continue; // O(1)
36
+ ship($u);
37
+ }
38
+ ```
39
+
40
+ ## Escalate to
41
+
42
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,35 @@
1
+ ---
2
+ id: php-loose-equality
3
+ title: == / != — loose equality has surprising coercions
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: php
8
+ tags: php, info, invariant-guard
9
+ ---
10
+
11
+ # == / != — loose equality has surprising coercions
12
+
13
+ PHP `==` does type coercion in surprising ways (`"0" == false` is `true`, `"abc" == 0` was `true` before PHP 8). `===` compares value AND type — no surprises.
14
+
15
+ ## Fix
16
+
17
+ Use `===` / `!==` unless type-juggling is explicitly intended (with a comment).
18
+
19
+ ## Incorrect
20
+
21
+ ```php
22
+ <?php
23
+ if ($status == 0) { /* matches "", "0", false, null, 0, "abc" pre-PHP 8 */ }
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```php
29
+ <?php
30
+ if ($status === 0) { /* matches only int 0 */ }
31
+ ```
32
+
33
+ ## Escalate to
34
+
35
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
@@ -0,0 +1,47 @@
1
+ ---
2
+ id: php-query-in-loop
3
+ title: DB query inside loop — N+1
4
+ severity: error
5
+ impact: CRITICAL
6
+ impactDescription: Will break or scale-fail in production
7
+ language: php
8
+ tags: php, error, complexity-cuts
9
+ ---
10
+
11
+ # DB query inside loop — N+1
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Collect the ids, run ONE `WHERE id IN (...)` query, index by id, then loop. In Eloquent use eager loading: `Model::with('relation')`.
18
+
19
+ ## Incorrect
20
+
21
+ ```php
22
+ <?php
23
+ foreach ($orderIds as $id) {
24
+ $row = $db->query("SELECT * FROM orders WHERE id = $id"); // 1 query per id
25
+ process($row);
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```php
32
+ <?php
33
+ // One bulk query
34
+ $placeholders = implode(',', array_fill(0, count($orderIds), '?'));
35
+ $stmt = $db->prepare("SELECT * FROM orders WHERE id IN ($placeholders)");
36
+ $stmt->execute($orderIds);
37
+ foreach ($stmt->fetchAll() as $row) {
38
+ process($row);
39
+ }
40
+
41
+ // Eloquent / Doctrine: use eager loading
42
+ $orders = Order::with('items')->whereIn('id', $orderIds)->get();
43
+ ```
44
+
45
+ ## Escalate to
46
+
47
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,39 @@
1
+ ---
2
+ id: py-bare-except
3
+ title: Bare except — hides timeouts, OOM, KeyboardInterrupt
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: python
8
+ tags: python, info, invariant-guard
9
+ ---
10
+
11
+ # Bare except — hides timeouts, OOM, KeyboardInterrupt
12
+
13
+ Bare `except:` catches `SystemExit`, `KeyboardInterrupt`, `MemoryError`, and `GeneratorExit` — things you almost never want to swallow. It hides timeouts, OOM, and Ctrl-C.
14
+
15
+ ## Fix
16
+
17
+ Catch specific exceptions: `except (TimeoutError, ConnectionError):`.
18
+
19
+ ## Incorrect
20
+
21
+ ```python
22
+ try:
23
+ do_work()
24
+ except:
25
+ log("failed") # also swallows Ctrl-C, OOM, timeouts
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```python
31
+ try:
32
+ do_work()
33
+ except Exception as e: # excludes SystemExit, KeyboardInterrupt
34
+ log(f"failed: {e}")
35
+ ```
36
+
37
+ ## Escalate to
38
+
39
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
@@ -0,0 +1,42 @@
1
+ ---
2
+ id: py-django-loop-without-eager
3
+ title: Django ORM iteration — verify select_related/prefetch_related to avoid N+1
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: python
8
+ tags: python, warning, complexity-cuts
9
+ ---
10
+
11
+ # Django ORM iteration — verify select_related/prefetch_related to avoid N+1
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Add .select_related('fk') for FK/OneToOne, .prefetch_related('rel') for reverse FK / M2M.
18
+
19
+ ## Incorrect
20
+
21
+ ```python
22
+ # N+1 queries
23
+ for order in Order.objects.all():
24
+ print(order.user.email) # extra query per order
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```python
30
+ # 1 query with JOIN
31
+ for order in Order.objects.select_related("user"):
32
+ print(order.user.email)
33
+
34
+ # For reverse FK / M2M:
35
+ for user in User.objects.prefetch_related("orders"):
36
+ for order in user.orders.all():
37
+ ...
38
+ ```
39
+
40
+ ## Escalate to
41
+
42
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,37 @@
1
+ ---
2
+ id: py-in-list-literal
3
+ title: Membership against a list literal — O(n) per check; use a set
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: python
8
+ tags: python, info, complexity-cuts
9
+ ---
10
+
11
+ # Membership against a list literal — O(n) per check; use a set
12
+
13
+ `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).
14
+
15
+ ## Fix
16
+
17
+ Hoist to a module-level frozenset({...}).
18
+
19
+ ## Incorrect
20
+
21
+ ```python
22
+ # O(n × m)
23
+ roles = ["admin", "editor", "owner"]
24
+ result = [u for u in users if u.role in roles]
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```python
30
+ # O(n + m)
31
+ roles = {"admin", "editor", "owner"}
32
+ result = [u for u in users if u.role in roles]
33
+ ```
34
+
35
+ ## Escalate to
36
+
37
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,39 @@
1
+ ---
2
+ id: py-mutable-default-arg
3
+ title: Mutable default argument — shared across calls
4
+ severity: error
5
+ impact: CRITICAL
6
+ impactDescription: Will break or scale-fail in production
7
+ language: python
8
+ tags: python, error, invariant-guard
9
+ ---
10
+
11
+ # Mutable default argument — shared across calls
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Default to None; create inside: `def f(x=None): x = x or []`.
18
+
19
+ ## Incorrect
20
+
21
+ ```python
22
+ def collect(item, bucket=[]): # shared across all calls!
23
+ bucket.append(item)
24
+ return bucket
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```python
30
+ def collect(item, bucket=None):
31
+ if bucket is None:
32
+ bucket = []
33
+ bucket.append(item)
34
+ return bucket
35
+ ```
36
+
37
+ ## Escalate to
38
+
39
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
@@ -0,0 +1,33 @@
1
+ ---
2
+ id: py-open-without-with
3
+ title: open() without `with` — risk of leaked file descriptors
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: python
8
+ tags: python, info
9
+ ---
10
+
11
+ # open() without `with` — risk of leaked file descriptors
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ Use `with open(path) as f:` to ensure the handle is released.
18
+
19
+ ## Incorrect
20
+
21
+ ```python
22
+ f = open(path)
23
+ data = f.read()
24
+ f.close() # skipped if read() raises
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```python
30
+ with open(path) as f:
31
+ data = f.read()
32
+ # f is closed even if read() raises
33
+ ```
@@ -0,0 +1,35 @@
1
+ ---
2
+ id: py-range-len
3
+ title: range(len(x)) — un-Pythonic; use enumerate(x)
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: python
8
+ tags: python, info
9
+ ---
10
+
11
+ # range(len(x)) — un-Pythonic; use enumerate(x)
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ Use `for i, item in enumerate(x):` when you need the index too.
18
+
19
+ ## Incorrect
20
+
21
+ ```python
22
+ for i in range(len(xs)):
23
+ process(xs[i])
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```python
29
+ for x in xs:
30
+ process(x)
31
+
32
+ # When you actually need the index
33
+ for i, x in enumerate(xs):
34
+ process(i, x)
35
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ id: py-string-concat-in-loop
3
+ title: String += inside loop — O(n^2)
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: python
8
+ tags: python, warning, complexity-cuts
9
+ ---
10
+
11
+ # String += inside loop — O(n^2)
12
+
13
+ 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).
14
+
15
+ ## Fix
16
+
17
+ Append to a list, then ''.join(list). Or use io.StringIO.
18
+
19
+ ## Incorrect
20
+
21
+ ```python
22
+ # O(n²)
23
+ s = ""
24
+ for line in lines:
25
+ s += line + "\n"
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```python
31
+ # O(n)
32
+ s = "\n".join(lines) + "\n"
33
+
34
+ # Or, for incremental building:
35
+ parts = []
36
+ for line in lines:
37
+ parts.append(line)
38
+ s = "\n".join(parts)
39
+ ```
40
+
41
+ ## Escalate to
42
+
43
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,41 @@
1
+ ---
2
+ id: rb-bare-rescue
3
+ title: Bare rescue — catches StandardError, hides bugs
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: ruby
8
+ tags: ruby, warning, invariant-guard
9
+ ---
10
+
11
+ # Bare rescue — catches StandardError, hides bugs
12
+
13
+ A bare `rescue` (without a class) catches `StandardError` — including `NoMethodError`, `ArgumentError`, and other bugs you almost always want to surface. Catch the specific class.
14
+
15
+ ## Fix
16
+
17
+ Catch a specific class: `rescue Net::ReadTimeout` etc. Bare rescue swallows too much.
18
+
19
+ ## Incorrect
20
+
21
+ ```ruby
22
+ begin
23
+ fetch_remote
24
+ rescue
25
+ retry # also catches typos and bugs in fetch_remote
26
+ end
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```ruby
32
+ begin
33
+ fetch_remote
34
+ rescue Net::ReadTimeout, Net::OpenTimeout => e
35
+ retry
36
+ end
37
+ ```
38
+
39
+ ## Escalate to
40
+
41
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
@@ -0,0 +1,37 @@
1
+ ---
2
+ id: rb-include-in-iterator
3
+ title: Array#include? inside iterator — O(n*m); use Set
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: ruby
8
+ tags: ruby, warning, complexity-cuts
9
+ ---
10
+
11
+ # Array#include? inside iterator — O(n*m); use Set
12
+
13
+ `Array#include?` is O(n). Used inside an iterator over m items it is O(n·m). `Set#include?` is O(1).
14
+
15
+ ## Fix
16
+
17
+ `require 'set'; allowed = Set.new(list); ... allowed.include?(x)`.
18
+
19
+ ## Incorrect
20
+
21
+ ```ruby
22
+ # O(n·m)
23
+ users.select { |u| banned.include?(u.id) }
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```ruby
29
+ # O(n+m)
30
+ require 'set'
31
+ banned_set = Set.new(banned)
32
+ users.select { |u| banned_set.include?(u.id) }
33
+ ```
34
+
35
+ ## Escalate to
36
+
37
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,39 @@
1
+ ---
2
+ id: rb-n-plus-one-activerecord
3
+ title: ActiveRecord iteration touching an association — N+1 without includes
4
+ severity: error
5
+ impact: CRITICAL
6
+ impactDescription: Will break or scale-fail in production
7
+ language: ruby
8
+ tags: ruby, error, complexity-cuts
9
+ ---
10
+
11
+ # ActiveRecord iteration touching an association — N+1 without includes
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Use `Model.includes(:assoc)` or `:preload` / `:eager_load` before iterating.
18
+
19
+ ## Incorrect
20
+
21
+ ```ruby
22
+ # N+1 queries
23
+ Post.all.each do |p|
24
+ puts p.author.name # 1 extra query per post
25
+ end
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```ruby
31
+ # 2 queries total (or 1 with eager_load)
32
+ Post.includes(:author).each do |p|
33
+ puts p.author.name
34
+ end
35
+ ```
36
+
37
+ ## Escalate to
38
+
39
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,39 @@
1
+ ---
2
+ id: rb-string-concat-in-loop
3
+ title: String += in loop — O(n^2) since += creates a new string each iteration
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: ruby
8
+ tags: ruby, info, complexity-cuts
9
+ ---
10
+
11
+ # String += in loop — O(n^2) since += creates a new string each iteration
12
+
13
+ `s += x` creates a new string each iteration — O(n²). `<<` mutates in place (O(n) amortized). `Array#join` is O(n) and idiomatic.
14
+
15
+ ## Fix
16
+
17
+ Use `<<` (mutates) or `parts.join` if building from an array.
18
+
19
+ ## Incorrect
20
+
21
+ ```ruby
22
+ s = ""
23
+ parts.each { |p| s += p } # O(n²)
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```ruby
29
+ # Mutating concat
30
+ s = String.new
31
+ parts.each { |p| s << p }
32
+
33
+ # Idiomatic
34
+ s = parts.join
35
+ ```
36
+
37
+ ## Escalate to
38
+
39
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.