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,118 @@
1
+ # Memory reference
2
+
3
+ Memory bugs are quiet. They don't break tests. They get noticed when a process OOMs in production at 3am.
4
+
5
+ ## The big classes of leak
6
+
7
+ ### 1. Unbounded cache
8
+ ```js
9
+ // Bad — grows forever
10
+ const cache = new Map();
11
+ function get(key) {
12
+ if (!cache.has(key)) cache.set(key, expensive(key));
13
+ return cache.get(key);
14
+ }
15
+
16
+ // Good — bounded
17
+ import { LRUCache } from 'lru-cache';
18
+ const cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 5 });
19
+ ```
20
+ Any `Map` / `dict` keyed by user input is a candidate leak.
21
+
22
+ ### 2. Event listeners not removed
23
+ ```js
24
+ // Bad — every component instance adds a listener; unmount doesn't remove it
25
+ useEffect(() => {
26
+ window.addEventListener('resize', handler);
27
+ }, []);
28
+
29
+ // Good
30
+ useEffect(() => {
31
+ window.addEventListener('resize', handler);
32
+ return () => window.removeEventListener('resize', handler);
33
+ }, []);
34
+ ```
35
+ Same for: `setInterval`, `setTimeout` (for long timers), `IntersectionObserver`, `MutationObserver`, EventEmitter `.on`, Postgres / Redis client subscriptions, WebSocket.
36
+
37
+ ### 3. Closures retaining large parents
38
+ ```js
39
+ // Bad — handler closes over `largeData` even though it only uses `id`
40
+ function makeHandler(largeData) {
41
+ const id = largeData.id;
42
+ return () => doStuff(id, largeData); // largeData stays alive as long as handler exists
43
+ }
44
+
45
+ // Better — destructure what you need
46
+ function makeHandler({ id }) {
47
+ return () => doStuff(id);
48
+ }
49
+ ```
50
+ Watch for: handlers attached to long-lived emitters, cached partial applications, React `useCallback` deps that include big objects.
51
+
52
+ ### 4. Detached DOM
53
+ ```js
54
+ // Bad — node removed from DOM, but JS still references it
55
+ const cache = { lastTooltip: tooltipEl };
56
+ tooltipEl.remove(); // removed from DOM but cache still holds → not GC'd
57
+ ```
58
+ Null out references when you're done.
59
+
60
+ ### 5. Streams not consumed / not closed
61
+ ```js
62
+ // Bad — leaks file handle if `process` throws
63
+ const stream = fs.createReadStream(path);
64
+ await process(stream);
65
+
66
+ // Good
67
+ const stream = fs.createReadStream(path);
68
+ try { await process(stream); } finally { stream.destroy(); }
69
+ ```
70
+ Same for: DB cursors, HTTP responses (in Node, `res.on('data')` without consuming), child processes.
71
+
72
+ ### 6. Globals and module-level state
73
+ A module-level `Map`, `Array`, or counter that's appended to from request handlers but never trimmed is a leak.
74
+
75
+ ## Buffer vs stream
76
+
77
+ If the input can be unbounded (user upload, log file, ML response), **stream** it:
78
+
79
+ ```js
80
+ // Bad — loads full body into memory
81
+ const body = await req.text();
82
+ const data = JSON.parse(body);
83
+
84
+ // Good — streaming JSON parser, or process line-by-line
85
+ import readline from 'readline';
86
+ const rl = readline.createInterface({ input: req });
87
+ for await (const line of rl) handle(JSON.parse(line));
88
+ ```
89
+
90
+ In Python: read `chunk = f.read(64 * 1024)` in a loop, or use generators. Don't `f.read()` an arbitrary file.
91
+
92
+ ## Large objects in request scope
93
+
94
+ Holding a 50 MB result set in memory per concurrent request → 50 reqs = 2.5 GB. Patterns:
95
+
96
+ - Paginate. Don't return "all".
97
+ - Project (`SELECT id, name`) — don't `SELECT *`.
98
+ - Stream the response. In Node, write chunks to `res`. In Python/Django, `StreamingHttpResponse`.
99
+
100
+ ## "Deep clone to be safe" anti-pattern
101
+
102
+ ```js
103
+ const copy = JSON.parse(JSON.stringify(huge)); // O(n) time + O(n) memory + breaks Dates, Maps, etc.
104
+ ```
105
+ Almost always wrong. Either you need a true clone (use `structuredClone`) or you don't (just spread the keys you need).
106
+
107
+ ## Observable signs in prod
108
+
109
+ - RSS / heap grows monotonically over hours and doesn't drop after low traffic → cache or listener leak.
110
+ - Pods OOM-killed during long-running migrations → buffer instead of stream.
111
+ - Latency P99 climbs slowly during a deploy's lifetime, drops at restart → GC pressure from leak.
112
+
113
+ ## Tools
114
+
115
+ - Node: `node --inspect`, Chrome DevTools "Memory" → take heap snapshots over time, diff them.
116
+ - Browser: same DevTools panel, "Detached elements" view for DOM leaks.
117
+ - Python: `tracemalloc`, `objgraph` for retention paths.
118
+ - Production: track RSS over time; alert on monotonic growth.
@@ -0,0 +1,139 @@
1
+ # N+1 query reference
2
+
3
+ The single most common performance bug shipped by AI assistants. One outer query, then one query per row.
4
+
5
+ ## How to spot it
6
+
7
+ Any of these is N+1 until proven otherwise:
8
+
9
+ ```js
10
+ // JS / TS
11
+ for (const user of users) {
12
+ const posts = await db.post.findMany({ where: { userId: user.id } });
13
+ }
14
+
15
+ users.map(async u => await fetchProfile(u.id)); // hidden — runs in parallel but still N calls
16
+ await Promise.all(users.map(u => fetchProfile(u.id))); // parallel — still N HTTP calls
17
+ ```
18
+
19
+ ```python
20
+ # Python / Django
21
+ for user in users:
22
+ posts = Post.objects.filter(user=user) # N+1
23
+ profile = user.profile # N+1 if not select_related
24
+
25
+ # SQLAlchemy
26
+ for user in session.query(User).all():
27
+ print(user.posts) # lazy load = N+1
28
+ ```
29
+
30
+ ```ruby
31
+ # Active Record
32
+ @users.each do |u|
33
+ puts u.posts.count # N+1 + extra count query
34
+ end
35
+ ```
36
+
37
+ Heuristic: if the loop body calls anything that hits the DB, network, or filesystem — stop and batch.
38
+
39
+ ## Fixes — by ORM
40
+
41
+ ### Prisma (TS)
42
+ ```ts
43
+ // Bad
44
+ for (const u of users) { u.posts = await db.post.findMany({ where: { userId: u.id } }); }
45
+
46
+ // Good — eager via include
47
+ const users = await db.user.findMany({ include: { posts: true } });
48
+
49
+ // Good — explicit batch
50
+ const posts = await db.post.findMany({ where: { userId: { in: users.map(u => u.id) } } });
51
+ const byUser = Map.groupBy(posts, p => p.userId); // or reduce
52
+ ```
53
+
54
+ ### Drizzle (TS)
55
+ ```ts
56
+ // Bad
57
+ for (const u of users) await db.select().from(posts).where(eq(posts.userId, u.id));
58
+
59
+ // Good
60
+ const all = await db.select().from(posts).where(inArray(posts.userId, users.map(u => u.id)));
61
+ ```
62
+
63
+ ### Django
64
+ ```python
65
+ # Bad
66
+ for u in User.objects.all():
67
+ print(u.profile.bio) # N+1
68
+
69
+ # Good
70
+ for u in User.objects.select_related('profile'): # FK / OneToOne — JOIN
71
+ print(u.profile.bio)
72
+
73
+ for u in User.objects.prefetch_related('posts'): # reverse FK / M2M — batched IN query
74
+ print(list(u.posts.all()))
75
+ ```
76
+
77
+ ### SQLAlchemy
78
+ ```python
79
+ # Bad: default lazy
80
+ users = session.query(User).all()
81
+ for u in users: u.posts # N+1
82
+
83
+ # Good
84
+ from sqlalchemy.orm import selectinload, joinedload
85
+ users = session.query(User).options(selectinload(User.posts)).all()
86
+ ```
87
+
88
+ ### Active Record (Ruby)
89
+ ```ruby
90
+ # Bad
91
+ @users.each { |u| u.posts.each { |p| ... } }
92
+
93
+ # Good
94
+ @users = User.includes(:posts)
95
+ ```
96
+
97
+ ## Raw SQL — the underlying fix
98
+
99
+ All ORMs are doing one of these under the hood:
100
+
101
+ ```sql
102
+ -- JOIN (good for 1:1 and small 1:many)
103
+ SELECT u.*, p.*
104
+ FROM users u
105
+ LEFT JOIN posts p ON p.user_id = u.id
106
+ WHERE u.tenant_id = $1;
107
+
108
+ -- Two queries with IN (better when "many" is large — avoids row duplication)
109
+ SELECT * FROM users WHERE tenant_id = $1;
110
+ SELECT * FROM posts WHERE user_id IN ($1, $2, ..., $N);
111
+ ```
112
+
113
+ Prefer the second form when the "many" side is wide. JOIN-then-group inflates payload by `O(parents * children)`.
114
+
115
+ ## DataLoader pattern (when you can't restructure)
116
+
117
+ In GraphQL / per-request batching, where requests come in one at a time but you want to coalesce:
118
+
119
+ ```ts
120
+ const userLoader = new DataLoader(async (ids) => {
121
+ const users = await db.user.findMany({ where: { id: { in: ids } } });
122
+ return ids.map(id => users.find(u => u.id === id));
123
+ });
124
+
125
+ // Now N parallel resolver calls become 1 batched query within the request tick.
126
+ await Promise.all(userIds.map(id => userLoader.load(id)));
127
+ ```
128
+
129
+ ## When N+1 is actually fine (rare)
130
+
131
+ - N is bounded and tiny (`n <= 5` forever, e.g. enum lookup).
132
+ - The "loop" is async-streaming over an unbounded source (e.g. event-by-event processing where batching would break correctness).
133
+ - Each call is to a different sharded service and cannot be batched.
134
+
135
+ In all these cases, **leave a comment** stating why this is intentional, otherwise the next pass will "fix" it back into an N+1.
136
+
137
+ ## Quick mental test
138
+
139
+ Read the code aloud: "for each user, fetch its posts". If you say "for each X, fetch Y" — it's N+1. Rephrase as: "fetch all posts for these users."
@@ -0,0 +1,38 @@
1
+ ---
2
+ id: cpp-map-double-lookup
3
+ title: map.count(k) then map[k] — two lookups; use find()
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: cpp
8
+ tags: cpp, info, complexity-cuts
9
+ ---
10
+
11
+ # map.count(k) then map[k] — two lookups; use find()
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ Use `auto it = m.find(k); if (it != m.end()) use(it->second);` — one lookup.
18
+
19
+ ## Incorrect
20
+
21
+ ```cpp
22
+ if (m.count(k)) {
23
+ use(m[k]); // second lookup
24
+ }
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```cpp
30
+ auto it = m.find(k);
31
+ if (it != m.end()) {
32
+ use(it->second);
33
+ }
34
+ ```
35
+
36
+ ## Escalate to
37
+
38
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,33 @@
1
+ ---
2
+ id: cpp-range-loop-copy
3
+ title: Range-for `auto x` copies each element — use `const auto&` for non-trivial types
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: cpp
8
+ tags: cpp, info
9
+ ---
10
+
11
+ # Range-for `auto x` copies each element — use `const auto&` for non-trivial types
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ Prefer `for (const auto& x : container)` unless you intentionally need a copy.
18
+
19
+ ## Incorrect
20
+
21
+ ```cpp
22
+ for (auto s : large_strings) {
23
+ process(s); // s is a fresh copy each iteration
24
+ }
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```cpp
30
+ for (const auto& s : large_strings) {
31
+ process(s); // reference, no copy
32
+ }
33
+ ```
@@ -0,0 +1,36 @@
1
+ ---
2
+ id: cpp-raw-new
3
+ title: Raw `new` outside of smart-pointer ctor — manual delete, exception-unsafe
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: cpp
8
+ tags: cpp, warning, invariant-guard
9
+ ---
10
+
11
+ # Raw `new` outside of smart-pointer ctor — manual delete, exception-unsafe
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Use `std::make_unique<T>(...)` or `std::make_shared<T>(...)`. Reserve raw `new` for placement-new or interop with C APIs.
18
+
19
+ ## Incorrect
20
+
21
+ ```cpp
22
+ Widget* w = new Widget(42);
23
+ configure(w); // if this throws, w leaks
24
+ delete w;
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```cpp
30
+ auto w = std::make_unique<Widget>(42);
31
+ configure(w.get()); // destructs on every exit path, including throw
32
+ ```
33
+
34
+ ## Escalate to
35
+
36
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
@@ -0,0 +1,45 @@
1
+ ---
2
+ id: cpp-string-concat-in-loop
3
+ title: std::string += inside loop — O(n^2) without reserve()
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: cpp
8
+ tags: cpp, warning, complexity-cuts
9
+ ---
10
+
11
+ # std::string += inside loop — O(n^2) without reserve()
12
+
13
+ `std::string::operator+=` past capacity reallocates. Without a `reserve`, repeated concatenation is O(n²). `std::ostringstream` or one `reserve` fixes it.
14
+
15
+ ## Fix
16
+
17
+ Call `s.reserve(total)` once if size known, or accumulate into a `std::ostringstream` and call `.str()` at the end.
18
+
19
+ ## Incorrect
20
+
21
+ ```cpp
22
+ std::string s;
23
+ for (const auto& part : parts) {
24
+ s += part;
25
+ }
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```cpp
31
+ std::ostringstream os;
32
+ for (const auto& part : parts) {
33
+ os << part;
34
+ }
35
+ auto s = os.str();
36
+
37
+ // Or, if size known:
38
+ std::string s;
39
+ s.reserve(total_size);
40
+ for (const auto& part : parts) s += part;
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,40 @@
1
+ ---
2
+ id: cpp-vector-push-no-reserve
3
+ title: vector push_back in loop without reserve() — log-amortized reallocation
4
+ severity: info
5
+ impact: MEDIUM
6
+ impactDescription: Suboptimal; flag when n is large or on a hot path
7
+ language: cpp
8
+ tags: cpp, info, complexity-cuts
9
+ ---
10
+
11
+ # vector push_back in loop without reserve() — log-amortized reallocation
12
+
13
+ `std::vector::push_back` past the current capacity doubles the backing array and copies/moves every element. Calling `reserve(n)` first does one allocation.
14
+
15
+ ## Fix
16
+
17
+ Call `v.reserve(n)` before the loop when n is known.
18
+
19
+ ## Incorrect
20
+
21
+ ```cpp
22
+ std::vector<int> v;
23
+ for (int i = 0; i < n; ++i) {
24
+ v.push_back(compute(i)); // reallocates log(n) times
25
+ }
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```cpp
31
+ std::vector<int> v;
32
+ v.reserve(n);
33
+ for (int i = 0; i < n; ++i) {
34
+ v.push_back(compute(i));
35
+ }
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,45 @@
1
+ ---
2
+ id: cs-async-void
3
+ title: async void — exceptions are unobserved and crash the process
4
+ severity: error
5
+ impact: CRITICAL
6
+ impactDescription: Will break or scale-fail in production
7
+ language: csharp
8
+ tags: csharp, error, invariant-guard
9
+ ---
10
+
11
+ # async void — exceptions are unobserved and crash the process
12
+
13
+ `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`).
14
+
15
+ ## Fix
16
+
17
+ Return Task. async void is only acceptable for true event handlers (OnClick, etc.).
18
+
19
+ ## Incorrect
20
+
21
+ ```csharp
22
+ public async void DoWork() {
23
+ await Task.Delay(100);
24
+ throw new Exception("boom"); // crashes the process
25
+ }
26
+ ```
27
+
28
+ ## Correct
29
+
30
+ ```csharp
31
+ public async Task DoWork() {
32
+ await Task.Delay(100);
33
+ throw new Exception("boom"); // caller can await + catch
34
+ }
35
+
36
+ // Event handlers stay async void with a try/catch at the boundary:
37
+ private async void OnClick(object s, EventArgs e) {
38
+ try { await DoWorkAsync(); }
39
+ catch (Exception ex) { _log.LogError(ex, "OnClick failed"); }
40
+ }
41
+ ```
42
+
43
+ ## Escalate to
44
+
45
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
@@ -0,0 +1,32 @@
1
+ ---
2
+ id: cs-disposable-no-using
3
+ title: IDisposable allocated without using — leak on exception
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: csharp
8
+ tags: csharp, warning
9
+ ---
10
+
11
+ # IDisposable allocated without using — leak on exception
12
+
13
+ `IDisposable` resources allocated without `using` leak if an exception fires before `Dispose()`. `using var` ensures cleanup even on throw.
14
+
15
+ ## Fix
16
+
17
+ Wrap in `using var x = new ...;` or `using (var x = ...) { }`.
18
+
19
+ ## Incorrect
20
+
21
+ ```csharp
22
+ var stream = new FileStream(path, FileMode.Open);
23
+ var data = ReadAll(stream); // if this throws, stream is never disposed
24
+ stream.Dispose();
25
+ ```
26
+
27
+ ## Correct
28
+
29
+ ```csharp
30
+ using var stream = new FileStream(path, FileMode.Open);
31
+ var data = ReadAll(stream); // disposed on every exit path
32
+ ```
@@ -0,0 +1,36 @@
1
+ ---
2
+ id: cs-list-contains-in-loop
3
+ title: List.Contains inside LINQ/loop — O(n*m); use HashSet<T>
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: csharp
8
+ tags: csharp, warning, complexity-cuts
9
+ ---
10
+
11
+ # List.Contains inside LINQ/loop — O(n*m); use HashSet<T>
12
+
13
+ `List<T>.Contains` is O(n). Inside LINQ over m items it is O(n·m). `HashSet<T>` is O(1).
14
+
15
+ ## Fix
16
+
17
+ Convert to HashSet once: `var s = new HashSet<T>(list); s.Contains(x);`
18
+
19
+ ## Incorrect
20
+
21
+ ```csharp
22
+ // O(n·m)
23
+ var active = users.Where(u => !banned.Contains(u.Id)).ToList();
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```csharp
29
+ // O(n+m)
30
+ var bannedSet = new HashSet<int>(banned);
31
+ var active = users.Where(u => !bannedSet.Contains(u.Id)).ToList();
32
+ ```
33
+
34
+ ## Escalate to
35
+
36
+ If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
@@ -0,0 +1,42 @@
1
+ ---
2
+ id: cs-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: csharp
8
+ tags: csharp, warning, complexity-cuts
9
+ ---
10
+
11
+ # string += inside loop — O(n^2) on immutable string
12
+
13
+ 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.
14
+
15
+ ## Fix
16
+
17
+ Use StringBuilder: `var sb = new StringBuilder(); foreach (...) sb.Append(x); sb.ToString();`
18
+
19
+ ## Incorrect
20
+
21
+ ```csharp
22
+ // O(n²)
23
+ string s = "";
24
+ foreach (var line in lines) {
25
+ s += line + "\n";
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```csharp
32
+ // O(n)
33
+ var sb = new StringBuilder(lines.Count * 80);
34
+ foreach (var line in lines) {
35
+ sb.Append(line).Append('\n');
36
+ }
37
+ var 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,39 @@
1
+ ---
2
+ id: go-defer-in-loop
3
+ title: defer inside loop — defers accumulate until function returns
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: go
8
+ tags: go, warning
9
+ ---
10
+
11
+ # defer inside loop — defers accumulate until function returns
12
+
13
+ `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.
14
+
15
+ ## Fix
16
+
17
+ Wrap the body in a function so each iteration's defer fires at iteration end, or close resources explicitly.
18
+
19
+ ## Incorrect
20
+
21
+ ```go
22
+ for _, f := range files {
23
+ fp, _ := os.Open(f)
24
+ defer fp.Close() // accumulates; nothing closes until the outer function returns
25
+ process(fp)
26
+ }
27
+ ```
28
+
29
+ ## Correct
30
+
31
+ ```go
32
+ for _, f := range files {
33
+ func() {
34
+ fp, _ := os.Open(f)
35
+ defer fp.Close() // closes at end of this iteration
36
+ process(fp)
37
+ }()
38
+ }
39
+ ```
@@ -0,0 +1,38 @@
1
+ ---
2
+ id: go-err-not-checked
3
+ title: Error return value discarded — silent failures
4
+ severity: warning
5
+ impact: HIGH
6
+ impactDescription: Hot-path or correctness risk at realistic n
7
+ language: go
8
+ tags: go, warning, invariant-guard
9
+ ---
10
+
11
+ # Error return value discarded — silent failures
12
+
13
+ Discarding the error return with `_` is silent failure. The function looks like it succeeded; downstream code sees zero values and behaves unpredictably.
14
+
15
+ ## Fix
16
+
17
+ Handle the error or comment why it is safe to ignore (`_ = err // <reason>`).
18
+
19
+ ## Incorrect
20
+
21
+ ```go
22
+ data, _ := os.ReadFile(path)
23
+ return parse(data) // parse on empty buffer if file missing
24
+ ```
25
+
26
+ ## Correct
27
+
28
+ ```go
29
+ data, err := os.ReadFile(path)
30
+ if err != nil {
31
+ return nil, fmt.Errorf("read %s: %w", path, err)
32
+ }
33
+ return parse(data), nil
34
+ ```
35
+
36
+ ## Escalate to
37
+
38
+ If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.