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.
- package/LICENSE +201 -0
- package/README.md +238 -0
- package/cli/gen-agents-md.js +60 -0
- package/cli/gen-rule-docs.js +885 -0
- package/cli/lemmaly.js +162 -0
- package/commands/benchmark.md +40 -0
- package/commands/budget.md +53 -0
- package/commands/complexity.md +26 -0
- package/commands/cut.md +27 -0
- package/commands/hotpath.md +22 -0
- package/commands/invariant.md +22 -0
- package/commands/n-plus-one.md +20 -0
- package/commands/profile.md +34 -0
- package/commands/regress.md +43 -0
- package/commands/scale-check.md +37 -0
- package/commands/ship-check.md +26 -0
- package/package.json +48 -0
- package/rules/cpp.json +46 -0
- package/rules/csharp.json +38 -0
- package/rules/go.json +46 -0
- package/rules/java.json +38 -0
- package/rules/javascript.json +102 -0
- package/rules/php.json +38 -0
- package/rules/python.json +62 -0
- package/rules/ruby.json +38 -0
- package/rules/rust.json +38 -0
- package/rules/shell.json +38 -0
- package/rules/sql.json +54 -0
- package/skills/complexity-cuts/SKILL.md +259 -0
- package/skills/invariant-guard/SKILL.md +310 -0
- package/skills/lemmaly/AGENTS.md +1869 -0
- package/skills/lemmaly/SKILL.md +365 -0
- package/skills/lemmaly/references/async.md +135 -0
- package/skills/lemmaly/references/complexity.md +66 -0
- package/skills/lemmaly/references/hot-paths.md +87 -0
- package/skills/lemmaly/references/memory.md +118 -0
- package/skills/lemmaly/references/n-plus-one.md +139 -0
- package/skills/lemmaly/rules/cpp-map-double-lookup.md +38 -0
- package/skills/lemmaly/rules/cpp-range-loop-copy.md +33 -0
- package/skills/lemmaly/rules/cpp-raw-new.md +36 -0
- package/skills/lemmaly/rules/cpp-string-concat-in-loop.md +45 -0
- package/skills/lemmaly/rules/cpp-vector-push-no-reserve.md +40 -0
- package/skills/lemmaly/rules/cs-async-void.md +45 -0
- package/skills/lemmaly/rules/cs-disposable-no-using.md +32 -0
- package/skills/lemmaly/rules/cs-list-contains-in-loop.md +36 -0
- package/skills/lemmaly/rules/cs-string-concat-in-loop.md +42 -0
- package/skills/lemmaly/rules/go-defer-in-loop.md +39 -0
- package/skills/lemmaly/rules/go-err-not-checked.md +38 -0
- package/skills/lemmaly/rules/go-loop-var-capture.md +47 -0
- package/skills/lemmaly/rules/go-slice-append-no-cap.md +39 -0
- package/skills/lemmaly/rules/go-string-concat-in-loop.md +44 -0
- package/skills/lemmaly/rules/java-arraylist-remove-in-for-i.md +44 -0
- package/skills/lemmaly/rules/java-bare-catch-exception.md +42 -0
- package/skills/lemmaly/rules/java-list-contains-in-loop.md +40 -0
- package/skills/lemmaly/rules/java-string-concat-in-loop.md +42 -0
- package/skills/lemmaly/rules/js-anonymous-handler-jsx.md +31 -0
- package/skills/lemmaly/rules/js-array-key-index.md +29 -0
- package/skills/lemmaly/rules/js-async-in-foreach.md +43 -0
- package/skills/lemmaly/rules/js-await-in-for-loop.md +41 -0
- package/skills/lemmaly/rules/js-deep-clone-via-json.md +33 -0
- package/skills/lemmaly/rules/js-helper-call-in-iterator.md +41 -0
- package/skills/lemmaly/rules/js-includes-in-iterator.md +37 -0
- package/skills/lemmaly/rules/js-inline-object-jsx-prop.md +35 -0
- package/skills/lemmaly/rules/js-nested-for-loops.md +45 -0
- package/skills/lemmaly/rules/js-spread-in-reduce.md +38 -0
- package/skills/lemmaly/rules/js-unique-via-indexof.md +35 -0
- package/skills/lemmaly/rules/js-useeffect-missing-deps.md +33 -0
- package/skills/lemmaly/rules/php-count-in-for-condition.md +45 -0
- package/skills/lemmaly/rules/php-in-array-in-loop.md +42 -0
- package/skills/lemmaly/rules/php-loose-equality.md +35 -0
- package/skills/lemmaly/rules/php-query-in-loop.md +47 -0
- package/skills/lemmaly/rules/py-bare-except.md +39 -0
- package/skills/lemmaly/rules/py-django-loop-without-eager.md +42 -0
- package/skills/lemmaly/rules/py-in-list-literal.md +37 -0
- package/skills/lemmaly/rules/py-mutable-default-arg.md +39 -0
- package/skills/lemmaly/rules/py-open-without-with.md +33 -0
- package/skills/lemmaly/rules/py-range-len.md +35 -0
- package/skills/lemmaly/rules/py-string-concat-in-loop.md +43 -0
- package/skills/lemmaly/rules/rb-bare-rescue.md +41 -0
- package/skills/lemmaly/rules/rb-include-in-iterator.md +37 -0
- package/skills/lemmaly/rules/rb-n-plus-one-activerecord.md +39 -0
- package/skills/lemmaly/rules/rb-string-concat-in-loop.md +39 -0
- package/skills/lemmaly/rules/rs-clone-in-loop.md +38 -0
- package/skills/lemmaly/rules/rs-string-push-no-capacity.md +43 -0
- package/skills/lemmaly/rules/rs-unwrap-in-prod.md +36 -0
- package/skills/lemmaly/rules/rs-vec-push-no-capacity.md +42 -0
- package/skills/lemmaly/rules/sh-for-ls.md +41 -0
- package/skills/lemmaly/rules/sh-set-e-no-pipefail.md +37 -0
- package/skills/lemmaly/rules/sh-unquoted-var.md +35 -0
- package/skills/lemmaly/rules/sh-useless-cat-pipe.md +32 -0
- package/skills/lemmaly/rules/sql-leading-wildcard-like.md +34 -0
- package/skills/lemmaly/rules/sql-not-in-subquery.md +38 -0
- package/skills/lemmaly/rules/sql-or-in-where.md +35 -0
- package/skills/lemmaly/rules/sql-select-no-limit.md +37 -0
- package/skills/lemmaly/rules/sql-select-star.md +29 -0
- package/skills/lemmaly/rules/sql-update-no-where.md +35 -0
- package/skills/mathguard/SKILL.md +277 -0
package/cli/lemmaly.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const RULES_DIR = path.join(__dirname, '..', 'rules');
|
|
8
|
+
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.turbo', 'coverage', '__pycache__', '.venv', 'venv']);
|
|
9
|
+
|
|
10
|
+
const COLOR = {
|
|
11
|
+
error: '\x1b[31m',
|
|
12
|
+
warning: '\x1b[33m',
|
|
13
|
+
info: '\x1b[36m',
|
|
14
|
+
dim: '\x1b[2m',
|
|
15
|
+
reset: '\x1b[0m',
|
|
16
|
+
bold: '\x1b[1m',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function loadRules() {
|
|
20
|
+
const byExt = new Map();
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
for (const f of fs.readdirSync(RULES_DIR)) {
|
|
23
|
+
if (!f.endsWith('.json')) continue;
|
|
24
|
+
const data = JSON.parse(fs.readFileSync(path.join(RULES_DIR, f), 'utf8'));
|
|
25
|
+
for (const ext of data.extensions) {
|
|
26
|
+
const list = byExt.get(ext) || [];
|
|
27
|
+
list.push(...data.rules);
|
|
28
|
+
byExt.set(ext, list);
|
|
29
|
+
}
|
|
30
|
+
for (const r of data.rules) seen.add(r.id);
|
|
31
|
+
}
|
|
32
|
+
return { byExt, total: seen.size };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function walk(target, out = []) {
|
|
36
|
+
const stat = fs.statSync(target);
|
|
37
|
+
if (stat.isFile()) { out.push(target); return out; }
|
|
38
|
+
for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
|
|
39
|
+
if (entry.name.startsWith('.') && entry.name !== '.github') continue;
|
|
40
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
41
|
+
const p = path.join(target, entry.name);
|
|
42
|
+
if (entry.isDirectory()) walk(p, out);
|
|
43
|
+
else out.push(p);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function lineOf(content, idx) {
|
|
49
|
+
let line = 1;
|
|
50
|
+
for (let i = 0; i < idx; i++) if (content.charCodeAt(i) === 10) line++;
|
|
51
|
+
return line;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function scanFile(file, byExt) {
|
|
55
|
+
const ext = path.extname(file).toLowerCase();
|
|
56
|
+
const rules = byExt.get(ext);
|
|
57
|
+
if (!rules || rules.length === 0) return [];
|
|
58
|
+
let content;
|
|
59
|
+
try { content = fs.readFileSync(file, 'utf8'); } catch { return []; }
|
|
60
|
+
const findings = [];
|
|
61
|
+
for (const rule of rules) {
|
|
62
|
+
let re;
|
|
63
|
+
try {
|
|
64
|
+
const flags = rule.flags || 'g';
|
|
65
|
+
re = new RegExp(rule.pattern, flags.includes('g') ? flags : flags + 'g');
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error(`${COLOR.warning}rule ${rule.id} has invalid regex: ${e.message}${COLOR.reset}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
for (const m of content.matchAll(re)) {
|
|
71
|
+
findings.push({ file, line: lineOf(content, m.index), rule });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return findings;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printFindings(findings, rel) {
|
|
78
|
+
const counts = { error: 0, warning: 0, info: 0 };
|
|
79
|
+
for (const f of findings) counts[f.rule.severity] = (counts[f.rule.severity] || 0) + 1;
|
|
80
|
+
|
|
81
|
+
const order = { error: 0, warning: 1, info: 2 };
|
|
82
|
+
findings.sort((a, b) => {
|
|
83
|
+
const s = order[a.rule.severity] - order[b.rule.severity];
|
|
84
|
+
if (s !== 0) return s;
|
|
85
|
+
if (a.file !== b.file) return a.file.localeCompare(b.file);
|
|
86
|
+
return a.line - b.line;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
for (const f of findings) {
|
|
90
|
+
const c = COLOR[f.rule.severity] || '';
|
|
91
|
+
const loc = `${rel(f.file)}:${f.line}`;
|
|
92
|
+
console.log(`${c}${f.rule.severity.toUpperCase().padEnd(7)}${COLOR.reset} ${loc} ${COLOR.bold}${f.rule.id}${COLOR.reset}`);
|
|
93
|
+
console.log(` ${f.rule.title}`);
|
|
94
|
+
if (f.rule.fix) console.log(` ${COLOR.dim}-> ${f.rule.fix}${COLOR.reset}`);
|
|
95
|
+
}
|
|
96
|
+
return counts;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function main() {
|
|
100
|
+
const argv = process.argv.slice(2);
|
|
101
|
+
const cmd = argv[0];
|
|
102
|
+
|
|
103
|
+
if (!cmd || cmd === '-h' || cmd === '--help') {
|
|
104
|
+
console.log(`lemmaly — algorithmic anti-pattern scanner
|
|
105
|
+
|
|
106
|
+
Usage:
|
|
107
|
+
lemmaly scan <path> Scan a file or directory.
|
|
108
|
+
lemmaly rules List all loaded rules.
|
|
109
|
+
lemmaly --help Show this message.
|
|
110
|
+
|
|
111
|
+
Exit codes:
|
|
112
|
+
0 no errors
|
|
113
|
+
1 one or more 'error'-severity findings`);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { byExt, total } = loadRules();
|
|
118
|
+
|
|
119
|
+
if (cmd === 'rules') {
|
|
120
|
+
const printed = new Set();
|
|
121
|
+
for (const list of byExt.values()) {
|
|
122
|
+
for (const r of list) {
|
|
123
|
+
if (printed.has(r.id)) continue;
|
|
124
|
+
printed.add(r.id);
|
|
125
|
+
const c = COLOR[r.severity] || '';
|
|
126
|
+
console.log(`${c}${r.severity.padEnd(7)}${COLOR.reset} ${r.id.padEnd(34)} ${r.title}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
console.log(`\n${printed.size} rules loaded.`);
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (cmd !== 'scan') {
|
|
134
|
+
console.error(`unknown command: ${cmd}`);
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const target = path.resolve(argv[1] || '.');
|
|
139
|
+
if (!fs.existsSync(target)) {
|
|
140
|
+
console.error(`path not found: ${target}`);
|
|
141
|
+
process.exit(2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const files = walk(target);
|
|
145
|
+
const findings = [];
|
|
146
|
+
for (const f of files) findings.push(...scanFile(f, byExt));
|
|
147
|
+
|
|
148
|
+
const rel = (p) => path.relative(process.cwd(), p) || p;
|
|
149
|
+
const counts = printFindings(findings, rel);
|
|
150
|
+
|
|
151
|
+
const totalFindings = (counts.error || 0) + (counts.warning || 0) + (counts.info || 0);
|
|
152
|
+
console.log('');
|
|
153
|
+
if (totalFindings === 0) {
|
|
154
|
+
console.log(`${COLOR.info}clean${COLOR.reset} — scanned ${files.length} files against ${total} rules.`);
|
|
155
|
+
} else {
|
|
156
|
+
const e = counts.error || 0, w = counts.warning || 0, i = counts.info || 0;
|
|
157
|
+
console.log(`${COLOR.error}${e} errors${COLOR.reset}, ${COLOR.warning}${w} warnings${COLOR.reset}, ${COLOR.info}${i} info${COLOR.reset} across ${files.length} files (${total} rules).`);
|
|
158
|
+
}
|
|
159
|
+
process.exit((counts.error || 0) > 0 ? 1 : 0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
main();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Write and run a micro-benchmark that produces a real, reproducible number for a code path.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /benchmark <path>
|
|
6
|
+
|
|
7
|
+
No number without measurement. This command produces the number.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Identify the unit under test.** A function, an endpoint, a query. Smaller is better.
|
|
12
|
+
2. **Pick a runner.**
|
|
13
|
+
- JS/TS: `tinybench` or hand-rolled `performance.now()` with warmup.
|
|
14
|
+
- Python: `pytest-benchmark` or `timeit` with N runs.
|
|
15
|
+
- SQL: `EXPLAIN ANALYZE` against a realistic dataset.
|
|
16
|
+
3. **Generate a representative fixture.** Size, distribution, and shape matching production. State the `n`.
|
|
17
|
+
4. **Warm up.** At least 10 throwaway iterations before measuring (JIT, cache).
|
|
18
|
+
5. **Run >= 30 iterations.** Report median + p95, not mean.
|
|
19
|
+
6. **State the environment.** Node/Python version, machine, whether other load was running.
|
|
20
|
+
|
|
21
|
+
## Output format
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
benchmark: getUserFeed
|
|
25
|
+
fixture: n=10,000 posts, 50 follows, warm DB
|
|
26
|
+
runs: 100
|
|
27
|
+
median: 142 ms
|
|
28
|
+
p95: 189 ms
|
|
29
|
+
env: Node 22.4, M3 Pro, idle
|
|
30
|
+
|
|
31
|
+
variant A (sequential) median 142 ms / p95 189 ms
|
|
32
|
+
variant B (Promise.all) median 47 ms / p95 63 ms ← 3.0x median
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Forbidden
|
|
36
|
+
|
|
37
|
+
- Reporting a single run.
|
|
38
|
+
- Reporting mean without p95 (means lie about tails).
|
|
39
|
+
- Comparing variants on different fixtures or different machines.
|
|
40
|
+
- Rounding to make a number prettier.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Define a measurable performance budget for a surface so future changes can be checked against it.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /budget <surface>
|
|
6
|
+
|
|
7
|
+
Without a budget, "fast enough" is a vibe. With a budget, it's a checkable claim.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Pick the surface.** A page, an endpoint, a job. One at a time.
|
|
12
|
+
2. **Pick the right metrics for it.**
|
|
13
|
+
|
|
14
|
+
| Surface | Metrics |
|
|
15
|
+
|---------|---------|
|
|
16
|
+
| Page (web) | LCP, INP, CLS, JS bundle KB, # of requests |
|
|
17
|
+
| API endpoint | p50, p95, p99 latency under target RPS; allocations/request |
|
|
18
|
+
| Background job | wall-clock per batch; memory peak; rows/sec |
|
|
19
|
+
| Database query | EXPLAIN cost; rows examined; whether an index is used |
|
|
20
|
+
|
|
21
|
+
3. **Set numeric thresholds.** Tied to user-facing target.
|
|
22
|
+
- "INP < 200ms p95 at 4G throttling."
|
|
23
|
+
- "POST /orders < 250ms p99 at 200 RPS warm."
|
|
24
|
+
4. **Write the budget into a file** at `perf-budgets/<surface>.md` so `/ship-check` can read it. Include date and rationale.
|
|
25
|
+
|
|
26
|
+
## Output template
|
|
27
|
+
|
|
28
|
+
```markdown
|
|
29
|
+
# Budget: POST /api/orders
|
|
30
|
+
Set: 2026-05-22
|
|
31
|
+
Owner: morse
|
|
32
|
+
|
|
33
|
+
Target users: 95% should see < 250ms p99 at peak (200 RPS).
|
|
34
|
+
|
|
35
|
+
Budget:
|
|
36
|
+
latency p50 <= 80ms
|
|
37
|
+
latency p95 <= 180ms
|
|
38
|
+
latency p99 <= 250ms
|
|
39
|
+
DB queries per request <= 4
|
|
40
|
+
allocations per request <= 50 KB
|
|
41
|
+
|
|
42
|
+
Rationale:
|
|
43
|
+
Peak measured at 180 RPS, growing 30%/quarter. Headroom for 2x.
|
|
44
|
+
|
|
45
|
+
Measured today (median of 100 runs):
|
|
46
|
+
p50 62ms, p95 140ms, p99 210ms — within budget.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Reject
|
|
50
|
+
|
|
51
|
+
- Budgets stated as percentages ("30% faster") without absolute numbers.
|
|
52
|
+
- Budgets without a measurement method.
|
|
53
|
+
- Budgets set without acknowledging current measured baseline.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Declare time and space complexity before writing the body. Force the cost into comments first.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /complexity <signature>
|
|
6
|
+
|
|
7
|
+
State the cost before the code exists. No "should be O(log n)" without a derivation.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Read the signature.** Identify the input that drives `n`. If there are several, name them (`n` users × `m` events).
|
|
12
|
+
2. **Derive time complexity.** Walk every loop, recursion, and library call. Sum the dominant term.
|
|
13
|
+
3. **Derive space complexity.** Count allocations that scale with `n`. Constant-size temporaries don't count.
|
|
14
|
+
4. **Name the data structure.** Set, Map, heap, deque — declare what shape the data sits in before the loop.
|
|
15
|
+
5. **State the loop invariant.** One sentence: what stays true on every iteration.
|
|
16
|
+
6. **Write three header comments** at the top of the body:
|
|
17
|
+
- `// Time: O(?) Space: O(?)`
|
|
18
|
+
- `// Structure: <name> for <reason>`
|
|
19
|
+
- `// Invariant: <what stays true>`
|
|
20
|
+
7. **Only then write the body.**
|
|
21
|
+
|
|
22
|
+
## Forbidden
|
|
23
|
+
|
|
24
|
+
- Writing the body first and back-filling the comments.
|
|
25
|
+
- "Should be O(log n)" without naming the recurrence.
|
|
26
|
+
- Choosing a structure by reflex (`Array.includes` for membership, nested `for` for joins).
|
package/commands/cut.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Apply the corrective playbook to slow code. Match the pattern, lower the dominant term, re-derive cost.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /cut <function-or-path>
|
|
6
|
+
|
|
7
|
+
Cut the dominant term. One change. Re-derive the cost. Keep the output identical.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Read the current code.** State current time and space complexity.
|
|
12
|
+
2. **Match the anti-pattern.** Common shapes:
|
|
13
|
+
- Nested scan over two collections → **index the inner sequence, scan the outer once.**
|
|
14
|
+
- `Array.includes` / `indexOf` inside a loop → **Set/Map for O(1) lookup.**
|
|
15
|
+
- Repeated query inside a loop → **batched `IN (...)` or a join.**
|
|
16
|
+
- Recursive scan with repeated subproblems → **memoize or convert to iterative DP.**
|
|
17
|
+
- Sort inside a hot path → **sort once, reuse.**
|
|
18
|
+
3. **Apply the playbook.** One change. Don't bundle. Don't "while I was in there."
|
|
19
|
+
4. **Re-derive cost.** Write `// Was: O(?) · Now: O(?)` as a header comment.
|
|
20
|
+
5. **Prove same output.** Run the existing tests, or write one if none exists.
|
|
21
|
+
6. **Hand to `/benchmark`** to confirm the win is real before claiming it.
|
|
22
|
+
|
|
23
|
+
## Forbidden
|
|
24
|
+
|
|
25
|
+
- Speculative micro-optimizations that don't move the dominant term.
|
|
26
|
+
- Changing the function's contract while "fixing" it.
|
|
27
|
+
- Claiming the cut worked without a measurement.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Drill into a single hot path and propose a concrete optimization with measured before/after.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /hotpath <name-or-path>
|
|
6
|
+
|
|
7
|
+
Focus on one path. Don't drift.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Read the path end-to-end.** Trace every I/O, allocation, and loop.
|
|
12
|
+
2. **State current complexity.** Time and space, in terms of the path's `n`.
|
|
13
|
+
3. **State the bottleneck in one sentence.** "Dominated by N sequential DB queries inside the items loop."
|
|
14
|
+
4. **Propose ONE change.** The smallest change that moves the dominant term. No bundles.
|
|
15
|
+
5. **Write a benchmark.** Use `/benchmark` to capture before/after with the same input fixture.
|
|
16
|
+
6. **Report measured delta.** Refuse to claim improvement without numbers.
|
|
17
|
+
|
|
18
|
+
## Forbidden
|
|
19
|
+
|
|
20
|
+
- Speculative micro-optimizations after the dominant term is fixed.
|
|
21
|
+
- "While I was in there" refactors.
|
|
22
|
+
- "This is faster" without a benchmark.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: State loop invariant, base case, and termination measure before writing the body of a loop or recursion.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /invariant <function-or-loop>
|
|
6
|
+
|
|
7
|
+
Prove the loop is correct on paper before running a single test.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Identify the iterative structure.** A loop, a recursion, or a fold.
|
|
12
|
+
2. **State the invariant.** One sentence describing a property that holds **before each iteration**. Example: "`seen` contains every element of `xs[0..i)`."
|
|
13
|
+
3. **State the base case.** What is true at iteration zero, or at the recursion's bottom. Cover empty input, single-element input, and unreachable branches.
|
|
14
|
+
4. **State the termination measure.** A non-negative integer that strictly decreases each iteration. Example: "`hi - lo` decreases on every step."
|
|
15
|
+
5. **Write all three as comments above the body.**
|
|
16
|
+
6. **Sanity-check the boundary.** Walk the last iteration by hand. Off-by-one errors die here.
|
|
17
|
+
|
|
18
|
+
## Forbidden
|
|
19
|
+
|
|
20
|
+
- Recursion without a stated base case.
|
|
21
|
+
- "Obviously terminates" — name the measure.
|
|
22
|
+
- Skipping the empty-input case because "it won't happen in practice."
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Find N+1 query / I/O patterns in the current path or repo, ranked by severity.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /n-plus-one
|
|
6
|
+
|
|
7
|
+
Find loops that issue one DB / HTTP / FS call per item. Load `references/n-plus-one.md` before reporting.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Scan for I/O-in-loop patterns.** `for`/`forEach`/`map`/`while` with any of: `await db.`, `await fetch`, `await prisma.`, `.objects.get`, `session.query`, `User.find`, `axios.`, `fs.readFile`.
|
|
12
|
+
2. **Classify each hit:**
|
|
13
|
+
- **Confirmed N+1** — issues one round-trip per iteration.
|
|
14
|
+
- **Hidden N+1** — `Promise.all(items.map(fetchOne))` still hits the backend N times.
|
|
15
|
+
- **False positive** — bounded `n <= 5`, or different backend per call, or comment explains why.
|
|
16
|
+
3. **Output:** file:line — pattern — proposed fix (eager-load / IN-batch / DataLoader).
|
|
17
|
+
|
|
18
|
+
## Refuse to "fix" inline
|
|
19
|
+
|
|
20
|
+
Don't rewrite code in this command. Report. Let the user pick which to fix with `/hotpath`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Locate the hot paths in this code/repo and rank them by expected impact.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /profile
|
|
6
|
+
|
|
7
|
+
Find what runs most, scales worst, and matters most. Do not optimize — only locate.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Identify entry points.** HTTP routes, queue handlers, cron jobs, React page components, CLI commands.
|
|
12
|
+
2. **For each entry point, classify:**
|
|
13
|
+
- Frequency: per request? per render? per frame? per startup?
|
|
14
|
+
- `n` it iterates over (ask the user if unknown — do not guess).
|
|
15
|
+
- I/O fan-out (DB queries, HTTP calls, file reads).
|
|
16
|
+
3. **Rank by `frequency * n * cost-per-op`.** Output a ranked table.
|
|
17
|
+
4. **Output, don't fix.** End with: "Use `/hotpath <name>` to drill into one."
|
|
18
|
+
|
|
19
|
+
## Output format
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
HOT PATHS (ranked)
|
|
23
|
+
|
|
24
|
+
#1 POST /api/orders.create
|
|
25
|
+
frequency: per request, ~2k/min peak
|
|
26
|
+
n: line_items (median 3, p99 40)
|
|
27
|
+
fan-out: 1 user lookup + 1 insert per item + 1 inventory check per item ← N+1 risk
|
|
28
|
+
hypothesis: O(n) DB roundtrips, dominated by network
|
|
29
|
+
|
|
30
|
+
#2 <UserListPage>
|
|
31
|
+
...
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Refuse to start optimizing in this command. Optimization is `/hotpath`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Diff performance characteristics of a path before and after a change. Flag regressions.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /regress <path>
|
|
6
|
+
|
|
7
|
+
A regression is a measured slowdown vs a baseline. This command produces the diff.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Identify the baseline.** A git ref (commit, branch, tag) where the path was acceptable.
|
|
12
|
+
2. **Run `/benchmark` on baseline.** Capture median + p95 with a stated fixture.
|
|
13
|
+
3. **Run `/benchmark` on HEAD.** Same fixture. Same machine. Same load.
|
|
14
|
+
4. **Compute deltas.**
|
|
15
|
+
- Absolute: `HEAD - baseline` in ms.
|
|
16
|
+
- Relative: `(HEAD - baseline) / baseline` as a percentage.
|
|
17
|
+
5. **Decide.**
|
|
18
|
+
- Slower by > 10% or > budget threshold → regression. Block.
|
|
19
|
+
- Slower within noise (< 5%) → flag, allow.
|
|
20
|
+
- Faster → record the new number as the next baseline.
|
|
21
|
+
|
|
22
|
+
## Output format
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
path: GET /api/users/:id
|
|
26
|
+
fixture: warm DB, n_friends=200, n_posts=500
|
|
27
|
+
runs: 100 each
|
|
28
|
+
|
|
29
|
+
baseline HEAD delta
|
|
30
|
+
median 28 ms 41 ms +13 ms (+46%) ← REGRESSION
|
|
31
|
+
p95 42 ms 78 ms +36 ms (+86%) ← REGRESSION
|
|
32
|
+
DB queries 3 14 ← N+1 introduced
|
|
33
|
+
|
|
34
|
+
Likely cause:
|
|
35
|
+
commit abc1234 added per-friend profile fetch.
|
|
36
|
+
See user-service.ts:84 — `for (const f of friends) await getProfile(f.id)`.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Forbidden
|
|
40
|
+
|
|
41
|
+
- Comparing runs across different machines or load conditions.
|
|
42
|
+
- Calling something "noise" without running enough iterations to establish the noise floor (>= 30 runs).
|
|
43
|
+
- "Acceptable regression" without an explicit user-visible justification.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Project this code to 10x its current scale and name what breaks first.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /scale-check
|
|
6
|
+
|
|
7
|
+
What works at today's `n` may not at 10x. This command names the first thing to break.
|
|
8
|
+
|
|
9
|
+
## Procedure
|
|
10
|
+
|
|
11
|
+
1. **Ask: "What is `n` today, and what's 10x?"** If the user doesn't know, refuse to speculate — ask them to estimate.
|
|
12
|
+
2. **For each operation in the path, compute cost at 10x.**
|
|
13
|
+
- O(n) at n=1k → 10ms turns into 100ms.
|
|
14
|
+
- O(n^2) at n=1k → seconds turn into minutes.
|
|
15
|
+
- O(n) DB roundtrips at 50 → 500 → connection pool exhausted, not just slower.
|
|
16
|
+
3. **Identify the first breaking constraint.**
|
|
17
|
+
- Latency budget exceeded?
|
|
18
|
+
- Memory pressure (RSS / heap)?
|
|
19
|
+
- DB connection pool / query timeout?
|
|
20
|
+
- Rate limit on an external service?
|
|
21
|
+
- Event-loop blocked > 200ms?
|
|
22
|
+
4. **Output: ONE thing to fix first.** Plus what becomes the next bottleneck after that.
|
|
23
|
+
|
|
24
|
+
## Output template
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
At 1x (n = 5k): total ~120ms, fine.
|
|
28
|
+
At 10x (n = 50k): total ~2.1s, REQUEST TIMEOUT.
|
|
29
|
+
|
|
30
|
+
First failure mode:
|
|
31
|
+
N+1 queries in items loop → connection pool exhausted at ~200 concurrent reqs.
|
|
32
|
+
|
|
33
|
+
Fix: batch with `WHERE id IN (...)`.
|
|
34
|
+
|
|
35
|
+
After the fix, next bottleneck:
|
|
36
|
+
JSON.stringify of full result on response — ~150ms blocking. Stream the response.
|
|
37
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Pre-deploy performance audit. Block the ship if a critical issue is unaddressed.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /ship-check
|
|
6
|
+
|
|
7
|
+
Run before merging or deploying changes that touch a hot path.
|
|
8
|
+
|
|
9
|
+
## Checklist (the model must answer each, on the record)
|
|
10
|
+
|
|
11
|
+
1. **Touched hot paths.** Which routes / components / queries changed?
|
|
12
|
+
2. **N+1 audit.** Did you scan changed files with `/n-plus-one`? Result?
|
|
13
|
+
3. **Complexity statement.** For each non-trivial new function: time + space, in terms of `n`. Pasted above the function.
|
|
14
|
+
4. **Bench numbers.** Any claim of "faster"/"same"/"acceptable" backed by a `/benchmark` run? Number pasted.
|
|
15
|
+
5. **Scale check.** What breaks first at 10x? (`/scale-check`)
|
|
16
|
+
6. **Memory.** New unbounded caches? New listeners without cleanup? New large buffers?
|
|
17
|
+
7. **Frontend specifically.** New inline objects/functions as memoized-child props? Effects with object deps? Lists > 200 items not virtualized?
|
|
18
|
+
8. **Budget.** Does this fit the `/budget` set for this surface?
|
|
19
|
+
|
|
20
|
+
## Severity gates
|
|
21
|
+
|
|
22
|
+
- **Block ship:** confirmed N+1 in a request handler, unbounded cache keyed by user input, sync block > 200ms on main thread, no benchmark for a performance-justified change.
|
|
23
|
+
- **Warn, ship allowed:** "fine at today's n, breaks at 10x" with a tracking issue filed.
|
|
24
|
+
- **OK:** all of the above answered and either green or accepted with a comment.
|
|
25
|
+
|
|
26
|
+
Refuse to give a green light if (4) is missing for any change pitched as a performance win.
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lemmaly",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Algorithm-first discipline layer that makes AI coding assistants think like a computer scientist before writing code. Catches O(n^2) loops, N+1 queries, missed base cases, brute-force solutions, and unjustified complexity claims. Pairs with mathguard, invariant-guard, and complexity-cuts.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"lemmaly": "cli/lemmaly.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"scan": "node cli/lemmaly.js scan .",
|
|
10
|
+
"test": "node test/run.js && node test/consistency.js && node tests/skill-triggering/run.js",
|
|
11
|
+
"test:rules": "node test/run.js",
|
|
12
|
+
"test:consistency": "node test/consistency.js",
|
|
13
|
+
"test:triggering": "node tests/skill-triggering/run.js",
|
|
14
|
+
"gen:rule-docs": "node cli/gen-rule-docs.js",
|
|
15
|
+
"gen:agents-md": "node cli/gen-agents-md.js",
|
|
16
|
+
"gen": "npm run gen:rule-docs && npm run gen:agents-md"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"algorithms",
|
|
20
|
+
"complexity",
|
|
21
|
+
"big-o",
|
|
22
|
+
"performance",
|
|
23
|
+
"claude",
|
|
24
|
+
"cursor",
|
|
25
|
+
"skill",
|
|
26
|
+
"linter",
|
|
27
|
+
"static-analysis",
|
|
28
|
+
"n-plus-one",
|
|
29
|
+
"computer-science"
|
|
30
|
+
],
|
|
31
|
+
"author": "Morse Chimwai",
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/morsechimwai/lemmaly"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"cli/",
|
|
42
|
+
"rules/",
|
|
43
|
+
"skills/",
|
|
44
|
+
"commands/",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
]
|
|
48
|
+
}
|
package/rules/cpp.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"language": "cpp",
|
|
3
|
+
"extensions": [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"],
|
|
4
|
+
"rules": [
|
|
5
|
+
{
|
|
6
|
+
"id": "cpp-vector-push-no-reserve",
|
|
7
|
+
"severity": "info",
|
|
8
|
+
"title": "vector push_back in loop without reserve() — log-amortized reallocation",
|
|
9
|
+
"pattern": "std::vector<[^>]+>\\s+(\\w+)\\s*;(?:(?!\\.reserve\\s*\\()[\\s\\S]){0,300}?\\bfor\\s*\\([^{]*\\)\\s*\\{[\\s\\S]{0,200}?\\1\\.push_back\\s*\\(",
|
|
10
|
+
"flags": "g",
|
|
11
|
+
"fix": "Call `v.reserve(n)` before the loop when n is known."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "cpp-string-concat-in-loop",
|
|
15
|
+
"severity": "warning",
|
|
16
|
+
"title": "std::string += inside loop — O(n^2) without reserve()",
|
|
17
|
+
"pattern": "std::string\\s+\\w+\\s*;[\\s\\S]{0,200}?\\bfor\\s*\\([^{]*\\)\\s*\\{[\\s\\S]{0,200}?\\w+\\s*\\+=\\s*",
|
|
18
|
+
"flags": "g",
|
|
19
|
+
"fix": "Call `s.reserve(total)` once if size known, or accumulate into a `std::ostringstream` and call `.str()` at the end."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "cpp-raw-new",
|
|
23
|
+
"severity": "warning",
|
|
24
|
+
"title": "Raw `new` outside of smart-pointer ctor — manual delete, exception-unsafe",
|
|
25
|
+
"pattern": "\\bnew\\s+[A-Za-z_][\\w:]*\\s*(?:\\(|\\[)",
|
|
26
|
+
"flags": "g",
|
|
27
|
+
"fix": "Use `std::make_unique<T>(...)` or `std::make_shared<T>(...)`. Reserve raw `new` for placement-new or interop with C APIs."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "cpp-range-loop-copy",
|
|
31
|
+
"severity": "info",
|
|
32
|
+
"title": "Range-for `auto x` copies each element — use `const auto&` for non-trivial types",
|
|
33
|
+
"pattern": "for\\s*\\(\\s*auto\\s+\\w+\\s*:\\s*\\w+\\s*\\)",
|
|
34
|
+
"flags": "g",
|
|
35
|
+
"fix": "Prefer `for (const auto& x : container)` unless you intentionally need a copy."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "cpp-map-double-lookup",
|
|
39
|
+
"severity": "info",
|
|
40
|
+
"title": "map.count(k) then map[k] — two lookups; use find()",
|
|
41
|
+
"pattern": "\\.count\\s*\\(\\s*(\\w+)\\s*\\)[\\s\\S]{0,80}?\\[\\s*\\1\\s*\\]",
|
|
42
|
+
"flags": "g",
|
|
43
|
+
"fix": "Use `auto it = m.find(k); if (it != m.end()) use(it->second);` — one lookup."
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"language": "csharp",
|
|
3
|
+
"extensions": [".cs"],
|
|
4
|
+
"rules": [
|
|
5
|
+
{
|
|
6
|
+
"id": "cs-string-concat-in-loop",
|
|
7
|
+
"severity": "warning",
|
|
8
|
+
"title": "string += inside loop — O(n^2) on immutable string",
|
|
9
|
+
"pattern": "\\bfor(?:each)?\\s*\\([^{]*\\)\\s*\\{[\\s\\S]{0,300}?\\b\\w+\\s*\\+=\\s*[\"'\\w]",
|
|
10
|
+
"flags": "g",
|
|
11
|
+
"fix": "Use StringBuilder: `var sb = new StringBuilder(); foreach (...) sb.Append(x); sb.ToString();`"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "cs-list-contains-in-loop",
|
|
15
|
+
"severity": "warning",
|
|
16
|
+
"title": "List.Contains inside LINQ/loop — O(n*m); use HashSet<T>",
|
|
17
|
+
"pattern": "\\.(Where|Select|Any|All|Count)\\s*\\([\\s\\S]{0,200}?\\.Contains\\s*\\(",
|
|
18
|
+
"flags": "g",
|
|
19
|
+
"fix": "Convert to HashSet once: `var s = new HashSet<T>(list); s.Contains(x);`"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "cs-async-void",
|
|
23
|
+
"severity": "error",
|
|
24
|
+
"title": "async void — exceptions are unobserved and crash the process",
|
|
25
|
+
"pattern": "\\b(?:public|private|protected|internal|static)\\s+(?:[a-zA-Z]+\\s+)*async\\s+void\\s+(?!On[A-Z])",
|
|
26
|
+
"flags": "g",
|
|
27
|
+
"fix": "Return Task. async void is only acceptable for true event handlers (OnClick, etc.)."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "cs-disposable-no-using",
|
|
31
|
+
"severity": "warning",
|
|
32
|
+
"title": "IDisposable allocated without using — leak on exception",
|
|
33
|
+
"pattern": "(?<!using\\s)(?<!using\\s+var\\s+\\w+\\s*=\\s*)\\bnew\\s+(?:FileStream|StreamReader|StreamWriter|SqlConnection|HttpClient|MemoryStream)\\s*\\(",
|
|
34
|
+
"flags": "g",
|
|
35
|
+
"fix": "Wrap in `using var x = new ...;` or `using (var x = ...) { }`."
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|