deadrule 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 +21 -0
- package/README.md +110 -0
- package/dist/analyzers/cost.d.ts +3 -0
- package/dist/analyzers/cost.js +17 -0
- package/dist/analyzers/defaults.d.ts +2 -0
- package/dist/analyzers/defaults.js +68 -0
- package/dist/analyzers/overlap.d.ts +5 -0
- package/dist/analyzers/overlap.js +98 -0
- package/dist/analyzers/staleness.d.ts +2 -0
- package/dist/analyzers/staleness.js +11 -0
- package/dist/analyzers/vagueness.d.ts +2 -0
- package/dist/analyzers/vagueness.js +52 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -0
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +85 -0
- package/dist/reporter.d.ts +2 -0
- package/dist/reporter.js +164 -0
- package/dist/scanner.d.ts +6 -0
- package/dist/scanner.js +68 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.js +1 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JSK9999
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# deadrule
|
|
2
|
+
|
|
3
|
+
Find dead rules in your AI coding assistant config.
|
|
4
|
+
|
|
5
|
+
AI coding tools (Claude Code, Cursor, Codex) let you define rules and skills. Over time, rules pile up. Some duplicate each other. Some repeat what the AI already does. Some are too vague to follow. **deadrule** finds them.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx deadrule
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
No API keys. No setup. No runtime dependencies.
|
|
14
|
+
|
|
15
|
+
## What it checks
|
|
16
|
+
|
|
17
|
+
| Check | What it finds |
|
|
18
|
+
|-------|--------------|
|
|
19
|
+
| **Duplicates** | Rules that overlap with each other |
|
|
20
|
+
| **Overlapping directives** | Same instruction copied across multiple files |
|
|
21
|
+
| **Claude defaults** | Rules that repeat Claude's built-in behavior |
|
|
22
|
+
| **Vague directives** | Instructions too abstract to be actionable |
|
|
23
|
+
| **Token cost** | How many tokens each rule file consumes |
|
|
24
|
+
| **Stale rules** | Rules not modified in 90+ days |
|
|
25
|
+
|
|
26
|
+
## Custom Paths
|
|
27
|
+
|
|
28
|
+
Scan any directory of rules:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Scan a specific rules directory
|
|
32
|
+
deadrule ./my-rules/
|
|
33
|
+
|
|
34
|
+
# Scan multiple paths
|
|
35
|
+
deadrule ./rules/ ./agents/
|
|
36
|
+
|
|
37
|
+
# Scan a cloned rule set
|
|
38
|
+
deadrule ~/everything-claude-code/rules/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Where it scans (default)
|
|
42
|
+
|
|
43
|
+
- `~/.claude/rules/*.md` (global rules)
|
|
44
|
+
- `.claude/rules/*.md` (project rules)
|
|
45
|
+
- `CLAUDE.md` (project root)
|
|
46
|
+
- `.cursorrules` (Cursor)
|
|
47
|
+
|
|
48
|
+
## Real-World Test: Everything Claude Code (136 rules)
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
$ deadrule ./rules/ ./agents/
|
|
52
|
+
|
|
53
|
+
deadrule v0.1.0
|
|
54
|
+
================
|
|
55
|
+
|
|
56
|
+
Scanned: 136 rules (0 global, 136 project)
|
|
57
|
+
Total token cost: ~90,077 tokens (45.0% of context)
|
|
58
|
+
|
|
59
|
+
DUPLICATES
|
|
60
|
+
agents/java-build-resolver.md <-> agents/kotlin-build-resolver.md (51% overlap)
|
|
61
|
+
|
|
62
|
+
OVERLAPPING DIRECTIVES (50 found, showing top 10)
|
|
63
|
+
"Surgical fixes only - don't refactor, just fix the error"
|
|
64
|
+
-> repeated in: agents/cpp-build-resolver.md, agents/dart-build-resolver.md,
|
|
65
|
+
agents/java-build-resolver.md, agents/rust-build-resolver.md,
|
|
66
|
+
agents/go-build-resolver.md, agents/kotlin-build-resolver.md,
|
|
67
|
+
agents/pytorch-build-resolver.md
|
|
68
|
+
|
|
69
|
+
"Approve: No CRITICAL or HIGH issues"
|
|
70
|
+
-> repeated in: common/code-review.md, agents/code-reviewer.md,
|
|
71
|
+
agents/cpp-reviewer.md, agents/csharp-reviewer.md, ... (11 files)
|
|
72
|
+
|
|
73
|
+
REDUNDANT WITH CLAUDE DEFAULTS
|
|
74
|
+
common/security.md: "SQL injection prevention (parameterized queries)"
|
|
75
|
+
-> Claude already uses parameterized queries by default
|
|
76
|
+
|
|
77
|
+
java/security.md: "Always use parameterized queries - never concat..."
|
|
78
|
+
-> Claude already uses parameterized queries by default
|
|
79
|
+
|
|
80
|
+
TOKEN COST BREAKDOWN (top 10)
|
|
81
|
+
agents/flutter-reviewer.md ~ 3510 tokens ||||...... 4%
|
|
82
|
+
agents/performance-optimizer.md ~ 3111 tokens |||....... 3%
|
|
83
|
+
agents/code-reviewer.md ~ 2187 tokens ||........ 2%
|
|
84
|
+
... 126 more files (~69,115 tokens)
|
|
85
|
+
|
|
86
|
+
SUMMARY
|
|
87
|
+
1 duplicate pair(s)
|
|
88
|
+
50 overlapping directive(s)
|
|
89
|
+
13 redundant with Claude defaults
|
|
90
|
+
25 vague directive(s)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Key finding: **ECC loads ~90K tokens (45% of context window) in rules alone.**
|
|
94
|
+
|
|
95
|
+
## Roadmap
|
|
96
|
+
|
|
97
|
+
See [open issues](https://github.com/JSK9999/deadrule/issues) for planned features.
|
|
98
|
+
|
|
99
|
+
- [ ] Runtime rule tracking via hooks (find truly unused rules)
|
|
100
|
+
- [ ] `--json` output for CI/CD
|
|
101
|
+
- [ ] `--fix` auto-merge duplicate rules
|
|
102
|
+
- [ ] Cross-project rule comparison
|
|
103
|
+
|
|
104
|
+
## Contributing
|
|
105
|
+
|
|
106
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and guidelines.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const CONTEXT_WINDOW = 200_000;
|
|
2
|
+
export function analyzeCost(files) {
|
|
3
|
+
const totalTokens = files.reduce((sum, f) => sum + f.tokenCount, 0);
|
|
4
|
+
return files
|
|
5
|
+
.map((f) => ({
|
|
6
|
+
file: f.filename,
|
|
7
|
+
tokens: f.tokenCount,
|
|
8
|
+
percent: totalTokens > 0
|
|
9
|
+
? Math.round((f.tokenCount / totalTokens) * 100)
|
|
10
|
+
: 0,
|
|
11
|
+
}))
|
|
12
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
13
|
+
}
|
|
14
|
+
export function contextPercent(files) {
|
|
15
|
+
const total = files.reduce((sum, f) => sum + f.tokenCount, 0);
|
|
16
|
+
return ((total / CONTEXT_WINDOW) * 100).toFixed(1);
|
|
17
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const CLAUDE_DEFAULTS = [
|
|
2
|
+
{
|
|
3
|
+
pattern: /read.*(?:file|code|related).*before/i,
|
|
4
|
+
reason: "Claude Code already reads files before editing by default",
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
pattern: /parameterized\s+quer/i,
|
|
8
|
+
reason: "Claude already uses parameterized queries by default",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
pattern: /use\s+https/i,
|
|
12
|
+
reason: "Claude defaults to HTTPS in generated code",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
pattern: /don'?t\s+(?:add|use)\s+emoji/i,
|
|
16
|
+
reason: "Claude Code does not use emojis unless asked",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
pattern: /avoid\s+(?:unnecessary|premature)\s+(?:abstraction|optimization)/i,
|
|
20
|
+
reason: "Claude Code system prompt already discourages premature abstraction",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: /prefer\s+edit.*over.*(?:write|create)/i,
|
|
24
|
+
reason: "Claude Code already prefers editing existing files",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pattern: /keep\s+(?:response|output)s?\s+(?:short|concise|brief)/i,
|
|
28
|
+
reason: "Claude Code already aims for concise responses",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /don'?t\s+(?:create|add)\s+(?:unnecessary|extra)\s+files/i,
|
|
32
|
+
reason: "Claude Code already avoids creating unnecessary files",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
pattern: /validate\s+(?:all\s+)?(?:external\s+)?inputs/i,
|
|
36
|
+
reason: "Claude validates inputs in generated code by default",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: /(?:escape|encode|sanitize)\s+(?:output|html)/i,
|
|
40
|
+
reason: "Claude applies output encoding by default",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /don'?t\s+(?:add|include)\s+(?:comments|docstrings)\s+(?:to|for)\s+(?:code\s+)?(?:you\s+)?didn'?t/i,
|
|
44
|
+
reason: "Claude Code already avoids adding comments to unchanged code",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pattern: /don'?t\s+(?:make|suggest)\s+(?:changes|improvements)\s+beyond/i,
|
|
48
|
+
reason: "Claude Code already limits changes to what was requested",
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
export function analyzeDefaults(files) {
|
|
52
|
+
const matches = [];
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
for (const directive of file.directives) {
|
|
55
|
+
for (const def of CLAUDE_DEFAULTS) {
|
|
56
|
+
if (def.pattern.test(directive)) {
|
|
57
|
+
matches.push({
|
|
58
|
+
file: file.filename,
|
|
59
|
+
directive,
|
|
60
|
+
reason: def.reason,
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return matches;
|
|
68
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const STOP_WORDS = new Set([
|
|
2
|
+
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
3
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
4
|
+
"should", "may", "might", "shall", "can", "to", "of", "in", "for",
|
|
5
|
+
"on", "with", "at", "by", "from", "as", "into", "through", "during",
|
|
6
|
+
"before", "after", "and", "but", "or", "not", "no", "if", "then",
|
|
7
|
+
"than", "that", "this", "it", "its", "all", "each", "every", "any",
|
|
8
|
+
"use", "using", "used",
|
|
9
|
+
]);
|
|
10
|
+
const MAX_DIRECTIVE_RESULTS = 50;
|
|
11
|
+
export function analyzeOverlap(files) {
|
|
12
|
+
const duplicates = findDuplicateFiles(files);
|
|
13
|
+
const directiveOverlaps = findDirectiveOverlaps(files);
|
|
14
|
+
return { duplicates, directiveOverlaps };
|
|
15
|
+
}
|
|
16
|
+
function findDuplicateFiles(files) {
|
|
17
|
+
const results = [];
|
|
18
|
+
for (let i = 0; i < files.length; i++) {
|
|
19
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
20
|
+
// Skip if same base filename (intentional per-language copies)
|
|
21
|
+
const baseA = files[i].filename.split("/").pop();
|
|
22
|
+
const baseB = files[j].filename.split("/").pop();
|
|
23
|
+
if (baseA === baseB)
|
|
24
|
+
continue;
|
|
25
|
+
const wordsA = extractKeywords(files[i].content);
|
|
26
|
+
const wordsB = extractKeywords(files[j].content);
|
|
27
|
+
const sim = jaccard(wordsA, wordsB);
|
|
28
|
+
if (sim >= 0.5) {
|
|
29
|
+
results.push({
|
|
30
|
+
fileA: files[i].filename,
|
|
31
|
+
fileB: files[j].filename,
|
|
32
|
+
similarity: Math.round(sim * 100),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return results.sort((a, b) => b.similarity - a.similarity);
|
|
38
|
+
}
|
|
39
|
+
function findDirectiveOverlaps(files) {
|
|
40
|
+
// Index directives by keyword hash for fast lookup instead of N^2
|
|
41
|
+
const index = new Map();
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
for (const d of file.directives) {
|
|
44
|
+
const keywords = extractKeywords(d);
|
|
45
|
+
// Use sorted keywords as a rough hash
|
|
46
|
+
const key = [...keywords].sort().join("|");
|
|
47
|
+
if (!key)
|
|
48
|
+
continue;
|
|
49
|
+
if (!index.has(key))
|
|
50
|
+
index.set(key, []);
|
|
51
|
+
index.get(key).push({ file: file.filename, directive: d });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const results = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
// Exact keyword matches (fastest, highest confidence)
|
|
57
|
+
for (const [, entries] of index) {
|
|
58
|
+
if (entries.length < 2)
|
|
59
|
+
continue;
|
|
60
|
+
// Only compare across different files
|
|
61
|
+
for (let i = 0; i < entries.length && results.length < MAX_DIRECTIVE_RESULTS; i++) {
|
|
62
|
+
for (let j = i + 1; j < entries.length && results.length < MAX_DIRECTIVE_RESULTS; j++) {
|
|
63
|
+
if (entries[i].file === entries[j].file)
|
|
64
|
+
continue;
|
|
65
|
+
const pairKey = [entries[i].file, entries[j].file, entries[i].directive].sort().join("||");
|
|
66
|
+
if (seen.has(pairKey))
|
|
67
|
+
continue;
|
|
68
|
+
seen.add(pairKey);
|
|
69
|
+
results.push({
|
|
70
|
+
fileA: entries[i].file,
|
|
71
|
+
directiveA: entries[i].directive,
|
|
72
|
+
fileB: entries[j].file,
|
|
73
|
+
directiveB: entries[j].directive,
|
|
74
|
+
similarity: 100,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return results.sort((a, b) => b.similarity - a.similarity);
|
|
80
|
+
}
|
|
81
|
+
function extractKeywords(text) {
|
|
82
|
+
return new Set(text
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
85
|
+
.split(/\s+/)
|
|
86
|
+
.filter((w) => w.length > 2 && !STOP_WORDS.has(w)));
|
|
87
|
+
}
|
|
88
|
+
function jaccard(a, b) {
|
|
89
|
+
if (a.size === 0 && b.size === 0)
|
|
90
|
+
return 0;
|
|
91
|
+
let intersection = 0;
|
|
92
|
+
for (const word of a) {
|
|
93
|
+
if (b.has(word))
|
|
94
|
+
intersection++;
|
|
95
|
+
}
|
|
96
|
+
const union = a.size + b.size - intersection;
|
|
97
|
+
return union === 0 ? 0 : intersection / union;
|
|
98
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const STALE_DAYS = 90;
|
|
2
|
+
export function analyzeStaleness(files) {
|
|
3
|
+
const now = Date.now();
|
|
4
|
+
return files
|
|
5
|
+
.map((f) => ({
|
|
6
|
+
file: f.filename,
|
|
7
|
+
daysSinceModified: Math.floor((now - f.mtime.getTime()) / (1000 * 60 * 60 * 24)),
|
|
8
|
+
}))
|
|
9
|
+
.filter((r) => r.daysSinceModified >= STALE_DAYS)
|
|
10
|
+
.sort((a, b) => b.daysSinceModified - a.daysSinceModified);
|
|
11
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const VAGUE_THRESHOLD = 20;
|
|
2
|
+
const ABSTRACT_WORDS = new Set([
|
|
3
|
+
"good", "clean", "proper", "appropriate", "suitable", "nice",
|
|
4
|
+
"better", "best", "right", "correct", "reasonable", "adequate",
|
|
5
|
+
"effective", "efficient", "optimal", "ideal",
|
|
6
|
+
]);
|
|
7
|
+
export function analyzeVagueness(files) {
|
|
8
|
+
const results = [];
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
for (const directive of file.directives) {
|
|
11
|
+
const score = scoreActionability(directive);
|
|
12
|
+
if (score < VAGUE_THRESHOLD) {
|
|
13
|
+
results.push({ file: file.filename, directive, score });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return results.sort((a, b) => a.score - b.score);
|
|
18
|
+
}
|
|
19
|
+
function scoreActionability(directive) {
|
|
20
|
+
let score = 50; // baseline
|
|
21
|
+
// Concrete file/tool/pattern names
|
|
22
|
+
if (/\.[a-z]{2,4}\b/.test(directive))
|
|
23
|
+
score += 15;
|
|
24
|
+
if (/[A-Z][a-z]+(?:[A-Z][a-z]+)+/.test(directive))
|
|
25
|
+
score += 10; // CamelCase
|
|
26
|
+
// Has measurable threshold
|
|
27
|
+
if (/\d+/.test(directive))
|
|
28
|
+
score += 20;
|
|
29
|
+
if (/[<>]=?\s*\d+/.test(directive))
|
|
30
|
+
score += 10;
|
|
31
|
+
// Has code example or format
|
|
32
|
+
if (/`[^`]+`/.test(directive))
|
|
33
|
+
score += 15;
|
|
34
|
+
// NEVER/ALWAYS absolutes
|
|
35
|
+
if (/\b(?:never|always|must)\b/i.test(directive))
|
|
36
|
+
score += 10;
|
|
37
|
+
// Too short without specifics
|
|
38
|
+
if (directive.split(/\s+/).length < 5)
|
|
39
|
+
score -= 20;
|
|
40
|
+
// Too abstract
|
|
41
|
+
const words = directive.toLowerCase().split(/\s+/);
|
|
42
|
+
const abstractCount = words.filter((w) => ABSTRACT_WORDS.has(w)).length;
|
|
43
|
+
if (abstractCount >= 2)
|
|
44
|
+
score -= 30;
|
|
45
|
+
if (abstractCount === 1)
|
|
46
|
+
score -= 10;
|
|
47
|
+
// No verb (not actionable)
|
|
48
|
+
if (!/\b(?:use|add|create|remove|avoid|check|run|write|read|test|validate|apply|set|keep|ensure)\b/i.test(directive)) {
|
|
49
|
+
score -= 15;
|
|
50
|
+
}
|
|
51
|
+
return Math.max(0, Math.min(100, score));
|
|
52
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { scanRuleFiles } from "./scanner.js";
|
|
3
|
+
import { parseRuleFile } from "./parser.js";
|
|
4
|
+
import { analyzeOverlap } from "./analyzers/overlap.js";
|
|
5
|
+
import { analyzeDefaults } from "./analyzers/defaults.js";
|
|
6
|
+
import { analyzeVagueness } from "./analyzers/vagueness.js";
|
|
7
|
+
import { analyzeCost } from "./analyzers/cost.js";
|
|
8
|
+
import { analyzeStaleness } from "./analyzers/staleness.js";
|
|
9
|
+
import { printReport } from "./reporter.js";
|
|
10
|
+
async function main() {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const customPaths = process.argv.slice(2).filter((a) => !a.startsWith("-"));
|
|
13
|
+
const scanResults = await scanRuleFiles(cwd, customPaths);
|
|
14
|
+
if (scanResults.length === 0) {
|
|
15
|
+
console.log("No rule files found.");
|
|
16
|
+
console.log("Usage: deadrule [path...]");
|
|
17
|
+
console.log("Default: scans ~/.claude/rules/, .claude/rules/, CLAUDE.md");
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
const files = await Promise.all(scanResults.map((s) => parseRuleFile(s.path, s.scope)));
|
|
21
|
+
const { duplicates, directiveOverlaps } = analyzeOverlap(files);
|
|
22
|
+
const defaultMatches = analyzeDefaults(files);
|
|
23
|
+
const vagueDirectives = analyzeVagueness(files);
|
|
24
|
+
const costBreakdown = analyzeCost(files);
|
|
25
|
+
const staleRules = analyzeStaleness(files);
|
|
26
|
+
const result = {
|
|
27
|
+
totalFiles: files.length,
|
|
28
|
+
globalCount: files.filter((f) => f.scope === "global").length,
|
|
29
|
+
projectCount: files.filter((f) => f.scope === "project").length,
|
|
30
|
+
totalTokens: files.reduce((sum, f) => sum + f.tokenCount, 0),
|
|
31
|
+
duplicates,
|
|
32
|
+
directiveOverlaps,
|
|
33
|
+
defaultMatches,
|
|
34
|
+
vagueDirectives,
|
|
35
|
+
costBreakdown,
|
|
36
|
+
staleRules,
|
|
37
|
+
};
|
|
38
|
+
printReport(result);
|
|
39
|
+
const issues = duplicates.length +
|
|
40
|
+
directiveOverlaps.length +
|
|
41
|
+
defaultMatches.length +
|
|
42
|
+
vagueDirectives.length +
|
|
43
|
+
staleRules.length;
|
|
44
|
+
process.exit(issues > 0 ? 1 : 0);
|
|
45
|
+
}
|
|
46
|
+
main().catch((err) => {
|
|
47
|
+
console.error("Error:", err.message);
|
|
48
|
+
process.exit(2);
|
|
49
|
+
});
|
package/dist/parser.d.ts
ADDED
package/dist/parser.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { stat, readFile } from "node:fs/promises";
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
export async function parseRuleFile(path, scope) {
|
|
5
|
+
const content = await readFile(path, "utf-8");
|
|
6
|
+
const filename = shortName(path);
|
|
7
|
+
const fileStat = await stat(path);
|
|
8
|
+
const { frontmatter, body } = extractFrontmatter(content);
|
|
9
|
+
const directives = extractDirectives(body);
|
|
10
|
+
const tokenCount = Math.ceil(content.length / 4);
|
|
11
|
+
return {
|
|
12
|
+
path,
|
|
13
|
+
filename,
|
|
14
|
+
scope,
|
|
15
|
+
content,
|
|
16
|
+
frontmatter,
|
|
17
|
+
directives,
|
|
18
|
+
tokenCount,
|
|
19
|
+
mtime: fileStat.mtime,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function extractFrontmatter(content) {
|
|
23
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
24
|
+
if (!match)
|
|
25
|
+
return { frontmatter: {}, body: content };
|
|
26
|
+
const raw = match[1];
|
|
27
|
+
const body = match[2];
|
|
28
|
+
const frontmatter = {};
|
|
29
|
+
for (const line of raw.split("\n")) {
|
|
30
|
+
const sep = line.indexOf(":");
|
|
31
|
+
if (sep === -1)
|
|
32
|
+
continue;
|
|
33
|
+
const key = line.slice(0, sep).trim();
|
|
34
|
+
const value = line.slice(sep + 1).trim();
|
|
35
|
+
if (key)
|
|
36
|
+
frontmatter[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return { frontmatter, body };
|
|
39
|
+
}
|
|
40
|
+
function shortName(filePath) {
|
|
41
|
+
const home = homedir();
|
|
42
|
+
if (filePath.startsWith(home)) {
|
|
43
|
+
return "~/" + relative(home, filePath);
|
|
44
|
+
}
|
|
45
|
+
const cwd = process.cwd();
|
|
46
|
+
if (filePath.startsWith(cwd)) {
|
|
47
|
+
return relative(cwd, filePath);
|
|
48
|
+
}
|
|
49
|
+
// For custom paths, show last 2 segments
|
|
50
|
+
const parts = filePath.split("/");
|
|
51
|
+
return parts.slice(-2).join("/");
|
|
52
|
+
}
|
|
53
|
+
function extractDirectives(body) {
|
|
54
|
+
const directives = [];
|
|
55
|
+
let inCodeBlock = false;
|
|
56
|
+
for (const line of body.split("\n")) {
|
|
57
|
+
if (line.trim().startsWith("```")) {
|
|
58
|
+
inCodeBlock = !inCodeBlock;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (inCodeBlock)
|
|
62
|
+
continue;
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
const match = trimmed.match(/^[-*]\s+(.+)$/) ?? trimmed.match(/^\d+\.\s+(.+)$/);
|
|
65
|
+
if (match) {
|
|
66
|
+
let text = match[1].replace(/\*\*/g, "").trim();
|
|
67
|
+
// Strip checkbox markers
|
|
68
|
+
text = text.replace(/^\[[ x]\]\s*/i, "").trim();
|
|
69
|
+
if (text.length > 20 && isDirective(text))
|
|
70
|
+
directives.push(text);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return directives;
|
|
74
|
+
}
|
|
75
|
+
function isDirective(text) {
|
|
76
|
+
// Skip labels/categories (no verb, just a noun phrase)
|
|
77
|
+
const words = text.split(/\s+/);
|
|
78
|
+
if (words.length <= 3)
|
|
79
|
+
return false;
|
|
80
|
+
// Must contain a verb or actionable keyword
|
|
81
|
+
const hasVerb = /\b(?:use|add|create|remove|avoid|check|run|write|read|test|validate|apply|set|keep|ensure|never|always|must|should|don't|do not|implement|configure|enable|disable|include|exclude|prefer|require|prevent|handle|return|throw|log|call|pass|send|receive|store|load|save|delete|update|verify|confirm|follow|maintain)\b/i.test(text);
|
|
82
|
+
// Or contains technical specifics
|
|
83
|
+
const hasTechnical = /[`<>{}()\[\]\/\\]|=>|->|\.\w+\b/.test(text);
|
|
84
|
+
return hasVerb || hasTechnical;
|
|
85
|
+
}
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const MAX_ITEMS = 10;
|
|
2
|
+
export function printReport(result) {
|
|
3
|
+
const { totalFiles, globalCount, projectCount, totalTokens } = result;
|
|
4
|
+
console.log("");
|
|
5
|
+
console.log("deadrule v0.1.0");
|
|
6
|
+
console.log("================");
|
|
7
|
+
console.log("");
|
|
8
|
+
console.log(`Scanned: ${totalFiles} rules (${globalCount} global, ${projectCount} project)`);
|
|
9
|
+
console.log(`Total token cost: ~${totalTokens.toLocaleString()} tokens (${((totalTokens / 200_000) * 100).toFixed(1)}% of context)`);
|
|
10
|
+
printDuplicates(result);
|
|
11
|
+
printDirectiveOverlaps(result);
|
|
12
|
+
printDefaults(result);
|
|
13
|
+
printVague(result);
|
|
14
|
+
printCost(result);
|
|
15
|
+
printStale(result);
|
|
16
|
+
printSummary(result);
|
|
17
|
+
}
|
|
18
|
+
function printDuplicates(r) {
|
|
19
|
+
if (r.duplicates.length === 0)
|
|
20
|
+
return;
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log("DUPLICATES");
|
|
23
|
+
const shown = r.duplicates.slice(0, MAX_ITEMS);
|
|
24
|
+
for (const d of shown) {
|
|
25
|
+
console.log(` ${d.fileA} <-> ${d.fileB} (${d.similarity}% overlap)`);
|
|
26
|
+
}
|
|
27
|
+
if (r.duplicates.length > MAX_ITEMS) {
|
|
28
|
+
console.log(` ... and ${r.duplicates.length - MAX_ITEMS} more`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function printDirectiveOverlaps(r) {
|
|
32
|
+
if (r.directiveOverlaps.length === 0)
|
|
33
|
+
return;
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log(`OVERLAPPING DIRECTIVES (${r.directiveOverlaps.length} found, showing top ${Math.min(r.directiveOverlaps.length, MAX_ITEMS)})`);
|
|
36
|
+
// Group by directive text to avoid repetitive output
|
|
37
|
+
const grouped = groupOverlaps(r.directiveOverlaps);
|
|
38
|
+
let count = 0;
|
|
39
|
+
for (const [directive, files] of grouped) {
|
|
40
|
+
if (count >= MAX_ITEMS)
|
|
41
|
+
break;
|
|
42
|
+
console.log(` "${truncate(directive, 60)}"`);
|
|
43
|
+
console.log(` -> repeated in: ${files.join(", ")}`);
|
|
44
|
+
console.log("");
|
|
45
|
+
count++;
|
|
46
|
+
}
|
|
47
|
+
if (grouped.size > MAX_ITEMS) {
|
|
48
|
+
console.log(` ... and ${grouped.size - MAX_ITEMS} more patterns`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function groupOverlaps(overlaps) {
|
|
52
|
+
const groups = new Map();
|
|
53
|
+
for (const o of overlaps) {
|
|
54
|
+
// Use the shorter directive as key
|
|
55
|
+
const key = o.directiveA.length <= o.directiveB.length
|
|
56
|
+
? o.directiveA
|
|
57
|
+
: o.directiveB;
|
|
58
|
+
if (!groups.has(key))
|
|
59
|
+
groups.set(key, new Set());
|
|
60
|
+
const set = groups.get(key);
|
|
61
|
+
set.add(o.fileA);
|
|
62
|
+
set.add(o.fileB);
|
|
63
|
+
}
|
|
64
|
+
// Sort by number of files (most repeated first)
|
|
65
|
+
return new Map([...groups.entries()]
|
|
66
|
+
.sort((a, b) => b[1].size - a[1].size)
|
|
67
|
+
.map(([k, v]) => [k, [...v]]));
|
|
68
|
+
}
|
|
69
|
+
function printDefaults(r) {
|
|
70
|
+
if (r.defaultMatches.length === 0)
|
|
71
|
+
return;
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log("REDUNDANT WITH CLAUDE DEFAULTS");
|
|
74
|
+
const shown = r.defaultMatches.slice(0, MAX_ITEMS);
|
|
75
|
+
for (const m of shown) {
|
|
76
|
+
console.log(` ${m.file}: "${truncate(m.directive, 50)}"`);
|
|
77
|
+
console.log(` -> ${m.reason}`);
|
|
78
|
+
console.log("");
|
|
79
|
+
}
|
|
80
|
+
if (r.defaultMatches.length > MAX_ITEMS) {
|
|
81
|
+
console.log(` ... and ${r.defaultMatches.length - MAX_ITEMS} more`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function printVague(r) {
|
|
85
|
+
if (r.vagueDirectives.length === 0)
|
|
86
|
+
return;
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log("VAGUE DIRECTIVES");
|
|
89
|
+
const shown = r.vagueDirectives.slice(0, MAX_ITEMS);
|
|
90
|
+
for (const v of shown) {
|
|
91
|
+
console.log(` ${v.file}: "${truncate(v.directive, 50)}" (${v.score}/100)`);
|
|
92
|
+
}
|
|
93
|
+
if (r.vagueDirectives.length > MAX_ITEMS) {
|
|
94
|
+
console.log(` ... and ${r.vagueDirectives.length - MAX_ITEMS} more`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function printCost(r) {
|
|
98
|
+
if (r.costBreakdown.length === 0)
|
|
99
|
+
return;
|
|
100
|
+
console.log("");
|
|
101
|
+
console.log("TOKEN COST BREAKDOWN (top 10)");
|
|
102
|
+
const maxNameLen = Math.max(...r.costBreakdown.slice(0, MAX_ITEMS).map((c) => c.file.length));
|
|
103
|
+
const shown = r.costBreakdown.slice(0, MAX_ITEMS);
|
|
104
|
+
for (const c of shown) {
|
|
105
|
+
const bar = makeBar(c.percent);
|
|
106
|
+
const name = c.file.padEnd(maxNameLen);
|
|
107
|
+
console.log(` ${name} ~${String(c.tokens).padStart(5)} tokens ${bar} ${c.percent}%`);
|
|
108
|
+
}
|
|
109
|
+
if (r.costBreakdown.length > MAX_ITEMS) {
|
|
110
|
+
const rest = r.costBreakdown.slice(MAX_ITEMS);
|
|
111
|
+
const restTokens = rest.reduce((s, c) => s + c.tokens, 0);
|
|
112
|
+
console.log(` ... ${rest.length} more files (~${restTokens.toLocaleString()} tokens)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function printStale(r) {
|
|
116
|
+
if (r.staleRules.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log("STALE RULES (not modified in 90+ days)");
|
|
120
|
+
const shown = r.staleRules.slice(0, MAX_ITEMS);
|
|
121
|
+
for (const s of shown) {
|
|
122
|
+
console.log(` ${s.file} (${s.daysSinceModified} days)`);
|
|
123
|
+
}
|
|
124
|
+
if (r.staleRules.length > MAX_ITEMS) {
|
|
125
|
+
console.log(` ... and ${r.staleRules.length - MAX_ITEMS} more`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function printSummary(r) {
|
|
129
|
+
const issues = r.duplicates.length +
|
|
130
|
+
r.directiveOverlaps.length +
|
|
131
|
+
r.defaultMatches.length +
|
|
132
|
+
r.vagueDirectives.length +
|
|
133
|
+
r.staleRules.length;
|
|
134
|
+
console.log("");
|
|
135
|
+
console.log("SUMMARY");
|
|
136
|
+
if (issues === 0) {
|
|
137
|
+
console.log(" All rules look healthy!");
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
if (r.duplicates.length > 0) {
|
|
141
|
+
console.log(` ${r.duplicates.length} duplicate pair(s)`);
|
|
142
|
+
}
|
|
143
|
+
if (r.directiveOverlaps.length > 0) {
|
|
144
|
+
console.log(` ${r.directiveOverlaps.length} overlapping directive(s)`);
|
|
145
|
+
}
|
|
146
|
+
if (r.defaultMatches.length > 0) {
|
|
147
|
+
console.log(` ${r.defaultMatches.length} redundant with Claude defaults`);
|
|
148
|
+
}
|
|
149
|
+
if (r.vagueDirectives.length > 0) {
|
|
150
|
+
console.log(` ${r.vagueDirectives.length} vague directive(s)`);
|
|
151
|
+
}
|
|
152
|
+
if (r.staleRules.length > 0) {
|
|
153
|
+
console.log(` ${r.staleRules.length} stale rule(s)`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
console.log("");
|
|
157
|
+
}
|
|
158
|
+
function makeBar(percent) {
|
|
159
|
+
const filled = Math.round(percent / 10);
|
|
160
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
161
|
+
}
|
|
162
|
+
function truncate(text, max) {
|
|
163
|
+
return text.length > max ? text.slice(0, max - 3) + "..." : text;
|
|
164
|
+
}
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { existsSync, statSync } from "node:fs";
|
|
5
|
+
export async function scanRuleFiles(cwd, customPaths = []) {
|
|
6
|
+
// Custom paths take priority — skip default scanning
|
|
7
|
+
if (customPaths.length > 0) {
|
|
8
|
+
const results = [];
|
|
9
|
+
for (const p of customPaths) {
|
|
10
|
+
const abs = resolve(p);
|
|
11
|
+
if (!existsSync(abs))
|
|
12
|
+
continue;
|
|
13
|
+
if (statSync(abs).isDirectory()) {
|
|
14
|
+
const files = await scanDirRecursive(abs, "project");
|
|
15
|
+
results.push(...files);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
results.push({ path: abs, scope: "project" });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return results;
|
|
22
|
+
}
|
|
23
|
+
const results = [];
|
|
24
|
+
// Global rules: ~/.claude/rules/*.md
|
|
25
|
+
const globalDir = join(homedir(), ".claude", "rules");
|
|
26
|
+
const globalFiles = await scanDir(globalDir, "global");
|
|
27
|
+
results.push(...globalFiles);
|
|
28
|
+
// Project rules: .claude/rules/*.md
|
|
29
|
+
const projectDir = join(cwd, ".claude", "rules");
|
|
30
|
+
const projectFiles = await scanDir(projectDir, "project");
|
|
31
|
+
results.push(...projectFiles);
|
|
32
|
+
// Project CLAUDE.md
|
|
33
|
+
const claudeMd = join(cwd, "CLAUDE.md");
|
|
34
|
+
if (existsSync(claudeMd)) {
|
|
35
|
+
results.push({ path: claudeMd, scope: "project" });
|
|
36
|
+
}
|
|
37
|
+
// Cursor rules
|
|
38
|
+
const cursorRules = join(cwd, ".cursorrules");
|
|
39
|
+
if (existsSync(cursorRules)) {
|
|
40
|
+
results.push({ path: cursorRules, scope: "project" });
|
|
41
|
+
}
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
async function scanDir(dir, scope) {
|
|
45
|
+
if (!existsSync(dir))
|
|
46
|
+
return [];
|
|
47
|
+
const entries = await readdir(dir);
|
|
48
|
+
return entries
|
|
49
|
+
.filter((f) => f.endsWith(".md"))
|
|
50
|
+
.map((f) => ({ path: join(dir, f), scope }));
|
|
51
|
+
}
|
|
52
|
+
async function scanDirRecursive(dir, scope) {
|
|
53
|
+
if (!existsSync(dir))
|
|
54
|
+
return [];
|
|
55
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
56
|
+
const results = [];
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const full = join(dir, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
const sub = await scanDirRecursive(full, scope);
|
|
61
|
+
results.push(...sub);
|
|
62
|
+
}
|
|
63
|
+
else if (entry.name.endsWith(".md")) {
|
|
64
|
+
results.push({ path: full, scope });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface RuleFile {
|
|
2
|
+
path: string;
|
|
3
|
+
filename: string;
|
|
4
|
+
scope: "global" | "project";
|
|
5
|
+
content: string;
|
|
6
|
+
frontmatter: Record<string, string>;
|
|
7
|
+
directives: string[];
|
|
8
|
+
tokenCount: number;
|
|
9
|
+
mtime: Date;
|
|
10
|
+
}
|
|
11
|
+
export interface OverlapResult {
|
|
12
|
+
fileA: string;
|
|
13
|
+
fileB: string;
|
|
14
|
+
similarity: number;
|
|
15
|
+
}
|
|
16
|
+
export interface DirectiveOverlap {
|
|
17
|
+
fileA: string;
|
|
18
|
+
directiveA: string;
|
|
19
|
+
fileB: string;
|
|
20
|
+
directiveB: string;
|
|
21
|
+
similarity: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DefaultMatch {
|
|
24
|
+
file: string;
|
|
25
|
+
directive: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
export interface VagueDirective {
|
|
29
|
+
file: string;
|
|
30
|
+
directive: string;
|
|
31
|
+
score: number;
|
|
32
|
+
}
|
|
33
|
+
export interface CostEntry {
|
|
34
|
+
file: string;
|
|
35
|
+
tokens: number;
|
|
36
|
+
percent: number;
|
|
37
|
+
}
|
|
38
|
+
export interface StaleRule {
|
|
39
|
+
file: string;
|
|
40
|
+
daysSinceModified: number;
|
|
41
|
+
}
|
|
42
|
+
export interface AnalysisResult {
|
|
43
|
+
totalFiles: number;
|
|
44
|
+
globalCount: number;
|
|
45
|
+
projectCount: number;
|
|
46
|
+
totalTokens: number;
|
|
47
|
+
duplicates: OverlapResult[];
|
|
48
|
+
directiveOverlaps: DirectiveOverlap[];
|
|
49
|
+
defaultMatches: DefaultMatch[];
|
|
50
|
+
vagueDirectives: VagueDirective[];
|
|
51
|
+
costBreakdown: CostEntry[];
|
|
52
|
+
staleRules: StaleRule[];
|
|
53
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deadrule",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Find dead rules in your AI coding assistant config",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"deadrule": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude-code",
|
|
21
|
+
"cursor",
|
|
22
|
+
"ai-coding",
|
|
23
|
+
"rules",
|
|
24
|
+
"linter"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"typescript": "^5.7.0",
|
|
29
|
+
"@types/node": "^22.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|