deadrule 0.1.0 → 0.2.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.
@@ -0,0 +1,12 @@
1
+ import { RuleFile } from "../types.js";
2
+ export interface UsageResult {
3
+ file: string;
4
+ domain: string;
5
+ reason: string;
6
+ }
7
+ export interface UsageStats {
8
+ sessionsScanned: number;
9
+ extensionsFound: Map<string, number>;
10
+ deadRules: UsageResult[];
11
+ }
12
+ export declare function analyzeUsage(files: RuleFile[]): Promise<UsageStats>;
@@ -0,0 +1,118 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join, extname } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { existsSync } from "node:fs";
5
+ // Map rule files to their target domain
6
+ const DOMAIN_PATTERNS = [
7
+ { name: "Python", match: /python|pytorch/i, extensions: [".py", ".pyx", ".pyi"] },
8
+ { name: "TypeScript", match: /typescript/i, extensions: [".ts", ".tsx"] },
9
+ { name: "JavaScript/Web", match: /javascript|web/i, extensions: [".js", ".jsx", ".ts", ".tsx", ".html", ".css"] },
10
+ { name: "Rust", match: /rust/i, extensions: [".rs"] },
11
+ { name: "Go", match: /golang|\/go\//i, extensions: [".go"] },
12
+ { name: "Java", match: /java\b/i, extensions: [".java"] },
13
+ { name: "Kotlin", match: /kotlin/i, extensions: [".kt", ".kts"] },
14
+ { name: "Swift", match: /swift/i, extensions: [".swift"] },
15
+ { name: "Dart/Flutter", match: /dart|flutter/i, extensions: [".dart"] },
16
+ { name: "C#", match: /csharp|c#|\.cs/i, extensions: [".cs"] },
17
+ { name: "C++", match: /cpp|c\+\+/i, extensions: [".cpp", ".cc", ".h", ".hpp"] },
18
+ { name: "PHP", match: /php/i, extensions: [".php"] },
19
+ { name: "Perl", match: /perl/i, extensions: [".pl", ".pm"] },
20
+ { name: "Ruby", match: /ruby/i, extensions: [".rb"] },
21
+ ];
22
+ export async function analyzeUsage(files) {
23
+ const editedExtensions = await collectEditedExtensions();
24
+ const sessionsScanned = editedExtensions.sessionCount;
25
+ const extensionsFound = editedExtensions.counts;
26
+ if (sessionsScanned === 0) {
27
+ return { sessionsScanned: 0, extensionsFound, deadRules: [] };
28
+ }
29
+ const deadRules = [];
30
+ for (const rule of files) {
31
+ const domain = detectDomain(rule);
32
+ if (!domain)
33
+ continue;
34
+ const hasActivity = domain.extensions.some((ext) => extensionsFound.has(ext));
35
+ if (!hasActivity) {
36
+ deadRules.push({
37
+ file: rule.filename,
38
+ domain: domain.name,
39
+ reason: `No ${domain.name} files edited in ${sessionsScanned} sessions`,
40
+ });
41
+ }
42
+ }
43
+ return { sessionsScanned, extensionsFound, deadRules };
44
+ }
45
+ function detectDomain(rule) {
46
+ const text = rule.filename + " " + rule.path;
47
+ for (const dp of DOMAIN_PATTERNS) {
48
+ if (dp.match.test(text)) {
49
+ return { name: dp.name, extensions: dp.extensions };
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ async function collectEditedExtensions() {
55
+ const claudeDir = join(homedir(), ".claude", "projects");
56
+ if (!existsSync(claudeDir)) {
57
+ return { sessionCount: 0, counts: new Map() };
58
+ }
59
+ const counts = new Map();
60
+ let sessionCount = 0;
61
+ const projects = await readdir(claudeDir);
62
+ for (const project of projects) {
63
+ const projectDir = join(claudeDir, project);
64
+ const files = await safeReaddir(projectDir);
65
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
66
+ for (const jsonlFile of jsonlFiles) {
67
+ sessionCount++;
68
+ const filePath = join(projectDir, jsonlFile);
69
+ const extensions = await extractExtensionsFromLog(filePath);
70
+ for (const ext of extensions) {
71
+ counts.set(ext, (counts.get(ext) || 0) + 1);
72
+ }
73
+ }
74
+ }
75
+ return { sessionCount, counts };
76
+ }
77
+ async function extractExtensionsFromLog(logPath) {
78
+ const extensions = new Set();
79
+ try {
80
+ const content = await readFile(logPath, "utf-8");
81
+ const lines = content.split("\n").filter(Boolean);
82
+ for (const line of lines) {
83
+ try {
84
+ const obj = JSON.parse(line);
85
+ if (obj.type !== "assistant" || !obj.message?.content)
86
+ continue;
87
+ for (const block of obj.message.content) {
88
+ if (block.type !== "tool_use")
89
+ continue;
90
+ if (block.name !== "Edit" && block.name !== "Write")
91
+ continue;
92
+ const filePath = block.input?.file_path;
93
+ if (!filePath || typeof filePath !== "string")
94
+ continue;
95
+ const ext = extname(filePath).toLowerCase();
96
+ if (ext && ext !== ".md" && ext !== ".json" && ext !== ".txt") {
97
+ extensions.add(ext);
98
+ }
99
+ }
100
+ }
101
+ catch {
102
+ // skip malformed lines
103
+ }
104
+ }
105
+ }
106
+ catch {
107
+ // skip unreadable files
108
+ }
109
+ return extensions;
110
+ }
111
+ async function safeReaddir(dir) {
112
+ try {
113
+ return await readdir(dir);
114
+ }
115
+ catch {
116
+ return [];
117
+ }
118
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { analyzeDefaults } from "./analyzers/defaults.js";
6
6
  import { analyzeVagueness } from "./analyzers/vagueness.js";
7
7
  import { analyzeCost } from "./analyzers/cost.js";
8
8
  import { analyzeStaleness } from "./analyzers/staleness.js";
9
+ import { analyzeUsage } from "./analyzers/usage.js";
9
10
  import { printReport } from "./reporter.js";
10
11
  async function main() {
11
12
  const cwd = process.cwd();
@@ -23,6 +24,7 @@ async function main() {
23
24
  const vagueDirectives = analyzeVagueness(files);
24
25
  const costBreakdown = analyzeCost(files);
25
26
  const staleRules = analyzeStaleness(files);
27
+ const usageStats = await analyzeUsage(files);
26
28
  const result = {
27
29
  totalFiles: files.length,
28
30
  globalCount: files.filter((f) => f.scope === "global").length,
@@ -34,13 +36,21 @@ async function main() {
34
36
  vagueDirectives,
35
37
  costBreakdown,
36
38
  staleRules,
39
+ usage: usageStats.sessionsScanned > 0
40
+ ? {
41
+ sessionsScanned: usageStats.sessionsScanned,
42
+ extensionsFound: [...usageStats.extensionsFound.entries()].sort((a, b) => b[1] - a[1]),
43
+ deadRules: usageStats.deadRules,
44
+ }
45
+ : undefined,
37
46
  };
38
47
  printReport(result);
39
48
  const issues = duplicates.length +
40
49
  directiveOverlaps.length +
41
50
  defaultMatches.length +
42
51
  vagueDirectives.length +
43
- staleRules.length;
52
+ staleRules.length +
53
+ (result.usage?.deadRules.length ?? 0);
44
54
  process.exit(issues > 0 ? 1 : 0);
45
55
  }
46
56
  main().catch((err) => {
package/dist/reporter.js CHANGED
@@ -7,6 +7,7 @@ export function printReport(result) {
7
7
  console.log("");
8
8
  console.log(`Scanned: ${totalFiles} rules (${globalCount} global, ${projectCount} project)`);
9
9
  console.log(`Total token cost: ~${totalTokens.toLocaleString()} tokens (${((totalTokens / 200_000) * 100).toFixed(1)}% of context)`);
10
+ printUsage(result);
10
11
  printDuplicates(result);
11
12
  printDirectiveOverlaps(result);
12
13
  printDefaults(result);
@@ -125,18 +126,47 @@ function printStale(r) {
125
126
  console.log(` ... and ${r.staleRules.length - MAX_ITEMS} more`);
126
127
  }
127
128
  }
129
+ function printUsage(r) {
130
+ if (!r.usage)
131
+ return;
132
+ const { sessionsScanned, extensionsFound, deadRules } = r.usage;
133
+ console.log("");
134
+ console.log(`DEAD RULES (based on ${sessionsScanned} sessions across all projects)`);
135
+ if (extensionsFound.length > 0) {
136
+ console.log(" Languages you actually use:");
137
+ const shown = extensionsFound.slice(0, 8);
138
+ for (const [ext, count] of shown) {
139
+ console.log(` ${ext.padEnd(8)} ${count} file edits`);
140
+ }
141
+ }
142
+ if (deadRules.length === 0) {
143
+ console.log(" No dead rules found — all rules match your usage.");
144
+ }
145
+ else {
146
+ console.log("");
147
+ console.log(" Rules for languages you NEVER use:");
148
+ for (const d of deadRules) {
149
+ console.log(` ${d.file}`);
150
+ console.log(` -> ${d.reason}`);
151
+ }
152
+ }
153
+ }
128
154
  function printSummary(r) {
129
155
  const issues = r.duplicates.length +
130
156
  r.directiveOverlaps.length +
131
157
  r.defaultMatches.length +
132
158
  r.vagueDirectives.length +
133
- r.staleRules.length;
159
+ r.staleRules.length +
160
+ (r.usage?.deadRules.length ?? 0);
134
161
  console.log("");
135
162
  console.log("SUMMARY");
136
163
  if (issues === 0) {
137
164
  console.log(" All rules look healthy!");
138
165
  }
139
166
  else {
167
+ if (r.usage && r.usage.deadRules.length > 0) {
168
+ console.log(` ${r.usage.deadRules.length} DEAD rule(s) — never relevant to your work`);
169
+ }
140
170
  if (r.duplicates.length > 0) {
141
171
  console.log(` ${r.duplicates.length} duplicate pair(s)`);
142
172
  }
package/dist/types.d.ts CHANGED
@@ -39,6 +39,16 @@ export interface StaleRule {
39
39
  file: string;
40
40
  daysSinceModified: number;
41
41
  }
42
+ export interface DeadByUsage {
43
+ file: string;
44
+ domain: string;
45
+ reason: string;
46
+ }
47
+ export interface UsageSummary {
48
+ sessionsScanned: number;
49
+ extensionsFound: [string, number][];
50
+ deadRules: DeadByUsage[];
51
+ }
42
52
  export interface AnalysisResult {
43
53
  totalFiles: number;
44
54
  globalCount: number;
@@ -50,4 +60,5 @@ export interface AnalysisResult {
50
60
  vagueDirectives: VagueDirective[];
51
61
  costBreakdown: CostEntry[];
52
62
  staleRules: StaleRule[];
63
+ usage?: UsageSummary;
53
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deadrule",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Find dead rules in your AI coding assistant config",
5
5
  "type": "module",
6
6
  "bin": {