cursor-doctor 1.0.0 → 1.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/package.json +1 -1
- package/src/audit.js +122 -23
- package/src/autofix.js +171 -9
- package/src/cli.js +16 -4
- package/src/templates.js +488 -0
package/package.json
CHANGED
package/src/audit.js
CHANGED
|
@@ -62,12 +62,12 @@ function findConflicts(rules) {
|
|
|
62
62
|
|
|
63
63
|
if (!overlapping && !a.alwaysApply && !b.alwaysApply) continue;
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
const
|
|
67
|
-
const
|
|
65
|
+
// Extract directives from both rules
|
|
66
|
+
const aDirectives = extractDirectives(a.body);
|
|
67
|
+
const bDirectives = extractDirectives(b.body);
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
const contradictions =
|
|
69
|
+
// Find conflicting directives
|
|
70
|
+
const contradictions = findDirectiveConflicts(aDirectives, bDirectives);
|
|
71
71
|
if (contradictions.length > 0) {
|
|
72
72
|
conflicts.push({
|
|
73
73
|
fileA: a.file,
|
|
@@ -92,27 +92,126 @@ function globsOverlap(a, b) {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
function
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
function extractDirectives(text) {
|
|
96
|
+
const directives = [];
|
|
97
|
+
const lines = text.split('\n');
|
|
98
|
+
|
|
99
|
+
// Handle compound directives like "always use X" or "never use X"
|
|
100
|
+
const compoundPattern = /\b(always|never)\s+(use|avoid|prefer|include|exclude)\s+([^.\n]{3,50})/gi;
|
|
101
|
+
// Single directives like "use X" or "avoid X"
|
|
102
|
+
const singlePattern = /\b(use|prefer|avoid|don't|do not|no|remove|add|include|exclude|enable|disable)\s+([^.\n]{3,50})/gi;
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('<!--') || trimmed.length < 5) continue;
|
|
107
|
+
|
|
108
|
+
// Try compound pattern first
|
|
109
|
+
compoundPattern.lastIndex = 0;
|
|
110
|
+
let match = compoundPattern.exec(trimmed);
|
|
111
|
+
if (match) {
|
|
112
|
+
const modifier = match[1].toLowerCase(); // always/never
|
|
113
|
+
const action = match[2].toLowerCase(); // use/avoid/etc
|
|
114
|
+
const subject = normalizeSubject(match[3]);
|
|
115
|
+
if (subject) {
|
|
116
|
+
// Combine modifier and action: "always use" becomes "use", "never use" becomes "never"
|
|
117
|
+
const finalAction = modifier === 'never' ? 'never' : action;
|
|
118
|
+
directives.push({ action: finalAction, subject, line: trimmed });
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Try single pattern
|
|
124
|
+
singlePattern.lastIndex = 0;
|
|
125
|
+
match = singlePattern.exec(trimmed);
|
|
126
|
+
if (match) {
|
|
127
|
+
const action = match[1].toLowerCase();
|
|
128
|
+
const subject = normalizeSubject(match[2]);
|
|
129
|
+
if (subject) {
|
|
130
|
+
directives.push({ action, subject, line: trimmed });
|
|
131
|
+
}
|
|
112
132
|
}
|
|
113
133
|
}
|
|
114
134
|
|
|
115
|
-
return
|
|
135
|
+
return directives;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeSubject(text) {
|
|
139
|
+
// Lowercase and trim
|
|
140
|
+
let normalized = text.toLowerCase().trim();
|
|
141
|
+
|
|
142
|
+
// Remove trailing punctuation
|
|
143
|
+
normalized = normalized.replace(/[.,;:!?]+$/, '');
|
|
144
|
+
|
|
145
|
+
// Remove leading articles
|
|
146
|
+
normalized = normalized.replace(/^(the|a|an)\s+/i, '');
|
|
147
|
+
|
|
148
|
+
// Remove extra whitespace
|
|
149
|
+
normalized = normalized.replace(/\s+/g, ' ');
|
|
150
|
+
|
|
151
|
+
// Return null if too short or too long
|
|
152
|
+
if (normalized.length < 3 || normalized.length > 50) return null;
|
|
153
|
+
|
|
154
|
+
return normalized;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findDirectiveConflicts(aDirectives, bDirectives) {
|
|
158
|
+
const conflicts = [];
|
|
159
|
+
const opposites = {
|
|
160
|
+
'use': ['never', 'avoid', 'don\'t', 'do not', 'no', 'remove', 'exclude', 'disable'],
|
|
161
|
+
'prefer': ['avoid', 'never', 'don\'t', 'do not', 'no'],
|
|
162
|
+
'always': ['never', 'avoid', 'don\'t', 'do not', 'no'],
|
|
163
|
+
'add': ['remove', 'exclude', 'no'],
|
|
164
|
+
'include': ['exclude', 'remove', 'no'],
|
|
165
|
+
'enable': ['disable', 'no'],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
for (const aDir of aDirectives) {
|
|
169
|
+
for (const bDir of bDirectives) {
|
|
170
|
+
// Check if subjects are similar (allowing for minor variations)
|
|
171
|
+
if (subjectsSimilar(aDir.subject, bDir.subject)) {
|
|
172
|
+
// Check if actions are contradictory
|
|
173
|
+
const aAction = aDir.action;
|
|
174
|
+
const bAction = bDir.action;
|
|
175
|
+
|
|
176
|
+
if (opposites[aAction] && opposites[aAction].includes(bAction)) {
|
|
177
|
+
conflicts.push(`"${aAction} ${aDir.subject}" vs "${bAction} ${bDir.subject}"`);
|
|
178
|
+
} else if (opposites[bAction] && opposites[bAction].includes(aAction)) {
|
|
179
|
+
conflicts.push(`"${aAction} ${aDir.subject}" vs "${bAction} ${bDir.subject}"`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return conflicts;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function subjectsSimilar(a, b) {
|
|
189
|
+
// Exact match
|
|
190
|
+
if (a === b) return true;
|
|
191
|
+
|
|
192
|
+
// One contains the other (allowing for variations)
|
|
193
|
+
if (a.length > 5 && b.includes(a)) return true;
|
|
194
|
+
if (b.length > 5 && a.includes(b)) return true;
|
|
195
|
+
|
|
196
|
+
// Extract key words (longer than 4 chars) and check for overlap
|
|
197
|
+
const wordsA = a.split(/\s+/).filter(w => w.length > 4);
|
|
198
|
+
const wordsB = b.split(/\s+/).filter(w => w.length > 4);
|
|
199
|
+
|
|
200
|
+
// If they share a significant word, consider them similar
|
|
201
|
+
for (const wordA of wordsA) {
|
|
202
|
+
for (const wordB of wordsB) {
|
|
203
|
+
if (wordA === wordB || wordA.includes(wordB) || wordB.includes(wordA)) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Remove common variations and compare
|
|
210
|
+
const cleanA = a.replace(/\b(ing|ed|s)\b/g, '').replace(/\s+/g, '');
|
|
211
|
+
const cleanB = b.replace(/\b(ing|ed|s)\b/g, '').replace(/\s+/g, '');
|
|
212
|
+
if (cleanA === cleanB) return true;
|
|
213
|
+
|
|
214
|
+
return false;
|
|
116
215
|
}
|
|
117
216
|
|
|
118
217
|
function findRedundancy(rules) {
|
package/src/autofix.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { lintProject, parseFrontmatter } = require('./index');
|
|
4
|
-
const { loadRules, findRedundancy } = require('./audit');
|
|
4
|
+
const { loadRules, findRedundancy, findConflicts } = require('./audit');
|
|
5
|
+
const { getTemplate } = require('./templates');
|
|
6
|
+
const { showStats } = require('./stats');
|
|
5
7
|
|
|
6
8
|
function fixFrontmatter(content) {
|
|
7
9
|
const fm = parseFrontmatter(content);
|
|
@@ -86,7 +88,7 @@ function splitOversizedFile(filePath, maxTokens = 1500) {
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
async function autoFix(dir, options = {}) {
|
|
89
|
-
const results = { fixed: [], splits: [], deduped: [], errors: [] };
|
|
91
|
+
const results = { fixed: [], splits: [], deduped: [], merged: [], annotated: [], generated: [], errors: [] };
|
|
90
92
|
const rulesDir = path.join(dir, '.cursor', 'rules');
|
|
91
93
|
|
|
92
94
|
if (!fs.existsSync(rulesDir)) {
|
|
@@ -153,19 +155,179 @@ async function autoFix(dir, options = {}) {
|
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
// 3.
|
|
158
|
+
// 3. Auto-merge redundant rules (>60% overlap)
|
|
157
159
|
const rules = loadRules(dir);
|
|
158
160
|
const redundant = findRedundancy(rules);
|
|
161
|
+
const merged = new Set(); // Track files we've already merged to avoid double-processing
|
|
162
|
+
|
|
159
163
|
for (const r of redundant) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
if (merged.has(r.fileA) || merged.has(r.fileB)) continue;
|
|
165
|
+
|
|
166
|
+
if (r.overlapPct >= 60) {
|
|
167
|
+
const ruleA = rules.find(rule => rule.file === r.fileA);
|
|
168
|
+
const ruleB = rules.find(rule => rule.file === r.fileB);
|
|
169
|
+
|
|
170
|
+
if (!ruleA || !ruleB) continue;
|
|
171
|
+
|
|
172
|
+
// Determine which rule has broader scope
|
|
173
|
+
const aBroader = isBroaderScope(ruleA, ruleB);
|
|
174
|
+
const keepRule = aBroader ? ruleA : ruleB;
|
|
175
|
+
const mergeRule = aBroader ? ruleB : ruleA;
|
|
176
|
+
|
|
177
|
+
// Merge bodies: combine unique lines
|
|
178
|
+
const mergedBody = mergeRuleBodies(keepRule.body, mergeRule.body);
|
|
179
|
+
|
|
180
|
+
// Rebuild the kept file with merged content
|
|
181
|
+
const newContent = rebuildRuleFile(keepRule.fm, mergedBody);
|
|
182
|
+
|
|
183
|
+
if (!options.dryRun) {
|
|
184
|
+
const keepPath = path.join(rulesDir, keepRule.file);
|
|
185
|
+
const mergePath = path.join(rulesDir, mergeRule.file);
|
|
186
|
+
fs.writeFileSync(keepPath, newContent, 'utf-8');
|
|
187
|
+
fs.unlinkSync(mergePath);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
results.merged.push({
|
|
191
|
+
kept: keepRule.file,
|
|
192
|
+
removed: mergeRule.file,
|
|
193
|
+
overlapPct: r.overlapPct,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
merged.add(keepRule.file);
|
|
197
|
+
merged.add(mergeRule.file);
|
|
198
|
+
} else {
|
|
199
|
+
// Just flag for manual review
|
|
200
|
+
results.deduped.push({
|
|
201
|
+
fileA: r.fileA,
|
|
202
|
+
fileB: r.fileB,
|
|
203
|
+
overlapPct: r.overlapPct,
|
|
204
|
+
action: 'manual review needed',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 4. Annotate conflicting rules
|
|
210
|
+
const conflicts = findConflicts(rules);
|
|
211
|
+
const annotated = new Set();
|
|
212
|
+
|
|
213
|
+
for (const conflict of conflicts) {
|
|
214
|
+
const fileAPath = path.join(rulesDir, conflict.fileA);
|
|
215
|
+
const fileBPath = path.join(rulesDir, conflict.fileB);
|
|
216
|
+
|
|
217
|
+
if (!annotated.has(conflict.fileA)) {
|
|
218
|
+
const content = fs.readFileSync(fileAPath, 'utf-8');
|
|
219
|
+
const annotatedContent = addConflictAnnotation(content, conflict.fileB, conflict.reason);
|
|
220
|
+
if (annotatedContent !== content) {
|
|
221
|
+
if (!options.dryRun) {
|
|
222
|
+
fs.writeFileSync(fileAPath, annotatedContent, 'utf-8');
|
|
223
|
+
}
|
|
224
|
+
results.annotated.push({ file: conflict.fileA, conflictsWith: conflict.fileB });
|
|
225
|
+
annotated.add(conflict.fileA);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!annotated.has(conflict.fileB)) {
|
|
230
|
+
const content = fs.readFileSync(fileBPath, 'utf-8');
|
|
231
|
+
const annotatedContent = addConflictAnnotation(content, conflict.fileA, conflict.reason);
|
|
232
|
+
if (annotatedContent !== content) {
|
|
233
|
+
if (!options.dryRun) {
|
|
234
|
+
fs.writeFileSync(fileBPath, annotatedContent, 'utf-8');
|
|
235
|
+
}
|
|
236
|
+
results.annotated.push({ file: conflict.fileB, conflictsWith: conflict.fileA });
|
|
237
|
+
annotated.add(conflict.fileB);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 5. Generate missing rules for coverage gaps
|
|
243
|
+
const stats = showStats(dir);
|
|
244
|
+
const gaps = stats.coverageGaps || [];
|
|
245
|
+
|
|
246
|
+
for (const gap of gaps) {
|
|
247
|
+
for (const suggestedRule of gap.suggestedRules) {
|
|
248
|
+
const template = getTemplate(suggestedRule);
|
|
249
|
+
if (template) {
|
|
250
|
+
const templatePath = path.join(rulesDir, template.name);
|
|
251
|
+
// Only generate if it doesn't already exist
|
|
252
|
+
if (!fs.existsSync(templatePath)) {
|
|
253
|
+
if (!options.dryRun) {
|
|
254
|
+
fs.writeFileSync(templatePath, template.content, 'utf-8');
|
|
255
|
+
}
|
|
256
|
+
results.generated.push({ file: template.name, reason: `coverage gap for ${gap.ext}` });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
166
260
|
}
|
|
167
261
|
|
|
168
262
|
return results;
|
|
169
263
|
}
|
|
170
264
|
|
|
265
|
+
function isBroaderScope(ruleA, ruleB) {
|
|
266
|
+
// alwaysApply is broader than glob-targeted
|
|
267
|
+
if (ruleA.alwaysApply && !ruleB.alwaysApply) return true;
|
|
268
|
+
if (ruleB.alwaysApply && !ruleA.alwaysApply) return false;
|
|
269
|
+
|
|
270
|
+
// More globs = broader scope
|
|
271
|
+
const aGlobCount = (ruleA.globs || []).length;
|
|
272
|
+
const bGlobCount = (ruleB.globs || []).length;
|
|
273
|
+
if (aGlobCount > bGlobCount) return true;
|
|
274
|
+
if (bGlobCount > aGlobCount) return false;
|
|
275
|
+
|
|
276
|
+
// Default to first rule
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function mergeRuleBodies(bodyA, bodyB) {
|
|
281
|
+
const linesA = bodyA.split('\n');
|
|
282
|
+
const linesB = bodyB.split('\n');
|
|
283
|
+
|
|
284
|
+
const merged = [...linesA];
|
|
285
|
+
const seenLines = new Set(linesA.map(l => l.trim()));
|
|
286
|
+
|
|
287
|
+
for (const line of linesB) {
|
|
288
|
+
const trimmed = line.trim();
|
|
289
|
+
if (trimmed.length > 0 && !seenLines.has(trimmed)) {
|
|
290
|
+
merged.push(line);
|
|
291
|
+
seenLines.add(trimmed);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return merged.join('\n');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function rebuildRuleFile(frontmatter, body) {
|
|
299
|
+
if (!frontmatter.found || !frontmatter.data) {
|
|
300
|
+
return body;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const fmLines = [];
|
|
304
|
+
for (const [k, v] of Object.entries(frontmatter.data)) {
|
|
305
|
+
if (typeof v === 'boolean') fmLines.push(`${k}: ${v}`);
|
|
306
|
+
else if (typeof v === 'string' && (v.startsWith('[') || v === 'true' || v === 'false')) fmLines.push(`${k}: ${v}`);
|
|
307
|
+
else fmLines.push(`${k}: ${v}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return `---\n${fmLines.join('\n')}\n---\n${body}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function addConflictAnnotation(content, conflictFile, reason) {
|
|
314
|
+
const annotation = `<!-- cursor-doctor: conflicts with ${conflictFile} — review manually -->\n`;
|
|
315
|
+
|
|
316
|
+
// Check if already annotated
|
|
317
|
+
if (content.includes('cursor-doctor: conflicts with')) {
|
|
318
|
+
return content;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Add after frontmatter if present
|
|
322
|
+
const fmMatch = content.match(/^---\n[\s\S]*?\n---\n/);
|
|
323
|
+
if (fmMatch) {
|
|
324
|
+
const fm = fmMatch[0];
|
|
325
|
+
const rest = content.slice(fm.length);
|
|
326
|
+
return fm + annotation + rest;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Otherwise add at top
|
|
330
|
+
return annotation + content;
|
|
331
|
+
}
|
|
332
|
+
|
|
171
333
|
module.exports = { autoFix, fixFrontmatter, splitOversizedFile };
|
package/src/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ const { autoFix } = require('./autofix');
|
|
|
11
11
|
const { isLicensed, activateLicense } = require('./license');
|
|
12
12
|
const { fixProject } = require('./fix');
|
|
13
13
|
|
|
14
|
-
const VERSION = '1.
|
|
14
|
+
const VERSION = '1.1.0';
|
|
15
15
|
|
|
16
16
|
const RED = '\x1b[31m';
|
|
17
17
|
const YELLOW = '\x1b[33m';
|
|
@@ -291,7 +291,10 @@ async function main() {
|
|
|
291
291
|
process.exit(1);
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
var totalActions = results.fixed.length + results.splits.length + results.merged.length +
|
|
295
|
+
results.annotated.length + results.generated.length + results.deduped.length;
|
|
296
|
+
|
|
297
|
+
if (totalActions === 0) {
|
|
295
298
|
console.log(' ' + GREEN + String.fromCharCode(10003) + RESET + ' Nothing to fix. Setup looks clean.');
|
|
296
299
|
console.log();
|
|
297
300
|
process.exit(0);
|
|
@@ -301,10 +304,19 @@ async function main() {
|
|
|
301
304
|
console.log(' ' + GREEN + String.fromCharCode(10003) + RESET + ' ' + results.fixed[i].file + ': ' + results.fixed[i].change);
|
|
302
305
|
}
|
|
303
306
|
for (var i = 0; i < results.splits.length; i++) {
|
|
304
|
-
console.log(' ' +
|
|
307
|
+
console.log(' ' + BLUE + String.fromCharCode(9986) + RESET + ' Split ' + results.splits[i].file + ' -> ' + results.splits[i].parts.join(', '));
|
|
308
|
+
}
|
|
309
|
+
for (var i = 0; i < results.merged.length; i++) {
|
|
310
|
+
console.log(' ' + CYAN + String.fromCharCode(8645) + RESET + ' Merged ' + results.merged[i].removed + ' into ' + results.merged[i].kept + ' (' + results.merged[i].overlapPct + '% overlap)');
|
|
311
|
+
}
|
|
312
|
+
for (var i = 0; i < results.annotated.length; i++) {
|
|
313
|
+
console.log(' ' + YELLOW + String.fromCharCode(9888) + RESET + ' Annotated ' + results.annotated[i].file + ' (conflicts with ' + results.annotated[i].conflictsWith + ')');
|
|
314
|
+
}
|
|
315
|
+
for (var i = 0; i < results.generated.length; i++) {
|
|
316
|
+
console.log(' ' + GREEN + String.fromCharCode(10010) + RESET + ' Generated ' + results.generated[i].file + ' (' + results.generated[i].reason + ')');
|
|
305
317
|
}
|
|
306
318
|
for (var i = 0; i < results.deduped.length; i++) {
|
|
307
|
-
console.log(' ' + YELLOW + '!' + RESET + ' ' + results.deduped[i].fileA + ' + ' + results.deduped[i].fileB + ': ' + results.deduped[i].overlapPct + '% overlap');
|
|
319
|
+
console.log(' ' + YELLOW + '!' + RESET + ' ' + results.deduped[i].fileA + ' + ' + results.deduped[i].fileB + ': ' + results.deduped[i].overlapPct + '% overlap (manual review)');
|
|
308
320
|
}
|
|
309
321
|
console.log();
|
|
310
322
|
process.exit(0);
|
package/src/templates.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
// Template rules for common stacks
|
|
2
|
+
// Each template should be genuinely useful, not generic filler
|
|
3
|
+
|
|
4
|
+
const TEMPLATES = {
|
|
5
|
+
typescript: {
|
|
6
|
+
name: 'typescript.mdc',
|
|
7
|
+
content: `---
|
|
8
|
+
description: TypeScript conventions and best practices
|
|
9
|
+
globs: ["*.ts", "*.tsx"]
|
|
10
|
+
alwaysApply: false
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# TypeScript Guidelines
|
|
14
|
+
|
|
15
|
+
## Type Safety
|
|
16
|
+
- Prefer explicit return types on exported functions
|
|
17
|
+
- Use strict TypeScript config: enable noImplicitAny, strictNullChecks, noUncheckedIndexedAccess
|
|
18
|
+
- Avoid \`any\` — use \`unknown\` for truly dynamic values, then narrow with type guards
|
|
19
|
+
- Use discriminated unions instead of optional properties when modeling states
|
|
20
|
+
|
|
21
|
+
## Naming
|
|
22
|
+
- Interfaces: PascalCase (e.g., \`UserProfile\`)
|
|
23
|
+
- Type aliases: PascalCase (e.g., \`RequestHandler\`)
|
|
24
|
+
- Generic parameters: single letter (T, K, V) for simple cases, descriptive names for complex ones
|
|
25
|
+
|
|
26
|
+
## Organization
|
|
27
|
+
- Colocate types with the code that uses them
|
|
28
|
+
- Export types from index files for public API
|
|
29
|
+
- Use \`type\` for unions/intersections, \`interface\` for object shapes that might be extended
|
|
30
|
+
|
|
31
|
+
## Patterns
|
|
32
|
+
- Use \`satisfies\` to validate object literals without widening types
|
|
33
|
+
- Prefer tuple types with labeled elements: \`[name: string, age: number]\`
|
|
34
|
+
- Use template literal types for string patterns (e.g., route keys)
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
react: {
|
|
39
|
+
name: 'react.mdc',
|
|
40
|
+
content: `---
|
|
41
|
+
description: React component patterns and conventions
|
|
42
|
+
globs: ["*.tsx", "*.jsx"]
|
|
43
|
+
alwaysApply: false
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# React Guidelines
|
|
47
|
+
|
|
48
|
+
## Component Structure
|
|
49
|
+
- Prefer function components with hooks over class components
|
|
50
|
+
- Use named exports for components (easier to refactor and tree-shake)
|
|
51
|
+
- Extract custom hooks when logic is reused across >2 components
|
|
52
|
+
- Keep components under 150 lines — split into subcomponents or hooks if longer
|
|
53
|
+
|
|
54
|
+
## Hooks
|
|
55
|
+
- Declare hooks at the top level, never conditionally
|
|
56
|
+
- Use \`useCallback\` for functions passed to memoized children
|
|
57
|
+
- Use \`useMemo\` only when profiling shows a performance benefit
|
|
58
|
+
- Custom hooks should start with \`use\` prefix
|
|
59
|
+
|
|
60
|
+
## Props
|
|
61
|
+
- Destructure props in the function signature for clarity
|
|
62
|
+
- Use TypeScript interfaces for prop types, not inline types
|
|
63
|
+
- Prefer required props over optional with defaults
|
|
64
|
+
|
|
65
|
+
## State Management
|
|
66
|
+
- Prefer URL state (query params) for shareable UI state
|
|
67
|
+
- Use context for deeply-nested data, not as global store
|
|
68
|
+
- Colocate state with the component that owns it — lift only when needed
|
|
69
|
+
|
|
70
|
+
## Patterns
|
|
71
|
+
- Use compound components for related UI groups (e.g., Tabs)
|
|
72
|
+
- Avoid prop drilling — use composition or context after 2-3 levels
|
|
73
|
+
- Keep JSX readable: extract complex conditionals into variables
|
|
74
|
+
`,
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
nextjs: {
|
|
78
|
+
name: 'nextjs.mdc',
|
|
79
|
+
content: `---
|
|
80
|
+
description: Next.js app architecture and routing
|
|
81
|
+
globs: ["*.ts", "*.tsx", "next.config.*"]
|
|
82
|
+
alwaysApply: false
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
# Next.js Guidelines
|
|
86
|
+
|
|
87
|
+
## App Router (13+)
|
|
88
|
+
- Use Server Components by default — add \`'use client'\` only when needed (interactivity, hooks, browser APIs)
|
|
89
|
+
- Server Components: data fetching, async/await directly in component
|
|
90
|
+
- Client Components: event handlers, state, effects, browser-only code
|
|
91
|
+
- Never import Server Components into Client Components
|
|
92
|
+
|
|
93
|
+
## Routing
|
|
94
|
+
- Colocate components in route folders, use \`_components/\` prefix for private files
|
|
95
|
+
- Use Route Handlers (route.ts) for API endpoints, not pages/api
|
|
96
|
+
- Dynamic routes: \`[id]\` for single, \`[...slug]\` for catch-all
|
|
97
|
+
- Parallel routes and intercepting routes for modals and multi-pane UIs
|
|
98
|
+
|
|
99
|
+
## Data Fetching
|
|
100
|
+
- Use \`fetch\` in Server Components — automatic deduplication and caching
|
|
101
|
+
- Prefer server-side fetching over client-side for initial data
|
|
102
|
+
- Use \`loading.tsx\` for instant loading states (Suspense boundaries)
|
|
103
|
+
- Use \`error.tsx\` for error boundaries
|
|
104
|
+
|
|
105
|
+
## Performance
|
|
106
|
+
- Use \`next/image\` for all images — automatic optimization
|
|
107
|
+
- Enable PPR (Partial Prerendering) when available
|
|
108
|
+
- Use \`revalidate\` in fetch options for ISR, not getStaticProps
|
|
109
|
+
- Lazy load client components with \`next/dynamic\`
|
|
110
|
+
`,
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
python: {
|
|
114
|
+
name: 'python.mdc',
|
|
115
|
+
content: `---
|
|
116
|
+
description: Python style and conventions
|
|
117
|
+
globs: ["*.py"]
|
|
118
|
+
alwaysApply: false
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
# Python Guidelines
|
|
122
|
+
|
|
123
|
+
## Style
|
|
124
|
+
- Follow PEP 8 for formatting (use black or ruff for auto-formatting)
|
|
125
|
+
- Type hints on all public functions: \`def get_user(id: int) -> User | None:\`
|
|
126
|
+
- Docstrings for modules, classes, and public functions (Google or NumPy style)
|
|
127
|
+
- Max line length: 88 characters (black default)
|
|
128
|
+
|
|
129
|
+
## Structure
|
|
130
|
+
- One class per file unless tightly coupled
|
|
131
|
+
- Group imports: stdlib, third-party, local (separated by blank lines)
|
|
132
|
+
- Use \`__init__.py\` to expose public API, keep internals private with \`_prefix\`
|
|
133
|
+
|
|
134
|
+
## Typing
|
|
135
|
+
- Use \`from __future__ import annotations\` for modern type syntax in 3.9+
|
|
136
|
+
- Prefer \`list[str]\` over \`List[str]\` (3.9+)
|
|
137
|
+
- Use \`| None\` instead of \`Optional\` (3.10+)
|
|
138
|
+
- Use Protocol for structural subtyping, not abstract classes
|
|
139
|
+
|
|
140
|
+
## Patterns
|
|
141
|
+
- Use dataclasses for data containers, not dicts
|
|
142
|
+
- Context managers (\`with\`) for resource management
|
|
143
|
+
- Comprehensions for simple transformations, generator expressions for large data
|
|
144
|
+
- Avoid mutable default arguments — use \`None\` and initialize in function body
|
|
145
|
+
`,
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
django: {
|
|
149
|
+
name: 'django.mdc',
|
|
150
|
+
content: `---
|
|
151
|
+
description: Django models, views, and architecture
|
|
152
|
+
globs: ["*.py"]
|
|
153
|
+
alwaysApply: false
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
# Django Guidelines
|
|
157
|
+
|
|
158
|
+
## Models
|
|
159
|
+
- Singular names: \`User\`, \`Order\` (Django pluralizes automatically)
|
|
160
|
+
- Use \`models.TextChoices\` or \`models.IntegerChoices\` for choice fields
|
|
161
|
+
- Indexes: add \`db_index=True\` to foreign keys and frequently queried fields
|
|
162
|
+
- Use \`select_related\` for one-to-one/foreign key, \`prefetch_related\` for many-to-many
|
|
163
|
+
- Custom managers for reusable querysets
|
|
164
|
+
|
|
165
|
+
## Views
|
|
166
|
+
- Prefer class-based views for CRUD, function-based for custom logic
|
|
167
|
+
- Use \`get_object_or_404\` instead of manual try/except for single-object lookups
|
|
168
|
+
- Keep views thin — move business logic to models, managers, or services
|
|
169
|
+
- Return JSON with \`JsonResponse\`, not serialized strings
|
|
170
|
+
|
|
171
|
+
## URLs
|
|
172
|
+
- Use path converters: \`path('users/<int:id>/', ...)\` not regex
|
|
173
|
+
- Name all URL patterns: \`name='user-detail'\`
|
|
174
|
+
- Namespace apps: \`app_name = 'blog'\` in urls.py
|
|
175
|
+
|
|
176
|
+
## Queries
|
|
177
|
+
- Use \`.only()\` and \`.defer()\` to limit fields when fetching large models
|
|
178
|
+
- Annotate/aggregate at database level, not Python loops
|
|
179
|
+
- Use \`Q\` objects for complex lookups, not raw SQL
|
|
180
|
+
- Avoid N+1 queries — use debug toolbar to catch them
|
|
181
|
+
|
|
182
|
+
## Settings
|
|
183
|
+
- Use environment variables for secrets (python-decouple or django-environ)
|
|
184
|
+
- Split settings: base.py, dev.py, prod.py
|
|
185
|
+
- Never commit SECRET_KEY or database credentials
|
|
186
|
+
`,
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
go: {
|
|
190
|
+
name: 'go.mdc',
|
|
191
|
+
content: `---
|
|
192
|
+
description: Go idioms and best practices
|
|
193
|
+
globs: ["*.go"]
|
|
194
|
+
alwaysApply: false
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
# Go Guidelines
|
|
198
|
+
|
|
199
|
+
## Style
|
|
200
|
+
- Run \`gofmt\` (automatic with most editors)
|
|
201
|
+
- Use \`golangci-lint\` for comprehensive linting
|
|
202
|
+
- Package names: lowercase, single word, no underscores
|
|
203
|
+
- Exported names: PascalCase; unexported: camelCase
|
|
204
|
+
|
|
205
|
+
## Error Handling
|
|
206
|
+
- Always check errors immediately: \`if err != nil { return err }\`
|
|
207
|
+
- Wrap errors with context: \`fmt.Errorf("failed to save user: %w", err)\`
|
|
208
|
+
- Use \`errors.Is\` and \`errors.As\` for sentinel and type checking
|
|
209
|
+
- Return errors, don't panic (except for truly unrecoverable situations)
|
|
210
|
+
|
|
211
|
+
## Concurrency
|
|
212
|
+
- Use channels for communication, mutexes for state protection
|
|
213
|
+
- Close channels from sender side, never receiver
|
|
214
|
+
- Use \`context.Context\` for cancellation and timeouts
|
|
215
|
+
- \`defer\` unlock calls immediately after lock: \`mu.Lock(); defer mu.Unlock()\`
|
|
216
|
+
|
|
217
|
+
## Organization
|
|
218
|
+
- Keep packages focused: one responsibility per package
|
|
219
|
+
- Use internal/ for private code (enforced by compiler)
|
|
220
|
+
- Minimize dependencies between packages (dependency graph should be acyclic)
|
|
221
|
+
|
|
222
|
+
## Patterns
|
|
223
|
+
- Accept interfaces, return structs
|
|
224
|
+
- Use \`io.Reader\`/\`io.Writer\` for streaming data
|
|
225
|
+
- Constructor pattern: \`func NewClient(opts ...Option) *Client\`
|
|
226
|
+
- Avoid getters/setters — expose fields directly or use methods that do work
|
|
227
|
+
`,
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
rust: {
|
|
231
|
+
name: 'rust.mdc',
|
|
232
|
+
content: `---
|
|
233
|
+
description: Rust patterns and idiomatic code
|
|
234
|
+
globs: ["*.rs"]
|
|
235
|
+
alwaysApply: false
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
# Rust Guidelines
|
|
239
|
+
|
|
240
|
+
## Ownership
|
|
241
|
+
- Prefer borrowing (\`&T\`, \`&mut T\`) over owned values when possible
|
|
242
|
+
- Use \`.clone()\` explicitly — no hidden copies
|
|
243
|
+
- Use \`Cow<str>\` when you might need to clone, but usually don't
|
|
244
|
+
- \`Arc<T>\` for shared ownership across threads, \`Rc<T>\` for single-threaded
|
|
245
|
+
|
|
246
|
+
## Error Handling
|
|
247
|
+
- Use \`Result<T, E>\` for recoverable errors, panic for bugs
|
|
248
|
+
- Use \`?\` operator to propagate errors, not manual \`match\`
|
|
249
|
+
- Use \`thiserror\` for library errors, \`anyhow\` for application errors
|
|
250
|
+
- Implement \`std::error::Error\` for custom error types
|
|
251
|
+
|
|
252
|
+
## Types
|
|
253
|
+
- Use newtypes for domain concepts: \`struct UserId(u64)\`
|
|
254
|
+
- Prefer \`Option<T>\` over nullable pointers
|
|
255
|
+
- Use \`enum\` for state machines and variants
|
|
256
|
+
- Use \`#[non_exhaustive]\` on public enums for forward compatibility
|
|
257
|
+
|
|
258
|
+
## Patterns
|
|
259
|
+
- Builder pattern for complex construction: \`Config::builder().timeout(30).build()\`
|
|
260
|
+
- Use \`impl Trait\` for return types instead of boxing when possible
|
|
261
|
+
- Use \`#[derive(Debug, Clone)]\` by default, add others as needed
|
|
262
|
+
- Avoid \`unwrap()\` in production code — use \`expect()\` with a message or propagate errors
|
|
263
|
+
|
|
264
|
+
## Performance
|
|
265
|
+
- Use iterators, not loops — they're zero-cost and composable
|
|
266
|
+
- Prefer \`&[T]\` over \`&Vec<T>\` in function signatures
|
|
267
|
+
- Use \`#[inline]\` sparingly, only after profiling
|
|
268
|
+
- Use \`cargo flamegraph\` or \`perf\` for profiling
|
|
269
|
+
`,
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
vue: {
|
|
273
|
+
name: 'vue.mdc',
|
|
274
|
+
content: `---
|
|
275
|
+
description: Vue 3 composition API and component patterns
|
|
276
|
+
globs: ["*.vue"]
|
|
277
|
+
alwaysApply: false
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
# Vue 3 Guidelines
|
|
281
|
+
|
|
282
|
+
## Composition API
|
|
283
|
+
- Use \`<script setup>\` for all components (less boilerplate)
|
|
284
|
+
- Declare reactive state with \`ref\` for primitives, \`reactive\` for objects
|
|
285
|
+
- Extract reusable logic into composables (functions that start with \`use\`)
|
|
286
|
+
- Use \`computed\` for derived state, not methods
|
|
287
|
+
|
|
288
|
+
## Component Structure
|
|
289
|
+
- Template-first: put \`<template>\` before \`<script>\`
|
|
290
|
+
- Single file components: template, script, style in one .vue file
|
|
291
|
+
- Keep components under 200 lines — extract child components if longer
|
|
292
|
+
- Use \`defineProps\` and \`defineEmits\` (no imports needed with \`<script setup>\`)
|
|
293
|
+
|
|
294
|
+
## Props and Events
|
|
295
|
+
- Use TypeScript for prop types: \`defineProps<{ userId: number }>()\`
|
|
296
|
+
- Emit events for child-to-parent communication, not prop mutations
|
|
297
|
+
- Use \`v-model\` for two-way binding, with \`update:modelValue\` event
|
|
298
|
+
|
|
299
|
+
## Directives
|
|
300
|
+
- Use \`v-if\` for conditional rendering, \`v-show\` for toggling visibility (DOM stays)
|
|
301
|
+
- Use \`v-for\` with \`:key\` — keys should be stable and unique
|
|
302
|
+
- Use \`v-memo\` for expensive lists that rarely change
|
|
303
|
+
|
|
304
|
+
## Performance
|
|
305
|
+
- Use \`shallowRef\` for large objects that are replaced, not mutated
|
|
306
|
+
- Use \`v-once\` for static content that never changes
|
|
307
|
+
- Lazy load routes with \`() => import('./views/About.vue')\`
|
|
308
|
+
`,
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
svelte: {
|
|
312
|
+
name: 'svelte.mdc',
|
|
313
|
+
content: `---
|
|
314
|
+
description: Svelte component patterns and reactivity
|
|
315
|
+
globs: ["*.svelte"]
|
|
316
|
+
alwaysApply: false
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
# Svelte Guidelines
|
|
320
|
+
|
|
321
|
+
## Reactivity
|
|
322
|
+
- Reactive statements: \`$: doubled = count * 2\` (re-runs when dependencies change)
|
|
323
|
+
- Use \`$:\` for side effects: \`$: console.log('count is', count)\`
|
|
324
|
+
- Update arrays/objects with assignment, not mutation: \`items = [...items, newItem]\`
|
|
325
|
+
- Use stores (\`writable\`, \`readable\`, \`derived\`) for global state
|
|
326
|
+
|
|
327
|
+
## Component Structure
|
|
328
|
+
- Export variables to make them props: \`export let name\`
|
|
329
|
+
- Use \`$$props\` and \`$$restProps\` to forward props
|
|
330
|
+
- Emit events with \`createEventDispatcher\` or bubble with \`on:click\`
|
|
331
|
+
- Use slots for composition: \`<slot />\` and named slots
|
|
332
|
+
|
|
333
|
+
## Directives
|
|
334
|
+
- \`use:action\` for lifecycle hooks on DOM elements
|
|
335
|
+
- \`bind:this\` to get DOM references
|
|
336
|
+
- \`class:active={isActive}\` for conditional classes
|
|
337
|
+
- \`on:event|modifiers\` for event handling (\`preventDefault\`, \`stopPropagation\`, etc.)
|
|
338
|
+
|
|
339
|
+
## SvelteKit (if used)
|
|
340
|
+
- Use \`+page.svelte\` for routes, \`+page.ts\` for load functions
|
|
341
|
+
- Use \`+layout.svelte\` for shared layouts
|
|
342
|
+
- Use \`$app/stores\` for page, navigating, updated stores
|
|
343
|
+
- Form actions in \`+page.server.ts\` for progressive enhancement
|
|
344
|
+
|
|
345
|
+
## Patterns
|
|
346
|
+
- Keep logic in \`<script>\`, not template — complex expressions should be variables
|
|
347
|
+
- Use \`{#await promise}\` for async data in templates
|
|
348
|
+
- Use transitions: \`transition:fade\`, \`in:fly\`, \`out:scale\`
|
|
349
|
+
`,
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
tailwind: {
|
|
353
|
+
name: 'tailwind.mdc',
|
|
354
|
+
content: `---
|
|
355
|
+
description: Tailwind CSS utility patterns and conventions
|
|
356
|
+
globs: ["*.css", "*.tsx", "*.jsx"]
|
|
357
|
+
alwaysApply: false
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
# Tailwind CSS Guidelines
|
|
361
|
+
|
|
362
|
+
## Utility Classes
|
|
363
|
+
- Use utilities for layout, spacing, colors — avoid custom CSS when possible
|
|
364
|
+
- Group utilities logically: layout → spacing → typography → colors → effects
|
|
365
|
+
- Use \`@apply\` sparingly, only for truly reusable patterns (extract components instead)
|
|
366
|
+
- Use arbitrary values when needed: \`w-[127px]\`, \`text-[#1da1f2]\`
|
|
367
|
+
|
|
368
|
+
## Responsive Design
|
|
369
|
+
- Mobile-first: default classes apply to all sizes, add \`md:\`, \`lg:\` for larger screens
|
|
370
|
+
- Use container utilities: \`container mx-auto px-4\`
|
|
371
|
+
- Use responsive utilities: \`grid-cols-1 md:grid-cols-2 lg:grid-cols-3\`
|
|
372
|
+
|
|
373
|
+
## Dark Mode
|
|
374
|
+
- Configure dark mode in tailwind.config (class or media strategy)
|
|
375
|
+
- Use \`dark:\` variant: \`bg-white dark:bg-gray-800\`
|
|
376
|
+
- Group related variants: \`text-gray-900 dark:text-gray-100\`
|
|
377
|
+
|
|
378
|
+
## Customization
|
|
379
|
+
- Extend theme in tailwind.config.js, don't replace defaults
|
|
380
|
+
- Use CSS variables for dynamic values (e.g., user themes)
|
|
381
|
+
- Use \`clsx\` or \`cn\` helper for conditional classes
|
|
382
|
+
- Keep config organized: colors, spacing, fonts, plugins
|
|
383
|
+
|
|
384
|
+
## Components
|
|
385
|
+
- Extract components when same classes repeat >3 times
|
|
386
|
+
- Use \`@layer components\` for component classes, \`@layer utilities\` for custom utilities
|
|
387
|
+
- Prefix custom classes to avoid conflicts: \`btn-primary\` not \`primary\`
|
|
388
|
+
`,
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
express: {
|
|
392
|
+
name: 'express.mdc',
|
|
393
|
+
content: `---
|
|
394
|
+
description: Express.js API routes and middleware
|
|
395
|
+
globs: ["*.js", "*.ts"]
|
|
396
|
+
alwaysApply: false
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
# Express Guidelines
|
|
400
|
+
|
|
401
|
+
## Routing
|
|
402
|
+
- Use Router() for modular route definitions: \`const router = express.Router()\`
|
|
403
|
+
- Group routes by resource: \`/api/users\`, \`/api/posts\`
|
|
404
|
+
- Use route parameters for dynamic segments: \`/users/:id\`
|
|
405
|
+
- Use query strings for filters: \`/users?role=admin\`
|
|
406
|
+
|
|
407
|
+
## Middleware
|
|
408
|
+
- Order matters: error handlers go last, after all routes
|
|
409
|
+
- Use \`app.use(express.json())\` for JSON body parsing
|
|
410
|
+
- Use \`next()\` to pass control, \`next(err)\` to trigger error handler
|
|
411
|
+
- Keep middleware focused: one responsibility per function
|
|
412
|
+
|
|
413
|
+
## Error Handling
|
|
414
|
+
- Use async error wrapper to avoid try/catch in every route
|
|
415
|
+
- Centralized error handler: \`app.use((err, req, res, next) => {...})\`
|
|
416
|
+
- Return consistent error format: \`{ error: { message, code, details } }\`
|
|
417
|
+
- Use HTTP status codes correctly: 400 (bad request), 401 (unauthorized), 404 (not found), 500 (server error)
|
|
418
|
+
|
|
419
|
+
## Request/Response
|
|
420
|
+
- Validate input before processing (use express-validator or zod)
|
|
421
|
+
- Use \`res.status(code).json(data)\`, not \`res.send\`
|
|
422
|
+
- Set appropriate headers: \`Content-Type\`, \`Cache-Control\`
|
|
423
|
+
- Use \`res.locals\` to pass data between middleware
|
|
424
|
+
|
|
425
|
+
## Security
|
|
426
|
+
- Use helmet for security headers
|
|
427
|
+
- Rate limit with express-rate-limit
|
|
428
|
+
- Sanitize input to prevent XSS and SQL injection
|
|
429
|
+
- Use CORS middleware, configure allowed origins
|
|
430
|
+
`,
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
testing: {
|
|
434
|
+
name: 'testing.mdc',
|
|
435
|
+
content: `---
|
|
436
|
+
description: Testing patterns and conventions
|
|
437
|
+
globs: ["*.test.*", "*.spec.*"]
|
|
438
|
+
alwaysApply: false
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
# Testing Guidelines
|
|
442
|
+
|
|
443
|
+
## Structure
|
|
444
|
+
- One test file per source file: \`user.ts\` → \`user.test.ts\`
|
|
445
|
+
- Use \`describe\` blocks to group related tests
|
|
446
|
+
- Test names: \`it('should <expected behavior> when <condition>')\`
|
|
447
|
+
- Arrange-Act-Assert pattern: setup → execute → verify
|
|
448
|
+
|
|
449
|
+
## What to Test
|
|
450
|
+
- Public API, not implementation details
|
|
451
|
+
- Edge cases: null, undefined, empty arrays, boundary values
|
|
452
|
+
- Error paths: what happens when things fail
|
|
453
|
+
- Integration points: API calls, database queries, external services
|
|
454
|
+
|
|
455
|
+
## Mocking
|
|
456
|
+
- Mock external dependencies (API clients, databases), not your own code
|
|
457
|
+
- Use dependency injection to make code testable
|
|
458
|
+
- Prefer test doubles that verify behavior (spies), not just return values (stubs)
|
|
459
|
+
- Reset mocks between tests: \`beforeEach(() => jest.clearAllMocks())\`
|
|
460
|
+
|
|
461
|
+
## Assertions
|
|
462
|
+
- One logical assertion per test (multiple expect calls are OK if testing same outcome)
|
|
463
|
+
- Use specific matchers: \`toEqual\` for deep equality, \`toBe\` for identity
|
|
464
|
+
- Use \`toThrow\` for error testing, with specific error message or type
|
|
465
|
+
|
|
466
|
+
## Best Practices
|
|
467
|
+
- Tests should be fast (<1s per test file ideal)
|
|
468
|
+
- Tests should be independent — order shouldn't matter
|
|
469
|
+
- Avoid snapshot tests for complex objects (brittle)
|
|
470
|
+
- Use factories or builders for test data, not inline objects
|
|
471
|
+
`,
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
function getTemplate(stack) {
|
|
476
|
+
const key = stack.toLowerCase();
|
|
477
|
+
return TEMPLATES[key] || null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function getAllTemplates() {
|
|
481
|
+
return Object.values(TEMPLATES);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function getTemplateNames() {
|
|
485
|
+
return Object.keys(TEMPLATES);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
module.exports = { getTemplate, getAllTemplates, getTemplateNames, TEMPLATES };
|