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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-doctor",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Fix your Cursor AI setup in seconds — health checks, diagnostics, and auto-repair for your .cursor config",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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
- // Check for contradictory instructions
66
- const aBody = a.body.toLowerCase();
67
- const bBody = b.body.toLowerCase();
65
+ // Extract directives from both rules
66
+ const aDirectives = extractDirectives(a.body);
67
+ const bDirectives = extractDirectives(b.body);
68
68
 
69
- // Simple contradiction detection
70
- const contradictions = findContradictions(aBody, bBody);
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 findContradictions(a, b) {
96
- const contradictions = [];
97
- const pairs = [
98
- [/always use semicolons/i, /never use semicolons|no semicolons/i],
99
- [/use single quotes/i, /use double quotes/i],
100
- [/use tabs/i, /use spaces/i],
101
- [/use css modules/i, /use tailwind|use styled-components/i],
102
- [/use relative imports/i, /use absolute imports|use path aliases/i],
103
- [/prefer classes/i, /prefer functions|prefer functional/i],
104
- [/use default exports/i, /use named exports|no default exports/i],
105
- [/use arrow functions/i, /use function declarations|avoid arrow functions/i],
106
- [/use any/i, /never use any|avoid any|no any/i],
107
- ];
108
-
109
- for (const [patA, patB] of pairs) {
110
- if ((patA.test(a) && patB.test(b)) || (patB.test(a) && patA.test(b))) {
111
- contradictions.push(`Conflicting style: "${patA.source}" vs "${patB.source}"`);
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 contradictions;
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. Remove redundancy (just flag, don't auto-delete)
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
- results.deduped.push({
161
- fileA: r.fileA,
162
- fileB: r.fileB,
163
- overlapPct: r.overlapPct,
164
- action: 'manual review needed run `cursor-doctor audit` for details',
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.0.0';
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
- if (results.fixed.length === 0 && results.splits.length === 0 && results.deduped.length === 0) {
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(' ' + GREEN + String.fromCharCode(10003) + RESET + ' Split ' + results.splits[i].file + ' -> ' + results.splits[i].parts.join(', '));
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);
@@ -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 };