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 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,3 @@
1
+ import { RuleFile, CostEntry } from "../types.js";
2
+ export declare function analyzeCost(files: RuleFile[]): CostEntry[];
3
+ export declare function contextPercent(files: RuleFile[]): string;
@@ -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,2 @@
1
+ import { RuleFile, DefaultMatch } from "../types.js";
2
+ export declare function analyzeDefaults(files: RuleFile[]): DefaultMatch[];
@@ -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,5 @@
1
+ import { RuleFile, OverlapResult, DirectiveOverlap } from "../types.js";
2
+ export declare function analyzeOverlap(files: RuleFile[]): {
3
+ duplicates: OverlapResult[];
4
+ directiveOverlaps: DirectiveOverlap[];
5
+ };
@@ -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,2 @@
1
+ import { RuleFile, StaleRule } from "../types.js";
2
+ export declare function analyzeStaleness(files: RuleFile[]): StaleRule[];
@@ -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,2 @@
1
+ import { RuleFile, VagueDirective } from "../types.js";
2
+ export declare function analyzeVagueness(files: RuleFile[]): VagueDirective[];
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ });
@@ -0,0 +1,2 @@
1
+ import { RuleFile } from "./types.js";
2
+ export declare function parseRuleFile(path: string, scope: "global" | "project"): Promise<RuleFile>;
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
+ }
@@ -0,0 +1,2 @@
1
+ import { AnalysisResult } from "./types.js";
2
+ export declare function printReport(result: AnalysisResult): void;
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ interface ScanResult {
2
+ path: string;
3
+ scope: "global" | "project";
4
+ }
5
+ export declare function scanRuleFiles(cwd: string, customPaths?: string[]): Promise<ScanResult[]>;
6
+ export {};
@@ -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
+ }
@@ -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
+ }