Skip to content

Commit 55f61f6

Browse files
committed
v1.7.7: improve lint accuracy — reduce false positives, catch dead rules
Lint accuracy (tested against 8 real-world repos): - Context-aware vague rule detection: skip qualified phrases like 'be consistent with X' or 'follow best practices for Y' - Detect non-functional rules: alwaysApply:false + empty globs → ERROR - Catch empty frontmatter values (description:, globs: with no value) - Fix false positives on empty descriptions (array/whitespace-only) - Fix 'body starts with description' false positive on empty descriptions - Updated tests to match new message text (80/80 passing)
1 parent 4910e56 commit 55f61f6

File tree

4 files changed

+41
-45
lines changed

4 files changed

+41
-45
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cursor-doctor",
3-
"version": "1.7.6",
3+
"version": "1.7.7",
44
"description": "Fix your Cursor AI setup in seconds — health checks, diagnostics, and auto-repair for your .cursor config",
55
"main": "src/index.js",
66
"bin": {

src/cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const { exportRules, importRules, detectDrift, setBaseline } = require('./team-s
1717
const { lintAgentConfigs, formatAgentLint } = require('./agents-lint');
1818
const { lintMcpConfigs, formatMcpLint } = require('./mcp-lint');
1919

20-
const VERSION = '1.7.6';
20+
const VERSION = '1.7.7';
2121

2222
var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
2323
const RED = useColor ? '\x1b[31m' : '';

src/index.js

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ const VAGUE_PATTERNS = [
2828
'be concise',
2929
];
3030

31+
function findVagueRules(content) {
32+
const issues = [];
33+
const lines = content.split('\n');
34+
for (var vi = 0; vi < lines.length; vi++) {
35+
const lineLower = lines[vi].toLowerCase().trim();
36+
if (lineLower.startsWith('#') || lineLower.startsWith('```') || lineLower.length === 0) continue;
37+
for (const pattern of VAGUE_PATTERNS) {
38+
const idx = lineLower.indexOf(pattern);
39+
if (idx === -1) continue;
40+
// Skip if phrase is qualified by context words (not standalone vague advice)
41+
const after = lineLower.slice(idx + pattern.length).trim();
42+
if (after.startsWith('with ') || after.startsWith('for ') || after.startsWith('in ') ||
43+
after.startsWith('by ') || after.startsWith('using ') || after.startsWith('according to ') ||
44+
after.startsWith('and ')) continue;
45+
issues.push({ severity: 'warning', message: `Vague rule detected: "${pattern}"`, line: vi + 1 });
46+
break;
47+
}
48+
}
49+
return issues;
50+
}
51+
3152
function parseFrontmatter(content) {
3253
var normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
3354
const match = normalized.match(/^---\n([\s\S]*?)\n---/);
@@ -149,8 +170,13 @@ async function lintMdcFile(filePath) {
149170
if (fm.data.alwaysApply === undefined && !hasGlobs) {
150171
issues.push({ severity: 'warning', message: 'No alwaysApply or globs set — rule may only apply when manually referenced', hint: 'Add alwaysApply: true for global rules, or add globs to scope to specific files' });
151172
}
152-
if (!fm.data.description) {
153-
issues.push({ severity: 'warning', message: 'Missing description in frontmatter', hint: 'Add a description so Cursor knows when to apply this rule' });
173+
var descEmpty = !fm.data.description || (typeof fm.data.description === 'string' && fm.data.description.trim() === '') || (Array.isArray(fm.data.description) && fm.data.description.length === 0);
174+
if (descEmpty) {
175+
issues.push({ severity: 'warning', message: 'Missing or empty description in frontmatter', hint: 'Add a description so Cursor knows when to apply this rule' });
176+
}
177+
// Non-functional rule: alwaysApply is explicitly false and no globs
178+
if (fm.data.alwaysApply === false && !hasGlobs) {
179+
issues.push({ severity: 'error', message: 'Rule will never load: alwaysApply is false and no globs are set', hint: 'Set alwaysApply: true for global rules, or add globs to scope to specific files' });
154180
}
155181
if (fm.data.globs && typeof fm.data.globs === 'string' && fm.data.globs.includes(',') && !fm.data.globs.trim().startsWith('[')) {
156182
issues.push({ severity: 'warning', message: 'Globs as comma-separated string — consider using YAML array format', hint: 'Use globs:\\n - "*.ts"\\n - "*.tsx"' });
@@ -177,26 +203,10 @@ async function lintMdcFile(filePath) {
177203
});
178204
}
179205

180-
// alwaysApply is false with no globs
181-
var parsedGlobs = fm.data.globs ? (Array.isArray(fm.data.globs) ? fm.data.globs : parseGlobs(fm.data.globs)) : [];
182-
if (fm.data.alwaysApply === false && parsedGlobs.length === 0) {
183-
issues.push({
184-
severity: 'error',
185-
message: 'alwaysApply is false with no globs — rule will never trigger',
186-
hint: 'Either set alwaysApply: true or add globs to specify when this rule applies.',
187-
});
188-
}
189206
}
190207

191-
// Vague rules
192-
const contentLower = content.toLowerCase();
193-
for (const pattern of VAGUE_PATTERNS) {
194-
const idx = contentLower.indexOf(pattern);
195-
if (idx !== -1) {
196-
const lineNum = content.slice(0, idx).split('\n').length;
197-
issues.push({ severity: 'warning', message: `Vague rule detected: "${pattern}"`, line: lineNum });
198-
}
199-
}
208+
// Vague rules (context-aware — skip qualified phrases)
209+
issues.push(...findVagueRules(content));
200210

201211
// Get body content for additional checks
202212
const body = getBody(content);
@@ -238,8 +248,8 @@ async function lintMdcFile(filePath) {
238248
});
239249
}
240250

241-
// 4. Description too short
242-
if (fm.data && fm.data.description && fm.data.description.length < 10) {
251+
// 4. Description too short (skip if already flagged as empty)
252+
if (fm.data && fm.data.description && typeof fm.data.description === 'string' && fm.data.description.trim().length > 0 && fm.data.description.trim().length < 10) {
243253
issues.push({
244254
severity: 'warning',
245255
message: 'Description is very short (<10 chars)',
@@ -380,7 +390,7 @@ async function lintMdcFile(filePath) {
380390
}
381391

382392
// Rule body starts with the description repeated
383-
if (fm.data && fm.data.description && body.trim().startsWith(fm.data.description)) {
393+
if (fm.data && fm.data.description && typeof fm.data.description === 'string' && fm.data.description.trim().length > 0 && body.trim().startsWith(fm.data.description)) {
384394
issues.push({
385395
severity: 'warning',
386396
message: 'Rule body starts with description repeated',
@@ -619,15 +629,8 @@ async function lintSkillFile(filePath) {
619629
issues.push({ severity: 'warning', message: 'Long skill with no headings', hint: 'Add ## sections to organize instructions for better agent comprehension' });
620630
}
621631

622-
// Vague rules (same as .mdc)
623-
const contentLower = content.toLowerCase();
624-
for (const pattern of VAGUE_PATTERNS) {
625-
const idx = contentLower.indexOf(pattern);
626-
if (idx !== -1) {
627-
const lineNum = content.slice(0, idx).split('\n').length;
628-
issues.push({ severity: 'warning', message: `Vague instruction: "${pattern}"`, line: lineNum });
629-
}
630-
}
632+
// Vague rules (context-aware)
633+
issues.push(...findVagueRules(content));
631634

632635
return { file: filePath, issues };
633636
}
@@ -687,15 +690,8 @@ async function lintCursorrules(filePath) {
687690
hint: 'Use .cursor/rules/*.mdc with alwaysApply: true for agent mode compatibility',
688691
});
689692

690-
// Vague rules
691-
const contentLower = content.toLowerCase();
692-
for (const pattern of VAGUE_PATTERNS) {
693-
const idx = contentLower.indexOf(pattern);
694-
if (idx !== -1) {
695-
const lineNum = content.slice(0, idx).split('\n').length;
696-
issues.push({ severity: 'warning', message: `Vague rule detected: "${pattern}"`, line: lineNum });
697-
}
698-
}
693+
// Vague rules (context-aware)
694+
issues.push(...findVagueRules(content));
699695

700696
return { file: filePath, issues };
701697
}

test/test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ Body content`);
209209

210210
const result = await lintMdcFile(filePath);
211211
const warnings = result.issues.filter(i => i.severity === 'warning');
212-
assert(warnings.some(w => w.message.includes('Missing description')));
212+
assert(warnings.some(w => w.message.includes('description')));
213213
});
214214

215215
asyncTest('lintMdcFile: missing alwaysApply AND globs → warning', async () => {
@@ -713,7 +713,7 @@ Body content`);
713713

714714
const result = await lintMdcFile(filePath);
715715
const warnings = result.issues.filter(i => i.severity === 'warning');
716-
assert(warnings.some(w => w.message.includes('Missing description')));
716+
assert(warnings.some(w => w.message.includes('description')));
717717
});
718718

719719
await asyncTest('lintMdcFile: missing alwaysApply AND globs → warning', async () => {

0 commit comments

Comments
 (0)