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,47 @@
1
+ ---
2
+ id: go-loop-var-capture
3
+ title: Loop variable captured by goroutine — pre-1.22 races on the last value
4
+ severity: error
5
+ impact: CRITICAL
6
+ impactDescription: Will break or scale-fail in production
7
+ language: go
8
+ tags: go, error, invariant-guard
9
+ ---
10
+
11
+ # Loop variable captured by goroutine — pre-1.22 races on the last value
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ 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+.)
18
+
19
+ ## Incorrect
20
+
21
+ ```go
22
+ // Pre-1.22: all goroutines print the last item
23
+ for _, v := range items {
24
+ go func() {
25
+ fmt.Println(v)
26
+ }()
27
+ }
28
+ ```
29
+
30
+ ## Correct
31
+
32
+ ```go
33
+ // Pin the variable explicitly
34
+ for _, v := range items {
35
+ v := v
36
+ go func() { fmt.Println(v) }()
37
+ }
38
+
39
+ // Or pass as a parameter
40
+ for _, v := range items {
41
+ go func(v string) { fmt.Println(v) }(v)
42
+ }
43
+ ```
44
+
45
+ ## Escalate to
46
+
47
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
@@ -0,0 +1,39 @@
1
+ ---
2
+ id: go-slice-append-no-cap
3
+ title: append in loop without preallocated capacity — repeated reallocation
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: go
8
+ tags: go, info, complexity-cuts
9
+ ---
10
+
11
+ # append in loop without preallocated capacity — repeated reallocation
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Preallocate: `out := make([]T, 0, len(in))` then `append`.
18
+
19
+ ## Incorrect
20
+
21
+ ```go
22
+ var out []int
23
+ for _, x := range in {
24
+ out = append(out, x*2) // grows; reallocates log(n) times
25
+ }
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```go
31
+ out := make([]int, 0, len(in))
32
+ for _, x := range in {
33
+ out = append(out, x*2)
34
+ }
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,44 @@
1
+ ---
2
+ id: go-string-concat-in-loop
3
+ title: string += inside loop — O(n^2); use strings.Builder
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: go
8
+ tags: go, warning, complexity-cuts
9
+ ---
10
+
11
+ # string += inside loop — O(n^2); use strings.Builder
12
+
13
+ Go strings are immutable. `s += x` allocates each iteration — O(n²). `strings.Builder` reuses one backing array.
14
+
15
+ ## Fix
16
+
17
+ Use strings.Builder: `var sb strings.Builder; for ... { sb.WriteString(x) }; sb.String()`.
18
+
19
+ ## Incorrect
20
+
21
+ ```go
22
+ // O(n²)
23
+ var s string
24
+ for _, line := range lines {
25
+ s += line + "\n"
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```go
32
+ // O(n)
33
+ var sb strings.Builder
34
+ sb.Grow(len(lines) * 80)
35
+ for _, line := range lines {
36
+ sb.WriteString(line)
37
+ sb.WriteByte('\n')
38
+ }
39
+ s := sb.String()
40
+ ```
41
+
42
+ ## Escalate to
43
+
44
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,44 @@
1
+ ---
2
+ id: java-arraylist-remove-in-for-i
3
+ title: list.remove(i) inside for-i — index shifts; ConcurrentModification risk
4
+ severity: error
5
+ impact: CRITICAL
6
+ impactDescription: Will break or scale-fail in production
7
+ language: java
8
+ tags: java, error, invariant-guard
9
+ ---
10
+
11
+ # list.remove(i) inside for-i — index shifts; ConcurrentModification risk
12
+
13
+ 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`.
14
+
15
+ ## Fix
16
+
17
+ Iterate backwards, use Iterator.remove(), or collect indexes and remove in one removeIf().
18
+
19
+ ## Incorrect
20
+
21
+ ```java
22
+ for (int i = 0; i < list.size(); i++) {
23
+ if (shouldRemove(list.get(i))) {
24
+ list.remove(i); // shifts everything; next element is skipped
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```java
32
+ // Idiomatic and correct
33
+ list.removeIf(this::shouldRemove);
34
+
35
+ // Or, with an explicit iterator:
36
+ var it = list.iterator();
37
+ while (it.hasNext()) {
38
+ if (shouldRemove(it.next())) it.remove();
39
+ }
40
+ ```
41
+
42
+ ## Escalate to
43
+
44
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
@@ -0,0 +1,42 @@
1
+ ---
2
+ id: java-bare-catch-exception
3
+ title: catch (Exception) with empty body or log-only — swallows root cause
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: java
8
+ tags: java, warning, invariant-guard
9
+ ---
10
+
11
+ # catch (Exception) with empty body or log-only — swallows root cause
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ Catch the narrowest exception, rethrow as a domain exception, or handle with a documented fallback. Never swallow silently.
18
+
19
+ ## Incorrect
20
+
21
+ ```java
22
+ try {
23
+ riskyCall();
24
+ } catch (Exception e) {
25
+ e.printStackTrace(); // swallowed; caller thinks everything is fine
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```java
32
+ try {
33
+ riskyCall();
34
+ } catch (IOException e) {
35
+ // narrow exception, rethrow as domain error with cause preserved
36
+ throw new ReadFailedException("riskyCall failed for " + ctx, e);
37
+ }
38
+ ```
39
+
40
+ ## Escalate to
41
+
42
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
@@ -0,0 +1,40 @@
1
+ ---
2
+ id: java-list-contains-in-loop
3
+ title: List.contains inside iterator — O(n*m); use HashSet
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: java
8
+ tags: java, warning, complexity-cuts
9
+ ---
10
+
11
+ # List.contains inside iterator — O(n*m); use HashSet
12
+
13
+ `List.contains` is O(n). Used inside a stream/iterator over m items it is O(n·m). A `HashSet` lookup is O(1).
14
+
15
+ ## Fix
16
+
17
+ Build a HashSet outside: `Set<T> s = new HashSet<>(list); s.contains(x);`
18
+
19
+ ## Incorrect
20
+
21
+ ```java
22
+ // O(n·m)
23
+ var active = users.stream()
24
+ .filter(u -> !banned.contains(u.id))
25
+ .toList();
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```java
31
+ // O(n+m)
32
+ var bannedSet = new HashSet<>(banned);
33
+ var active = users.stream()
34
+ .filter(u -> !bannedSet.contains(u.id))
35
+ .toList();
36
+ ```
37
+
38
+ ## Escalate to
39
+
40
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,42 @@
1
+ ---
2
+ id: java-string-concat-in-loop
3
+ title: String += inside loop — O(n^2) on immutable String
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: java
8
+ tags: java, warning, complexity-cuts
9
+ ---
10
+
11
+ # String += inside loop — O(n^2) on immutable String
12
+
13
+ Java `String` is immutable. `s += x` allocates a fresh `String` (and an underlying `char[]`) each iteration — O(n²) total work. `StringBuilder` reuses one buffer.
14
+
15
+ ## Fix
16
+
17
+ Use StringBuilder: `var sb = new StringBuilder(); for (...) sb.append(x); sb.toString();`
18
+
19
+ ## Incorrect
20
+
21
+ ```java
22
+ // O(n²)
23
+ String s = "";
24
+ for (var line : lines) {
25
+ s += line + "\n";
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```java
32
+ // O(n)
33
+ var sb = new StringBuilder(lines.size() * 80);
34
+ for (var line : lines) {
35
+ sb.append(line).append('\n');
36
+ }
37
+ String s = sb.toString();
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,31 @@
1
+ ---
2
+ id: js-anonymous-handler-jsx
3
+ title: Anonymous arrow handler in JSX — breaks memoized-child equality
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
+ # Anonymous arrow handler in JSX — breaks memoized-child equality
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Wrap with useCallback if the child is memoized. Otherwise ignore.
18
+
19
+ ## Incorrect
20
+
21
+ ```tsx
22
+ // Child is React.memo — this defeats it
23
+ <MemoButton onClick={() => save(id)} />
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```tsx
29
+ const onClick = useCallback(() => save(id), [id]);
30
+ <MemoButton onClick={onClick} />
31
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ id: js-array-key-index
3
+ title: key={index} on a list — breaks identity for reorderable items
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: javascript
8
+ tags: javascript, info
9
+ ---
10
+
11
+ # key={index} on a list — breaks identity for reorderable items
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ Use a stable id when the list can reorder, insert, or delete in the middle.
18
+
19
+ ## Incorrect
20
+
21
+ ```tsx
22
+ {items.map((item, i) => <Row key={i} item={item} />)}
23
+ ```
24
+
25
+ ## Correct
26
+
27
+ ```tsx
28
+ {items.map((item) => <Row key={item.id} item={item} />)}
29
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ id: js-async-in-foreach
3
+ title: async passed to .forEach — returned promises are dropped
4
+ severity: error
5
+ impact: CRITICAL
6
+ impactDescription: Will break or scale-fail in production
7
+ language: javascript
8
+ tags: javascript, error, complexity-cuts
9
+ ---
10
+
11
+ # async passed to .forEach — returned promises are dropped
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ Use `for (const x of arr) await fn(x)` or `await Promise.all(arr.map(fn))`.
18
+
19
+ ## Incorrect
20
+
21
+ ```ts
22
+ // Promises dropped; errors silently swallowed
23
+ items.forEach(async (item) => {
24
+ await save(item);
25
+ });
26
+ console.log('done'); // logs before any save completes
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```ts
32
+ // Sequential, with errors propagated
33
+ for (const item of items) {
34
+ await save(item);
35
+ }
36
+
37
+ // Parallel
38
+ await Promise.all(items.map((item) => save(item)));
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: js-await-in-for-loop
3
+ title: await inside for-loop — likely N+1 / serialized I/O
4
+ severity: error
5
+ impact: CRITICAL
6
+ impactDescription: Will break or scale-fail in production
7
+ language: javascript
8
+ tags: javascript, error, complexity-cuts
9
+ ---
10
+
11
+ # await inside for-loop — likely N+1 / serialized I/O
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Collect promises into an array and use Promise.all(...), or batch with a bulk query (WHERE id IN (...)).
18
+
19
+ ## Incorrect
20
+
21
+ ```ts
22
+ // Wall-clock: O(n × latency)
23
+ for (const id of ids) {
24
+ const user = await db.users.findById(id);
25
+ results.push(user);
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```ts
32
+ // Wall-clock: O(latency); one bulk query
33
+ const users = await db.users.findMany({ where: { id: { in: ids } } });
34
+
35
+ // Or, if calls are truly independent:
36
+ const results = await Promise.all(ids.map(id => db.users.findById(id)));
37
+ ```
38
+
39
+ ## Escalate to
40
+
41
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,33 @@
1
+ ---
2
+ id: js-deep-clone-via-json
3
+ title: JSON.parse(JSON.stringify(x)) — slow clone; loses Dates/Maps/undefined
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
+ # JSON.parse(JSON.stringify(x)) — slow clone; loses Dates/Maps/undefined
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ Use structuredClone(x), or copy only the fields you actually need.
18
+
19
+ ## Incorrect
20
+
21
+ ```ts
22
+ const copy = JSON.parse(JSON.stringify(state));
23
+ // state.createdAt was a Date — now a string
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```ts
29
+ const copy = structuredClone(state); // preserves Date, Map, Set, cycles
30
+
31
+ // Or, when you only need a few fields:
32
+ const copy = { id: state.id, name: state.name };
33
+ ```
@@ -0,0 +1,41 @@
1
+ ---
2
+ id: js-helper-call-in-iterator
3
+ title: get*/find*/fetch* helper called inside iterator — likely N round-trips
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
+ # get*/find*/fetch* helper called inside iterator — likely N round-trips
12
+
13
+ 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).
14
+
15
+ ## Fix
16
+
17
+ Hoist the helper out of the loop and pass a precomputed lookup (Map/Set) into the iterator, or batch with a single bulk query.
18
+
19
+ ## Incorrect
20
+
21
+ ```ts
22
+ // N round trips
23
+ const enriched = orders.map((o) => ({
24
+ ...o,
25
+ user: getUserById(o.userId),
26
+ }));
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```ts
32
+ // 1 round trip
33
+ const userIds = [...new Set(orders.map((o) => o.userId))];
34
+ const users = await db.users.findMany({ where: { id: { in: userIds } } });
35
+ const userById = new Map(users.map((u) => [u.id, u]));
36
+ const enriched = orders.map((o) => ({ ...o, user: userById.get(o.userId) }));
37
+ ```
38
+
39
+ ## Escalate to
40
+
41
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,37 @@
1
+ ---
2
+ id: js-includes-in-iterator
3
+ title: Array.includes inside .map/.filter/.forEach — O(n*m); use a Set
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: javascript
8
+ tags: javascript, info, complexity-cuts
9
+ ---
10
+
11
+ # Array.includes inside .map/.filter/.forEach — O(n*m); use a Set
12
+
13
+ `.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).
14
+
15
+ ## Fix
16
+
17
+ Build a Set once outside the iterator, then `set.has(x)` inside.
18
+
19
+ ## Incorrect
20
+
21
+ ```ts
22
+ // O(n × m)
23
+ const allowed = ['admin', 'editor', 'owner'];
24
+ const result = users.filter((u) => allowed.includes(u.role));
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```ts
30
+ // O(n + m)
31
+ const allowed = new Set(['admin', 'editor', 'owner']);
32
+ const result = users.filter((u) => allowed.has(u.role));
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,35 @@
1
+ ---
2
+ id: js-inline-object-jsx-prop
3
+ title: Inline object literal as JSX prop — new reference 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
+ # Inline object literal as JSX prop — new reference every render
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Hoist outside render, or wrap in useMemo if the child is React.memo.
18
+
19
+ ## Incorrect
20
+
21
+ ```tsx
22
+ <Chart options={{ animated: true, color: 'red' }} />
23
+ ```
24
+
25
+ ## Correct
26
+
27
+ ```tsx
28
+ // Hoist if static
29
+ const CHART_OPTIONS = { animated: true, color: 'red' };
30
+ <Chart options={CHART_OPTIONS} />
31
+
32
+ // Memoize if derived
33
+ const options = useMemo(() => ({ animated, color }), [animated, color]);
34
+ <Chart options={options} />
35
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ id: js-nested-for-loops
3
+ title: Nested for-loops — O(n*m); consider hashing one side
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: javascript
8
+ tags: javascript, info, complexity-cuts
9
+ ---
10
+
11
+ # Nested for-loops — O(n*m); consider hashing one side
12
+
13
+ Two `for` loops that check membership between arrays are O(n × m). Hashing one side into a `Set` makes it O(n + m).
14
+
15
+ ## Fix
16
+
17
+ If looking up between the two arrays, put one into a Set/Map first → O(n+m).
18
+
19
+ ## Incorrect
20
+
21
+ ```ts
22
+ // O(n × m)
23
+ const matches = [];
24
+ for (const a of left) {
25
+ for (const b of right) {
26
+ if (a.id === b.id) matches.push([a, b]);
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## Correct
32
+
33
+ ```ts
34
+ // O(n + m)
35
+ const rightById = new Map(right.map((b) => [b.id, b]));
36
+ const matches = [];
37
+ for (const a of left) {
38
+ const b = rightById.get(a.id);
39
+ if (b) matches.push([a, b]);
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,38 @@
1
+ ---
2
+ id: js-spread-in-reduce
3
+ title: Object spread inside reduce — O(n^2)
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
+ # Object spread inside reduce — O(n^2)
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Mutate the accumulator (`acc[k] = v; return acc`) or use Object.fromEntries(arr.map(...)).
18
+
19
+ ## Incorrect
20
+
21
+ ```ts
22
+ // O(n²)
23
+ const byId = items.reduce((acc, x) => ({ ...acc, [x.id]: x }), {});
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```ts
29
+ // O(n)
30
+ const byId = Object.fromEntries(items.map((x) => [x.id, x]));
31
+
32
+ // Or mutate the accumulator
33
+ const byId = items.reduce((acc, x) => { acc[x.id] = x; return acc; }, {});
34
+ ```
35
+
36
+ ## Escalate to
37
+
38
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.