instrlint 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/dist/cli.cjs ADDED
@@ -0,0 +1,2943 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // node_modules/.pnpm/tsup@8.5.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
27
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
28
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
+
30
+ // src/cli.ts
31
+ var import_commander = require("commander");
32
+
33
+ // src/commands/run-command.ts
34
+ var import_child_process = require("child_process");
35
+ var import_path8 = require("path");
36
+ var import_chalk4 = __toESM(require("chalk"), 1);
37
+
38
+ // src/core/scanner.ts
39
+ var import_fs = require("fs");
40
+ var import_path = require("path");
41
+ function detectAll(projectRoot) {
42
+ const detections = [];
43
+ const claudeDir = (0, import_path.join)(projectRoot, ".claude");
44
+ if ((0, import_fs.existsSync)(claudeDir)) {
45
+ const rootInDir = (0, import_path.join)(claudeDir, "CLAUDE.md");
46
+ const rootAtRoot = (0, import_path.join)(projectRoot, "CLAUDE.md");
47
+ detections.push({
48
+ tool: "claude-code",
49
+ rootFilePath: (0, import_fs.existsSync)(rootInDir) ? rootInDir : (0, import_fs.existsSync)(rootAtRoot) ? rootAtRoot : null,
50
+ configDir: claudeDir
51
+ });
52
+ }
53
+ const agentsDir = (0, import_path.join)(projectRoot, ".agents");
54
+ if ((0, import_fs.existsSync)(agentsDir)) {
55
+ const agentsMd = (0, import_path.join)(projectRoot, "AGENTS.md");
56
+ detections.push({
57
+ tool: "codex",
58
+ rootFilePath: (0, import_fs.existsSync)(agentsMd) ? agentsMd : null,
59
+ configDir: agentsDir
60
+ });
61
+ }
62
+ const codexDir = (0, import_path.join)(projectRoot, ".codex");
63
+ if ((0, import_fs.existsSync)(codexDir) && !detections.some((d) => d.tool === "codex")) {
64
+ const agentsMd = (0, import_path.join)(projectRoot, "AGENTS.md");
65
+ detections.push({
66
+ tool: "codex",
67
+ rootFilePath: (0, import_fs.existsSync)(agentsMd) ? agentsMd : null,
68
+ configDir: codexDir
69
+ });
70
+ }
71
+ const cursorDir = (0, import_path.join)(projectRoot, ".cursor");
72
+ const cursorRules = (0, import_path.join)(projectRoot, ".cursorrules");
73
+ if ((0, import_fs.existsSync)(cursorDir) || (0, import_fs.existsSync)(cursorRules)) {
74
+ detections.push({
75
+ tool: "cursor",
76
+ rootFilePath: (0, import_fs.existsSync)(cursorRules) ? cursorRules : null,
77
+ configDir: (0, import_fs.existsSync)(cursorDir) ? cursorDir : null
78
+ });
79
+ }
80
+ return detections;
81
+ }
82
+ function detectByRootFile(projectRoot) {
83
+ const claudeMd = (0, import_path.join)(projectRoot, "CLAUDE.md");
84
+ if ((0, import_fs.existsSync)(claudeMd)) {
85
+ return { tool: "claude-code", rootFilePath: claudeMd, configDir: null };
86
+ }
87
+ const agentsMd = (0, import_path.join)(projectRoot, "AGENTS.md");
88
+ if ((0, import_fs.existsSync)(agentsMd)) {
89
+ return { tool: "codex", rootFilePath: agentsMd, configDir: null };
90
+ }
91
+ return null;
92
+ }
93
+ function scanProject(projectRoot, forceTool) {
94
+ if (!(0, import_fs.existsSync)(projectRoot)) {
95
+ throw new Error(`Project root does not exist: ${projectRoot}`);
96
+ }
97
+ if (forceTool != null) {
98
+ const valid = ["claude-code", "codex", "cursor"];
99
+ if (!valid.includes(forceTool)) {
100
+ throw new Error(
101
+ `Invalid tool: "${forceTool}". Must be one of: ${valid.join(", ")}`
102
+ );
103
+ }
104
+ const tool = forceTool;
105
+ const detections2 = detectAll(projectRoot);
106
+ const match = detections2.find((d) => d.tool === tool);
107
+ return {
108
+ tool,
109
+ rootFilePath: match?.rootFilePath ?? null,
110
+ configDir: match?.configDir ?? null,
111
+ confidence: "high"
112
+ };
113
+ }
114
+ const detections = detectAll(projectRoot);
115
+ if (detections.length === 0) {
116
+ const byFile = detectByRootFile(projectRoot);
117
+ if (byFile != null) {
118
+ return { ...byFile, confidence: "low" };
119
+ }
120
+ return { tool: "unknown", rootFilePath: null, configDir: null, confidence: "low" };
121
+ }
122
+ if (detections.length === 1) {
123
+ const d = detections[0];
124
+ return { tool: d.tool, rootFilePath: d.rootFilePath, configDir: d.configDir, confidence: "high" };
125
+ }
126
+ const first = detections[0];
127
+ return {
128
+ tool: first.tool,
129
+ rootFilePath: first.rootFilePath,
130
+ configDir: first.configDir,
131
+ confidence: "ambiguous"
132
+ };
133
+ }
134
+
135
+ // src/adapters/claude-code.ts
136
+ var import_fs3 = require("fs");
137
+ var import_path2 = require("path");
138
+
139
+ // src/core/parser.ts
140
+ var import_fs2 = require("fs");
141
+ var KNOWN_KEYWORDS = /* @__PURE__ */ new Set([
142
+ "typescript",
143
+ "javascript",
144
+ "eslint",
145
+ "prettier",
146
+ "biome",
147
+ "react",
148
+ "vue",
149
+ "svelte",
150
+ "angular",
151
+ "next",
152
+ "nextjs",
153
+ "nuxt",
154
+ "jest",
155
+ "vitest",
156
+ "mocha",
157
+ "jasmine",
158
+ "playwright",
159
+ "cypress",
160
+ "postgresql",
161
+ "postgres",
162
+ "mysql",
163
+ "sqlite",
164
+ "mongodb",
165
+ "redis",
166
+ "docker",
167
+ "kubernetes",
168
+ "k8s",
169
+ "terraform",
170
+ "git",
171
+ "github",
172
+ "gitlab",
173
+ "npm",
174
+ "pnpm",
175
+ "yarn",
176
+ "bun",
177
+ "node",
178
+ "nodejs",
179
+ "deno",
180
+ "webpack",
181
+ "vite",
182
+ "esbuild",
183
+ "tsup",
184
+ "rollup",
185
+ "zod",
186
+ "prisma",
187
+ "drizzle",
188
+ "openai",
189
+ "anthropic",
190
+ "claude",
191
+ "commitlint",
192
+ "husky",
193
+ "lint-staged"
194
+ ]);
195
+ var KEYWORD_REGEX = new RegExp(
196
+ `\\b(${[...KNOWN_KEYWORDS].join("|")})\\b`,
197
+ "gi"
198
+ );
199
+ var PATH_REGEX = /(?<!\w)(?:\.{1,2}\/|(?:src|tests?|dist|lib|docs?|config|scripts?|packages?)\/)[^\s,;`'")\]>]+/g;
200
+ var RULE_IMPERATIVE_WORDS = /\b(must|should|never|always|prefer|avoid|ensure|require|forbid|use|do not|don't)\b/i;
201
+ var RULE_NEGATION_PATTERN = /\b(not|don't|do not)\s+\w+/i;
202
+ var STRONG_IMPERATIVE = /\b(must|shall|always|never)\b/i;
203
+ function parseYamlFrontmatter(content) {
204
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/m.exec(content);
205
+ if (match == null) {
206
+ return { body: content };
207
+ }
208
+ const yaml = match[1] ?? "";
209
+ const body = match[2] ?? "";
210
+ const paths = extractYamlStringArray(yaml, "paths");
211
+ const globs = extractYamlStringArray(yaml, "globs");
212
+ return {
213
+ ...paths != null ? { paths } : {},
214
+ ...globs != null ? { globs } : {},
215
+ body
216
+ };
217
+ }
218
+ function extractYamlStringArray(yaml, key) {
219
+ const blockMatch = new RegExp(
220
+ `^${key}:\\s*\\n((?:\\s+-\\s+.+\\n?)*)`,
221
+ "m"
222
+ ).exec(yaml);
223
+ if (blockMatch != null) {
224
+ return (blockMatch[1] ?? "").split("\n").map(
225
+ (l) => l.replace(/^\s+-\s+/, "").replace(/["']/g, "").trim()
226
+ ).filter((l) => l.length > 0);
227
+ }
228
+ const inlineMatch = new RegExp(`^${key}:\\s*\\[(.+)\\]`, "m").exec(yaml);
229
+ if (inlineMatch != null) {
230
+ return (inlineMatch[1] ?? "").split(",").map((s) => s.replace(/["']/g, "").trim()).filter((s) => s.length > 0);
231
+ }
232
+ return void 0;
233
+ }
234
+ function classifyLine(text, inCodeBlock, inHtmlComment) {
235
+ if (inCodeBlock) return "code";
236
+ if (inHtmlComment) return "comment";
237
+ const trimmed = text.trim();
238
+ if (trimmed.length === 0) return "blank";
239
+ if (/^#{1,6}\s/.test(trimmed)) return "heading";
240
+ if (trimmed.startsWith("```")) return "code";
241
+ if (/^<!--[\s\S]*-->$/.test(trimmed)) return "comment";
242
+ if (trimmed.startsWith("<!--")) return "comment";
243
+ if (isRule(trimmed)) return "rule";
244
+ return "other";
245
+ }
246
+ function isRule(text) {
247
+ const isList = text.startsWith("- ");
248
+ const body = isList ? text.slice(2) : text;
249
+ if (isList) {
250
+ if (RULE_IMPERATIVE_WORDS.test(body)) return true;
251
+ if (RULE_NEGATION_PATTERN.test(body)) return true;
252
+ } else {
253
+ if (STRONG_IMPERATIVE.test(body) && /^[A-Z]/.test(body)) return true;
254
+ if (RULE_IMPERATIVE_WORDS.test(body) && /^[A-Z][a-z]/.test(body))
255
+ return true;
256
+ }
257
+ return false;
258
+ }
259
+ function extractKeywords(text) {
260
+ const matches = text.matchAll(KEYWORD_REGEX);
261
+ const found = /* @__PURE__ */ new Set();
262
+ for (const m of matches) {
263
+ found.add(m[0].toLowerCase());
264
+ }
265
+ return [...found];
266
+ }
267
+ function extractPaths(text) {
268
+ const matches = text.matchAll(PATH_REGEX);
269
+ const found = [];
270
+ for (const m of matches) {
271
+ const cleaned = m[0].replace(/[.,;)>\]'"]+$/, "");
272
+ if (cleaned.length > 1) found.push(cleaned);
273
+ }
274
+ return [...new Set(found)];
275
+ }
276
+ function parseInstructionFile(filePath) {
277
+ const raw = (0, import_fs2.readFileSync)(filePath, "utf8");
278
+ const rawLines = raw.split(/\r?\n/);
279
+ if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
280
+ rawLines.pop();
281
+ }
282
+ let inCodeBlock = false;
283
+ let inHtmlComment = false;
284
+ const lines = [];
285
+ for (let i = 0; i < rawLines.length; i++) {
286
+ const text = rawLines[i] ?? "";
287
+ const trimmed = text.trim();
288
+ const type = classifyLine(text, inCodeBlock, inHtmlComment);
289
+ if (!inHtmlComment && trimmed.startsWith("```")) {
290
+ inCodeBlock = !inCodeBlock;
291
+ }
292
+ if (!inCodeBlock) {
293
+ if (inHtmlComment) {
294
+ if (trimmed.includes("-->")) inHtmlComment = false;
295
+ } else if (trimmed.startsWith("<!--") && !trimmed.includes("-->")) {
296
+ inHtmlComment = true;
297
+ }
298
+ }
299
+ lines.push({
300
+ lineNumber: i + 1,
301
+ text,
302
+ type,
303
+ keywords: extractKeywords(text),
304
+ referencedPaths: extractPaths(text)
305
+ });
306
+ }
307
+ return {
308
+ path: filePath,
309
+ lines,
310
+ lineCount: lines.length,
311
+ // tokenCount and tokenMethod will be filled by the adapter/estimator
312
+ tokenCount: 0,
313
+ tokenMethod: "estimated"
314
+ };
315
+ }
316
+
317
+ // src/detectors/token-estimator.ts
318
+ var encoder = null;
319
+ var initPromise = (async () => {
320
+ try {
321
+ const { getEncoding } = await import("js-tiktoken");
322
+ encoder = getEncoding("cl100k_base");
323
+ } catch {
324
+ process.stderr.write(
325
+ "[instrlint] Warning: js-tiktoken failed to load \u2014 falling back to character estimation\n"
326
+ );
327
+ encoder = null;
328
+ }
329
+ })();
330
+ async function ensureInitialized() {
331
+ await initPromise;
332
+ }
333
+ var CJK_REGEX = /[\u3000-\u9fff\uac00-\ud7af\uf900-\ufaff]/g;
334
+ function cjkRatio(text) {
335
+ if (text.length === 0) return 0;
336
+ const matches = text.match(CJK_REGEX);
337
+ return (matches?.length ?? 0) / text.length;
338
+ }
339
+ function countTokens(text) {
340
+ if (text.length === 0) return { count: 0, method: "measured" };
341
+ if (encoder != null) {
342
+ try {
343
+ return { count: encoder.encode(text).length, method: "measured" };
344
+ } catch {
345
+ }
346
+ }
347
+ return estimateFallback(text);
348
+ }
349
+ function estimateFallback(text) {
350
+ const ratio = cjkRatio(text);
351
+ const charsPerToken = 4 * (1 - ratio) + 2 * ratio;
352
+ return {
353
+ count: Math.ceil(text.length / charsPerToken),
354
+ method: "estimated"
355
+ };
356
+ }
357
+ function estimateMcpTokens(config) {
358
+ if (config.toolCount != null) {
359
+ return { count: config.toolCount * 400, method: "estimated" };
360
+ }
361
+ return { count: 2500, method: "estimated" };
362
+ }
363
+
364
+ // src/adapters/claude-code.ts
365
+ function withTokens(file) {
366
+ const raw = (() => {
367
+ try {
368
+ return (0, import_fs3.readFileSync)(file.path, "utf8");
369
+ } catch {
370
+ return "";
371
+ }
372
+ })();
373
+ const { count, method } = countTokens(raw);
374
+ return { ...file, tokenCount: count, tokenMethod: method };
375
+ }
376
+ function safeParseFile(filePath) {
377
+ try {
378
+ const file = parseInstructionFile(filePath);
379
+ return withTokens(file);
380
+ } catch {
381
+ process.stderr.write(
382
+ `[instrlint] Warning: could not read ${filePath}, skipping
383
+ `
384
+ );
385
+ return null;
386
+ }
387
+ }
388
+ function findRootFile(projectRoot) {
389
+ const candidates = [
390
+ (0, import_path2.join)(projectRoot, "CLAUDE.md"),
391
+ (0, import_path2.join)(projectRoot, ".claude", "CLAUDE.md")
392
+ ];
393
+ return candidates.find(import_fs3.existsSync) ?? null;
394
+ }
395
+ function loadRules(projectRoot) {
396
+ const rulesDir = (0, import_path2.join)(projectRoot, ".claude", "rules");
397
+ if (!(0, import_fs3.existsSync)(rulesDir)) return [];
398
+ const rules = [];
399
+ let entries = [];
400
+ try {
401
+ entries = (0, import_fs3.readdirSync)(rulesDir).filter((f) => f.endsWith(".md"));
402
+ } catch {
403
+ return [];
404
+ }
405
+ for (const filename of entries) {
406
+ const filePath = (0, import_path2.join)(rulesDir, filename);
407
+ try {
408
+ const raw = (0, import_fs3.readFileSync)(filePath, "utf8");
409
+ const { paths, globs, body } = parseYamlFrontmatter(raw);
410
+ const baseFile = parseInstructionFile(filePath);
411
+ const { count, method } = countTokens(body);
412
+ rules.push({
413
+ ...baseFile,
414
+ tokenCount: count,
415
+ tokenMethod: method,
416
+ ...paths != null ? { paths } : {},
417
+ ...globs != null ? { globs } : {}
418
+ });
419
+ } catch {
420
+ process.stderr.write(
421
+ `[instrlint] Warning: could not parse rule ${filePath}, skipping
422
+ `
423
+ );
424
+ }
425
+ }
426
+ return rules;
427
+ }
428
+ function loadSkills(projectRoot) {
429
+ const skillsDir = (0, import_path2.join)(projectRoot, ".claude", "skills");
430
+ if (!(0, import_fs3.existsSync)(skillsDir)) return [];
431
+ const skills = [];
432
+ let entries = [];
433
+ try {
434
+ entries = (0, import_fs3.readdirSync)(skillsDir);
435
+ } catch {
436
+ return [];
437
+ }
438
+ for (const skillName of entries) {
439
+ const skillFile = (0, import_path2.join)(skillsDir, skillName, "SKILL.md");
440
+ if (!(0, import_fs3.existsSync)(skillFile)) continue;
441
+ const parsed = safeParseFile(skillFile);
442
+ if (parsed != null) {
443
+ skills.push({ ...parsed, skillName });
444
+ }
445
+ }
446
+ return skills;
447
+ }
448
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
449
+ "node_modules",
450
+ "dist",
451
+ ".git",
452
+ ".claude",
453
+ ".turbo",
454
+ "coverage"
455
+ ]);
456
+ function findSubClaudeFiles(dir, projectRoot, depth = 0) {
457
+ if (depth > 10) return [];
458
+ const results = [];
459
+ let entries = [];
460
+ try {
461
+ entries = (0, import_fs3.readdirSync)(dir);
462
+ } catch {
463
+ return [];
464
+ }
465
+ for (const entry of entries) {
466
+ if (SKIP_DIRS.has(entry)) continue;
467
+ const full = (0, import_path2.join)(dir, entry);
468
+ try {
469
+ const stat = (0, import_fs3.statSync)(full);
470
+ if (stat.isDirectory()) {
471
+ results.push(...findSubClaudeFiles(full, projectRoot, depth + 1));
472
+ } else if (entry === "CLAUDE.md") {
473
+ if ((0, import_path2.relative)(projectRoot, full) === "CLAUDE.md") continue;
474
+ const parsed = safeParseFile(full);
475
+ if (parsed != null) results.push(parsed);
476
+ }
477
+ } catch {
478
+ }
479
+ }
480
+ return results;
481
+ }
482
+ function parseMcpServers(projectRoot) {
483
+ const candidates = [
484
+ (0, import_path2.join)(projectRoot, ".claude", "settings.json"),
485
+ (0, import_path2.join)(projectRoot, ".claude", "settings.local.json")
486
+ ];
487
+ const servers = [];
488
+ for (const settingsPath of candidates) {
489
+ if (!(0, import_fs3.existsSync)(settingsPath)) continue;
490
+ try {
491
+ const raw = (0, import_fs3.readFileSync)(settingsPath, "utf8");
492
+ const parsed = JSON.parse(raw);
493
+ if (parsed == null || typeof parsed !== "object" || !("mcpServers" in parsed)) {
494
+ continue;
495
+ }
496
+ const mcpServers = parsed.mcpServers;
497
+ for (const [name, entry] of Object.entries(mcpServers)) {
498
+ const toolCount = Array.isArray(entry.tools) ? entry.tools.length : void 0;
499
+ const config = {
500
+ name,
501
+ estimatedTokens: 0,
502
+ ...toolCount !== void 0 ? { toolCount } : {}
503
+ };
504
+ const { count } = estimateMcpTokens(config);
505
+ servers.push({ ...config, estimatedTokens: count });
506
+ }
507
+ } catch {
508
+ process.stderr.write(
509
+ `[instrlint] Warning: could not parse MCP config in ${settingsPath}, skipping
510
+ `
511
+ );
512
+ }
513
+ }
514
+ return servers;
515
+ }
516
+ function loadClaudeCodeProject(projectRoot) {
517
+ const rootFilePath = findRootFile(projectRoot);
518
+ const rootFile = rootFilePath != null ? safeParseFile(rootFilePath) ?? emptyFile(rootFilePath) : emptyFile((0, import_path2.join)(projectRoot, "CLAUDE.md"));
519
+ const rules = loadRules(projectRoot);
520
+ const skills = loadSkills(projectRoot);
521
+ const subFiles = findSubClaudeFiles(projectRoot, projectRoot);
522
+ const mcpServers = parseMcpServers(projectRoot);
523
+ return {
524
+ tool: "claude-code",
525
+ rootFile,
526
+ rules,
527
+ skills,
528
+ subFiles,
529
+ mcpServers
530
+ };
531
+ }
532
+ function emptyFile(path) {
533
+ return {
534
+ path,
535
+ lines: [],
536
+ lineCount: 0,
537
+ tokenCount: 0,
538
+ tokenMethod: "estimated"
539
+ };
540
+ }
541
+
542
+ // src/adapters/codex.ts
543
+ var import_fs4 = require("fs");
544
+ var import_path3 = require("path");
545
+ function safeParseFile2(filePath) {
546
+ try {
547
+ const raw = (0, import_fs4.readFileSync)(filePath, "utf8");
548
+ const file = parseInstructionFile(filePath);
549
+ const { count, method } = countTokens(raw);
550
+ return { ...file, tokenCount: count, tokenMethod: method };
551
+ } catch {
552
+ process.stderr.write(
553
+ `[instrlint] Warning: could not read ${filePath}, skipping
554
+ `
555
+ );
556
+ return null;
557
+ }
558
+ }
559
+ function emptyFile2(path) {
560
+ return {
561
+ path,
562
+ lines: [],
563
+ lineCount: 0,
564
+ tokenCount: 0,
565
+ tokenMethod: "estimated"
566
+ };
567
+ }
568
+ function loadSkills2(projectRoot) {
569
+ const skillsDir = (0, import_path3.join)(projectRoot, ".agents", "skills");
570
+ if (!(0, import_fs4.existsSync)(skillsDir)) return [];
571
+ const skills = [];
572
+ let entries = [];
573
+ try {
574
+ entries = (0, import_fs4.readdirSync)(skillsDir);
575
+ } catch {
576
+ return [];
577
+ }
578
+ for (const skillName of entries) {
579
+ const skillFile = (0, import_path3.join)(skillsDir, skillName, "SKILL.md");
580
+ if (!(0, import_fs4.existsSync)(skillFile)) continue;
581
+ const parsed = safeParseFile2(skillFile);
582
+ if (parsed != null) {
583
+ skills.push({ ...parsed, skillName });
584
+ }
585
+ }
586
+ return skills;
587
+ }
588
+ function parseMcpFromToml(toml) {
589
+ const servers = [];
590
+ const sectionRe = /^\[mcp_servers\.([^\]]+)\]/;
591
+ const keyValueRe = /^(\w+)\s*=\s*(.+)$/;
592
+ let currentName = null;
593
+ let currentTools;
594
+ const flush = () => {
595
+ if (currentName != null) {
596
+ const config = {
597
+ name: currentName,
598
+ estimatedTokens: 0,
599
+ ...currentTools !== void 0 ? { toolCount: currentTools.length } : {}
600
+ };
601
+ const { count } = estimateMcpTokens(config);
602
+ servers.push({ ...config, estimatedTokens: count });
603
+ }
604
+ currentName = null;
605
+ currentTools = void 0;
606
+ };
607
+ for (const line of toml.split("\n")) {
608
+ const trimmed = line.trim();
609
+ if (trimmed.startsWith("#") || trimmed === "") continue;
610
+ const sectionMatch = sectionRe.exec(trimmed);
611
+ if (sectionMatch != null) {
612
+ flush();
613
+ currentName = sectionMatch[1];
614
+ continue;
615
+ }
616
+ if (currentName == null) continue;
617
+ const kvMatch = keyValueRe.exec(trimmed);
618
+ if (kvMatch == null) continue;
619
+ const key = kvMatch[1];
620
+ const raw = kvMatch[2].trim();
621
+ if (key === "tools") {
622
+ const items = raw.match(/"([^"]+)"/g);
623
+ currentTools = items != null ? items.map((s) => s.slice(1, -1)) : [];
624
+ }
625
+ }
626
+ flush();
627
+ return servers;
628
+ }
629
+ function loadMcpServers(projectRoot) {
630
+ const tomlPath = (0, import_path3.join)(projectRoot, ".codex", "config.toml");
631
+ if (!(0, import_fs4.existsSync)(tomlPath)) return [];
632
+ try {
633
+ const raw = (0, import_fs4.readFileSync)(tomlPath, "utf8");
634
+ return parseMcpFromToml(raw);
635
+ } catch {
636
+ process.stderr.write(
637
+ `[instrlint] Warning: could not parse MCP config in ${tomlPath}, skipping
638
+ `
639
+ );
640
+ return [];
641
+ }
642
+ }
643
+ function loadCodexProject(projectRoot) {
644
+ const rootFilePath = (0, import_path3.join)(projectRoot, "AGENTS.md");
645
+ const rootFile = (0, import_fs4.existsSync)(rootFilePath) ? safeParseFile2(rootFilePath) ?? emptyFile2(rootFilePath) : emptyFile2(rootFilePath);
646
+ const skills = loadSkills2(projectRoot);
647
+ const mcpServers = loadMcpServers(projectRoot);
648
+ return {
649
+ tool: "codex",
650
+ rootFile,
651
+ rules: [],
652
+ skills,
653
+ subFiles: [],
654
+ mcpServers
655
+ };
656
+ }
657
+
658
+ // src/adapters/cursor.ts
659
+ var import_fs5 = require("fs");
660
+ var import_path4 = require("path");
661
+ function safeParseFile3(filePath) {
662
+ try {
663
+ const raw = (0, import_fs5.readFileSync)(filePath, "utf8");
664
+ const file = parseInstructionFile(filePath);
665
+ const { count, method } = countTokens(raw);
666
+ return { ...file, tokenCount: count, tokenMethod: method };
667
+ } catch {
668
+ process.stderr.write(
669
+ `[instrlint] Warning: could not read ${filePath}, skipping
670
+ `
671
+ );
672
+ return null;
673
+ }
674
+ }
675
+ function emptyFile3(path) {
676
+ return {
677
+ path,
678
+ lines: [],
679
+ lineCount: 0,
680
+ tokenCount: 0,
681
+ tokenMethod: "estimated"
682
+ };
683
+ }
684
+ function findRootFile2(projectRoot) {
685
+ const cursorRules = (0, import_path4.join)(projectRoot, ".cursorrules");
686
+ if ((0, import_fs5.existsSync)(cursorRules)) return cursorRules;
687
+ return null;
688
+ }
689
+ function loadRules2(projectRoot) {
690
+ const rulesDir = (0, import_path4.join)(projectRoot, ".cursor", "rules");
691
+ if (!(0, import_fs5.existsSync)(rulesDir)) return [];
692
+ const rules = [];
693
+ let entries = [];
694
+ try {
695
+ entries = (0, import_fs5.readdirSync)(rulesDir).filter((f) => f.endsWith(".md"));
696
+ } catch {
697
+ return [];
698
+ }
699
+ for (const filename of entries) {
700
+ const filePath = (0, import_path4.join)(rulesDir, filename);
701
+ try {
702
+ const raw = (0, import_fs5.readFileSync)(filePath, "utf8");
703
+ const { globs, body } = parseYamlFrontmatter(raw);
704
+ const baseFile = parseInstructionFile(filePath);
705
+ const { count, method } = countTokens(body);
706
+ rules.push({
707
+ ...baseFile,
708
+ tokenCount: count,
709
+ tokenMethod: method,
710
+ ...globs != null ? { globs } : {}
711
+ });
712
+ } catch {
713
+ process.stderr.write(
714
+ `[instrlint] Warning: could not parse rule ${filePath}, skipping
715
+ `
716
+ );
717
+ }
718
+ }
719
+ return rules;
720
+ }
721
+ function loadMcpServers2(projectRoot) {
722
+ const mcpPath = (0, import_path4.join)(projectRoot, ".cursor", "mcp.json");
723
+ if (!(0, import_fs5.existsSync)(mcpPath)) return [];
724
+ try {
725
+ const raw = (0, import_fs5.readFileSync)(mcpPath, "utf8");
726
+ const parsed = JSON.parse(raw);
727
+ if (parsed == null || typeof parsed !== "object" || !("mcpServers" in parsed)) {
728
+ return [];
729
+ }
730
+ const mcpServers = parsed.mcpServers;
731
+ return Object.entries(mcpServers).map(([name, entry]) => {
732
+ const toolCount = Array.isArray(entry.tools) ? entry.tools.length : void 0;
733
+ const config = {
734
+ name,
735
+ estimatedTokens: 0,
736
+ ...toolCount !== void 0 ? { toolCount } : {}
737
+ };
738
+ const { count } = estimateMcpTokens(config);
739
+ return { ...config, estimatedTokens: count };
740
+ });
741
+ } catch {
742
+ process.stderr.write(
743
+ `[instrlint] Warning: could not parse MCP config in ${mcpPath}, skipping
744
+ `
745
+ );
746
+ return [];
747
+ }
748
+ }
749
+ function loadCursorProject(projectRoot) {
750
+ const rootFilePath = findRootFile2(projectRoot);
751
+ const rootFile = rootFilePath != null ? safeParseFile3(rootFilePath) ?? emptyFile3(rootFilePath) : emptyFile3((0, import_path4.join)(projectRoot, ".cursorrules"));
752
+ const rules = loadRules2(projectRoot);
753
+ const mcpServers = loadMcpServers2(projectRoot);
754
+ return {
755
+ tool: "cursor",
756
+ rootFile,
757
+ rules,
758
+ skills: [],
759
+ subFiles: [],
760
+ mcpServers
761
+ };
762
+ }
763
+
764
+ // src/adapters/dispatch.ts
765
+ function loadProject(projectRoot, tool) {
766
+ switch (tool) {
767
+ case "claude-code":
768
+ return loadClaudeCodeProject(projectRoot);
769
+ case "codex":
770
+ return loadCodexProject(projectRoot);
771
+ case "cursor":
772
+ return loadCursorProject(projectRoot);
773
+ default:
774
+ return loadClaudeCodeProject(projectRoot);
775
+ }
776
+ }
777
+
778
+ // src/analyzers/budget.ts
779
+ var CONTEXT_WINDOW = 2e5;
780
+ var SYSTEM_PROMPT_TOKENS = 12e3;
781
+ var WARN_LINE_THRESHOLD = 200;
782
+ var CRITICAL_LINE_THRESHOLD = 400;
783
+ var WARN_BASELINE_PCT = 0.25;
784
+ var MCP_INFO_THRESHOLD = 1e4;
785
+ function sumTokens(items) {
786
+ if (items.length === 0) return { tokens: 0, method: "measured" };
787
+ const tokens = items.reduce((acc, f) => acc + f.tokenCount, 0);
788
+ const method = items.every((f) => f.tokenMethod === "measured") ? "measured" : "estimated";
789
+ return { tokens, method };
790
+ }
791
+ function analyzeBudget(instructions) {
792
+ const findings = [];
793
+ const rootFileTokens = instructions.rootFile.tokenCount;
794
+ const rootFileMethod = instructions.rootFile.tokenMethod;
795
+ const rootLines = instructions.rootFile.lineCount;
796
+ if (rootLines > CRITICAL_LINE_THRESHOLD) {
797
+ findings.push({
798
+ severity: "critical",
799
+ category: "budget",
800
+ file: instructions.rootFile.path,
801
+ messageKey: "budget.rootFileCritical",
802
+ messageParams: { lines: String(rootLines) },
803
+ suggestion: `Root instruction file is ${rootLines} lines \u2014 agent compliance drops significantly above 200 lines`,
804
+ autoFixable: false
805
+ });
806
+ } else if (rootLines > WARN_LINE_THRESHOLD) {
807
+ findings.push({
808
+ severity: "warning",
809
+ category: "budget",
810
+ file: instructions.rootFile.path,
811
+ messageKey: "budget.rootFileWarning",
812
+ messageParams: { lines: String(rootLines) },
813
+ suggestion: `Root instruction file is ${rootLines} lines (recommended: < 200)`,
814
+ autoFixable: false
815
+ });
816
+ }
817
+ const { tokens: rulesTokens, method: rulesMethod } = sumTokens(instructions.rules);
818
+ const { tokens: skillsTokens, method: skillsMethod } = sumTokens(instructions.skills);
819
+ const { tokens: subFilesTokens, method: subFilesMethod } = sumTokens(
820
+ instructions.subFiles
821
+ );
822
+ const mcpTokens = instructions.mcpServers.reduce(
823
+ (acc, s) => acc + s.estimatedTokens,
824
+ 0
825
+ );
826
+ for (const server of instructions.mcpServers) {
827
+ if (server.estimatedTokens > MCP_INFO_THRESHOLD) {
828
+ findings.push({
829
+ severity: "info",
830
+ category: "budget",
831
+ file: ".claude/settings.json",
832
+ messageKey: "budget.mcpLargeServer",
833
+ messageParams: {
834
+ name: server.name,
835
+ tokens: server.estimatedTokens.toLocaleString("en")
836
+ },
837
+ suggestion: `MCP server '${server.name}' consumes ~${server.estimatedTokens.toLocaleString("en")} tokens`,
838
+ autoFixable: false
839
+ });
840
+ }
841
+ }
842
+ const totalBaseline = SYSTEM_PROMPT_TOKENS + rootFileTokens + rulesTokens + skillsTokens + subFilesTokens + mcpTokens;
843
+ const availableTokens = CONTEXT_WINDOW - totalBaseline;
844
+ const pct2 = totalBaseline / CONTEXT_WINDOW;
845
+ if (pct2 > WARN_BASELINE_PCT) {
846
+ findings.push({
847
+ severity: "warning",
848
+ category: "budget",
849
+ file: instructions.rootFile.path,
850
+ messageKey: "budget.baselineHigh",
851
+ messageParams: { pct: Math.round(pct2 * 100).toString() },
852
+ suggestion: `Baseline context consumption is ${Math.round(pct2 * 100)}% of window`,
853
+ autoFixable: false
854
+ });
855
+ }
856
+ const tokenMethod = [rootFileMethod, rulesMethod, skillsMethod, subFilesMethod].every(
857
+ (m) => m === "measured"
858
+ ) ? "measured" : "estimated";
859
+ const fileBreakdown = [
860
+ { path: instructions.rootFile.path, tokenCount: rootFileTokens, tokenMethod: rootFileMethod },
861
+ ...instructions.rules.map((r) => ({
862
+ path: r.path,
863
+ tokenCount: r.tokenCount,
864
+ tokenMethod: r.tokenMethod
865
+ })),
866
+ ...instructions.skills.map((s) => ({
867
+ path: s.path,
868
+ tokenCount: s.tokenCount,
869
+ tokenMethod: s.tokenMethod
870
+ })),
871
+ ...instructions.subFiles.map((f) => ({
872
+ path: f.path,
873
+ tokenCount: f.tokenCount,
874
+ tokenMethod: f.tokenMethod
875
+ }))
876
+ ];
877
+ const summary = {
878
+ systemPromptTokens: SYSTEM_PROMPT_TOKENS,
879
+ rootFileTokens,
880
+ rootFileMethod,
881
+ rulesTokens,
882
+ rulesMethod,
883
+ skillsTokens,
884
+ skillsMethod,
885
+ subFilesTokens,
886
+ subFilesMethod,
887
+ mcpTokens,
888
+ totalBaseline,
889
+ availableTokens,
890
+ fileBreakdown,
891
+ tokenMethod
892
+ };
893
+ return { findings, summary };
894
+ }
895
+
896
+ // src/detectors/config-overlap.ts
897
+ var import_fs6 = require("fs");
898
+ var import_path5 = require("path");
899
+ function readJsonFile(projectRoot, filename) {
900
+ try {
901
+ const content = (0, import_fs6.readFileSync)((0, import_path5.join)(projectRoot, filename), "utf8");
902
+ return JSON.parse(content);
903
+ } catch {
904
+ return null;
905
+ }
906
+ }
907
+ function readTextFile(projectRoot, filename) {
908
+ try {
909
+ return (0, import_fs6.readFileSync)((0, import_path5.join)(projectRoot, filename), "utf8");
910
+ } catch {
911
+ return null;
912
+ }
913
+ }
914
+ function checkEditorConfig(projectRoot, key) {
915
+ const content = readTextFile(projectRoot, ".editorconfig");
916
+ if (!content) return false;
917
+ return new RegExp(`^${key}\\s*=\\s*.+`, "m").test(content);
918
+ }
919
+ function checkPrettierConfig(projectRoot, field) {
920
+ for (const filename of [".prettierrc", ".prettierrc.json"]) {
921
+ const parsed = readJsonFile(projectRoot, filename);
922
+ if (parsed !== null && typeof parsed === "object" && parsed !== null) {
923
+ if (field in parsed) return true;
924
+ }
925
+ }
926
+ return false;
927
+ }
928
+ function getPrettierField(projectRoot, field) {
929
+ for (const filename of [".prettierrc", ".prettierrc.json"]) {
930
+ const parsed = readJsonFile(projectRoot, filename);
931
+ if (parsed !== null && typeof parsed === "object") {
932
+ const obj = parsed;
933
+ if (field in obj) return obj[field];
934
+ }
935
+ }
936
+ return void 0;
937
+ }
938
+ function checkEslintRule(projectRoot, ruleName) {
939
+ const parsed = readJsonFile(projectRoot, ".eslintrc.json");
940
+ if (!parsed || typeof parsed !== "object") return false;
941
+ const rules = parsed["rules"];
942
+ if (!rules || typeof rules !== "object") return false;
943
+ const val = rules[ruleName];
944
+ if (val === void 0) return false;
945
+ if (val === "off" || val === 0) return false;
946
+ if (Array.isArray(val) && (val[0] === "off" || val[0] === 0)) return false;
947
+ return true;
948
+ }
949
+ var PATTERNS = [
950
+ {
951
+ id: "ts-strict",
952
+ rulePattern: /\b(typescript|ts)\b.*\bstrict\b|\bstrict\s*(mode|typing)/i,
953
+ configCheck: (root) => {
954
+ const tsconfig = readJsonFile(root, "tsconfig.json");
955
+ if (!tsconfig || typeof tsconfig !== "object") return false;
956
+ const opts = tsconfig["compilerOptions"];
957
+ if (!opts || typeof opts !== "object") return false;
958
+ return opts["strict"] === true;
959
+ },
960
+ configName: "tsconfig.json (compilerOptions.strict: true)"
961
+ },
962
+ {
963
+ id: "indent-spaces",
964
+ rulePattern: /\b(2|two|4|four)\s*(-?\s*)space\s*indent/i,
965
+ configCheck: (root) => checkEditorConfig(root, "indent_size") || checkPrettierConfig(root, "tabWidth"),
966
+ configName: ".editorconfig / .prettierrc (indentation)"
967
+ },
968
+ {
969
+ id: "import-order",
970
+ rulePattern: /import\s*(order|sort)|sort\s*import/i,
971
+ configCheck: (root) => {
972
+ const pkg = readJsonFile(root, "package.json");
973
+ if (!pkg || typeof pkg !== "object") return false;
974
+ const devDeps = pkg["devDependencies"];
975
+ const hasPlugin = devDeps && typeof devDeps === "object" && "eslint-plugin-import" in devDeps;
976
+ return hasPlugin === true && checkEslintRule(root, "import/order");
977
+ },
978
+ configName: "eslint-plugin-import (import/order)"
979
+ },
980
+ {
981
+ id: "conventional-commit",
982
+ rulePattern: /conventional\s*commit/i,
983
+ configCheck: (root) => {
984
+ const pkg = readJsonFile(root, "package.json");
985
+ if (pkg && typeof pkg === "object" && "commitlint" in pkg)
986
+ return true;
987
+ const configFiles = [
988
+ "commitlint.config.js",
989
+ "commitlint.config.cjs",
990
+ "commitlint.config.ts",
991
+ ".commitlintrc",
992
+ ".commitlintrc.json",
993
+ ".commitlintrc.yaml",
994
+ ".commitlintrc.yml"
995
+ ];
996
+ return configFiles.some((f) => (0, import_fs6.existsSync)((0, import_path5.join)(root, f)));
997
+ },
998
+ configName: "commitlint config"
999
+ },
1000
+ {
1001
+ id: "semicolons",
1002
+ rulePattern: /\b(semicolons?|always\s*use\s*;|semi\s*colon)/i,
1003
+ configCheck: (root) => checkPrettierConfig(root, "semi"),
1004
+ configName: ".prettierrc (semi)"
1005
+ },
1006
+ {
1007
+ id: "single-quote",
1008
+ rulePattern: /single\s*quotes?|prefer\s*'|use\s*'/i,
1009
+ configCheck: (root) => getPrettierField(root, "singleQuote") === true,
1010
+ configName: ".prettierrc (singleQuote: true)"
1011
+ },
1012
+ {
1013
+ id: "trailing-comma",
1014
+ rulePattern: /trailing\s*comma/i,
1015
+ configCheck: (root) => checkPrettierConfig(root, "trailingComma"),
1016
+ configName: ".prettierrc (trailingComma)"
1017
+ },
1018
+ {
1019
+ id: "max-line-length",
1020
+ rulePattern: /\b(max|maximum)\s*(line\s*)?(length|width|chars?)\b|\bprint\s*width\b/i,
1021
+ configCheck: (root) => checkPrettierConfig(root, "printWidth"),
1022
+ configName: ".prettierrc (printWidth)"
1023
+ },
1024
+ {
1025
+ id: "no-console",
1026
+ rulePattern: /\b(no|avoid|remove)\b.*\bconsole\.(log|warn|error)/i,
1027
+ configCheck: (root) => checkEslintRule(root, "no-console"),
1028
+ configName: "eslint (no-console)"
1029
+ },
1030
+ {
1031
+ id: "no-unused-vars",
1032
+ rulePattern: /\b(no|remove|avoid)\s*(unused|dead)\s*(var|variable|import)/i,
1033
+ configCheck: (root) => {
1034
+ const tsconfig = readJsonFile(root, "tsconfig.json");
1035
+ if (tsconfig && typeof tsconfig === "object") {
1036
+ const opts = tsconfig["compilerOptions"];
1037
+ if (opts && typeof opts === "object") {
1038
+ const o = opts;
1039
+ if (o["noUnusedLocals"] === true || o["noUnusedParameters"] === true)
1040
+ return true;
1041
+ }
1042
+ }
1043
+ return checkEslintRule(root, "no-unused-vars") || checkEslintRule(root, "@typescript-eslint/no-unused-vars");
1044
+ },
1045
+ configName: "tsconfig / eslint (no-unused-vars)"
1046
+ },
1047
+ {
1048
+ id: "test-framework",
1049
+ rulePattern: /\b(use|prefer)\s*(jest|vitest|mocha|jasmine)\b/i,
1050
+ configCheck: (root) => {
1051
+ const configs = [
1052
+ "vitest.config.ts",
1053
+ "vitest.config.js",
1054
+ "jest.config.ts",
1055
+ "jest.config.js",
1056
+ "jest.config.cjs",
1057
+ ".mocharc.js",
1058
+ ".mocharc.json",
1059
+ ".mocharc.yaml"
1060
+ ];
1061
+ if (configs.some((f) => (0, import_fs6.existsSync)((0, import_path5.join)(root, f)))) return true;
1062
+ const pkg = readJsonFile(root, "package.json");
1063
+ if (!pkg || typeof pkg !== "object") return false;
1064
+ const devDeps = pkg["devDependencies"] ?? {};
1065
+ return ["jest", "vitest", "mocha", "jasmine"].some(
1066
+ (fw) => typeof devDeps === "object" && fw in devDeps
1067
+ );
1068
+ },
1069
+ configName: "test framework config file"
1070
+ },
1071
+ {
1072
+ id: "formatter",
1073
+ rulePattern: /\b(format|prettier|biome)\s*(code|files|on\s*save)/i,
1074
+ configCheck: (root) => {
1075
+ const prettierFiles = [
1076
+ ".prettierrc",
1077
+ ".prettierrc.json",
1078
+ ".prettierrc.yaml",
1079
+ ".prettierrc.yml"
1080
+ ];
1081
+ if (prettierFiles.some((f) => (0, import_fs6.existsSync)((0, import_path5.join)(root, f)))) return true;
1082
+ return (0, import_fs6.existsSync)((0, import_path5.join)(root, "biome.json"));
1083
+ },
1084
+ configName: "prettier / biome config file"
1085
+ },
1086
+ {
1087
+ id: "end-of-line",
1088
+ rulePattern: /\b(line\s*ending|eol|crlf|lf)\b/i,
1089
+ configCheck: (root) => checkEditorConfig(root, "end_of_line") || checkPrettierConfig(root, "endOfLine"),
1090
+ configName: ".editorconfig / .prettierrc (endOfLine)"
1091
+ },
1092
+ {
1093
+ id: "tab-width",
1094
+ rulePattern: /\btab\s*(width|size)\b/i,
1095
+ configCheck: (root) => checkEditorConfig(root, "tab_width") || checkEditorConfig(root, "indent_size") || checkPrettierConfig(root, "tabWidth"),
1096
+ configName: ".editorconfig / .prettierrc (tabWidth)"
1097
+ },
1098
+ {
1099
+ id: "no-default-export",
1100
+ rulePattern: /\b(no|avoid|prefer\s*named)\s*(default\s*export)/i,
1101
+ configCheck: (root) => checkEslintRule(root, "import/no-default-export") || checkEslintRule(root, "no-restricted-exports"),
1102
+ configName: "eslint-plugin-import (no-default-export)"
1103
+ }
1104
+ ];
1105
+ function collectRuleLines(instructions) {
1106
+ const result = [];
1107
+ for (const l of instructions.rootFile.lines) {
1108
+ if (l.type === "rule")
1109
+ result.push({ line: l, file: instructions.rootFile.path });
1110
+ }
1111
+ for (const sub of instructions.subFiles) {
1112
+ for (const l of sub.lines) {
1113
+ if (l.type === "rule") result.push({ line: l, file: sub.path });
1114
+ }
1115
+ }
1116
+ for (const rule of instructions.rules) {
1117
+ for (const l of rule.lines) {
1118
+ if (l.type === "rule") result.push({ line: l, file: rule.path });
1119
+ }
1120
+ }
1121
+ return result;
1122
+ }
1123
+ function detectConfigOverlaps(instructions, projectRoot) {
1124
+ const findings = [];
1125
+ const ruleLines = collectRuleLines(instructions);
1126
+ for (const { line, file } of ruleLines) {
1127
+ for (const pattern of PATTERNS) {
1128
+ if (pattern.rulePattern.test(line.text) && pattern.configCheck(projectRoot)) {
1129
+ const ruleText = line.text.trim();
1130
+ const short = ruleText.length > 60 ? ruleText.slice(0, 60) + "\u2026" : ruleText;
1131
+ findings.push({
1132
+ severity: "warning",
1133
+ category: "dead-rule",
1134
+ file,
1135
+ line: line.lineNumber,
1136
+ messageKey: "deadRule.configOverlap",
1137
+ messageParams: {
1138
+ rule: ruleText.slice(0, 80),
1139
+ config: pattern.configName
1140
+ },
1141
+ suggestion: `Rule "${short}" is already enforced by ${pattern.configName}`,
1142
+ autoFixable: true
1143
+ });
1144
+ break;
1145
+ }
1146
+ }
1147
+ }
1148
+ return findings;
1149
+ }
1150
+
1151
+ // src/utils/text.ts
1152
+ var STOP_WORDS = /* @__PURE__ */ new Set([
1153
+ "the",
1154
+ "a",
1155
+ "an",
1156
+ "is",
1157
+ "are",
1158
+ "to",
1159
+ "for",
1160
+ "and",
1161
+ "or",
1162
+ "in",
1163
+ "of",
1164
+ "with",
1165
+ "that",
1166
+ "this"
1167
+ ]);
1168
+ function tokenizeWords(text) {
1169
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1);
1170
+ }
1171
+ function removeStopWords(words) {
1172
+ return words.filter((w) => !STOP_WORDS.has(w));
1173
+ }
1174
+ function jaccardSimilarity(setA, setB) {
1175
+ if (setA.length === 0 && setB.length === 0) return 0;
1176
+ const a = new Set(setA);
1177
+ const b = new Set(setB);
1178
+ let intersection = 0;
1179
+ for (const word of a) {
1180
+ if (b.has(word)) intersection++;
1181
+ }
1182
+ const union = (/* @__PURE__ */ new Set([...a, ...b])).size;
1183
+ return union === 0 ? 0 : intersection / union;
1184
+ }
1185
+
1186
+ // src/detectors/duplicate.ts
1187
+ var SIMILARITY_THRESHOLD = 0.7;
1188
+ var MIN_WORDS_AFTER_STOP = 4;
1189
+ function collectRuleLines2(instructions) {
1190
+ const result = [];
1191
+ const add = (lines, file) => {
1192
+ for (const l of lines) {
1193
+ if (l.type !== "rule") continue;
1194
+ const words = removeStopWords(tokenizeWords(l.text));
1195
+ if (words.length < MIN_WORDS_AFTER_STOP) continue;
1196
+ result.push({ line: l, file, words });
1197
+ }
1198
+ };
1199
+ add(instructions.rootFile.lines, instructions.rootFile.path);
1200
+ for (const sub of instructions.subFiles) add(sub.lines, sub.path);
1201
+ for (const rule of instructions.rules) add(rule.lines, rule.path);
1202
+ return result;
1203
+ }
1204
+ function detectDuplicates(instructions) {
1205
+ const findings = [];
1206
+ const ruleLines = collectRuleLines2(instructions);
1207
+ const reported = /* @__PURE__ */ new Set();
1208
+ for (let i = 0; i < ruleLines.length; i++) {
1209
+ for (let j = i + 1; j < ruleLines.length; j++) {
1210
+ const a = ruleLines[i];
1211
+ const b = ruleLines[j];
1212
+ const pairKey = `${a.file}:${a.line.lineNumber}|${b.file}:${b.line.lineNumber}`;
1213
+ if (reported.has(pairKey)) continue;
1214
+ const sim = jaccardSimilarity(a.words, b.words);
1215
+ if (sim < SIMILARITY_THRESHOLD) continue;
1216
+ reported.add(pairKey);
1217
+ const isExact = sim >= 1;
1218
+ const simPct = `${Math.round(sim * 100)}`;
1219
+ findings.push({
1220
+ severity: isExact ? "warning" : "info",
1221
+ category: "duplicate",
1222
+ file: b.file,
1223
+ line: b.line.lineNumber,
1224
+ messageKey: isExact ? "deadRule.exactDuplicate" : "deadRule.nearDuplicate",
1225
+ messageParams: {
1226
+ otherFile: a.file,
1227
+ otherLine: String(a.line.lineNumber),
1228
+ similarity: simPct
1229
+ },
1230
+ suggestion: isExact ? `Exact duplicate of line ${a.line.lineNumber} in ${a.file}` : `Very similar to line ${a.line.lineNumber} in ${a.file} (${simPct}% similar)`,
1231
+ autoFixable: isExact
1232
+ });
1233
+ }
1234
+ }
1235
+ return findings;
1236
+ }
1237
+
1238
+ // src/analyzers/dead-rules.ts
1239
+ function analyzeDeadRules(instructions, projectRoot) {
1240
+ return {
1241
+ findings: [
1242
+ ...detectConfigOverlaps(instructions, projectRoot),
1243
+ ...detectDuplicates(instructions)
1244
+ ]
1245
+ };
1246
+ }
1247
+
1248
+ // src/detectors/contradiction.ts
1249
+ var NEGATION_WORDS = ["never", "don't", "avoid", "forbid"];
1250
+ function isNegated(text, word) {
1251
+ const sentences = text.split(/[.!?]+\s+/);
1252
+ const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1253
+ const wordPresent = new RegExp(`\\b${escapedWord}\\b`, "i");
1254
+ for (const sentence of sentences) {
1255
+ if (!wordPresent.test(sentence)) continue;
1256
+ const lower = sentence.toLowerCase();
1257
+ for (const neg of NEGATION_WORDS) {
1258
+ const pattern = new RegExp(
1259
+ `\\b${neg}\\b(?:\\s+\\w+){0,1}\\s+\\b${escapedWord}\\b`,
1260
+ "i"
1261
+ );
1262
+ if (pattern.test(lower)) return true;
1263
+ }
1264
+ const notPattern = new RegExp(
1265
+ `\\b(?:do\\s+)?not\\b(?:\\s+\\w+){0,1}\\s+\\b${escapedWord}\\b`,
1266
+ "i"
1267
+ );
1268
+ if (notPattern.test(lower)) return true;
1269
+ }
1270
+ return false;
1271
+ }
1272
+ var POLARITY_STOP_WORDS = /* @__PURE__ */ new Set([
1273
+ // Polarity / negation markers (meta-level, not domain content)
1274
+ "never",
1275
+ "always",
1276
+ "avoid",
1277
+ "not",
1278
+ // Generic imperative verbs (describe HOW to comply, not WHAT topic)
1279
+ "use",
1280
+ "ensure",
1281
+ "require",
1282
+ "prefer",
1283
+ "follow",
1284
+ "keep",
1285
+ // Common modals and auxiliaries
1286
+ "must",
1287
+ "should",
1288
+ "can",
1289
+ "will",
1290
+ "may",
1291
+ // Generic quantifiers
1292
+ "all",
1293
+ "every",
1294
+ "each",
1295
+ "any"
1296
+ ]);
1297
+ function collectRuleLines3(instructions) {
1298
+ const sources = [
1299
+ instructions.rootFile,
1300
+ ...instructions.subFiles,
1301
+ ...instructions.rules
1302
+ ];
1303
+ const annotated = [];
1304
+ for (const file of sources) {
1305
+ for (const line of file.lines) {
1306
+ if (line.type !== "rule") continue;
1307
+ const words = removeStopWords(tokenizeWords(line.text)).filter(
1308
+ (w) => !POLARITY_STOP_WORDS.has(w)
1309
+ );
1310
+ if (words.length < 3) continue;
1311
+ annotated.push({ line, words, file: file.path });
1312
+ }
1313
+ }
1314
+ return annotated;
1315
+ }
1316
+ function detectContradictions(instructions) {
1317
+ const lines = collectRuleLines3(instructions);
1318
+ const findings = [];
1319
+ const reportedPairs = /* @__PURE__ */ new Set();
1320
+ for (let i = 0; i < lines.length; i++) {
1321
+ for (let j = i + 1; j < lines.length; j++) {
1322
+ const a = lines[i];
1323
+ const b = lines[j];
1324
+ const setA = new Set(a.words);
1325
+ const setB = new Set(b.words);
1326
+ const shared = [...setB].filter((w) => setA.has(w));
1327
+ if (shared.length < 3) continue;
1328
+ const hasContradiction = shared.some(
1329
+ (word) => isNegated(a.line.text, word) !== isNegated(b.line.text, word)
1330
+ );
1331
+ if (!hasContradiction) continue;
1332
+ const pairKey = `${a.file}:${a.line.lineNumber}|${b.file}:${b.line.lineNumber}`;
1333
+ if (reportedPairs.has(pairKey)) continue;
1334
+ reportedPairs.add(pairKey);
1335
+ const snippet = a.line.text.length > 60 ? `${a.line.text.slice(0, 60)}...` : a.line.text;
1336
+ findings.push({
1337
+ severity: "critical",
1338
+ category: "contradiction",
1339
+ file: b.file,
1340
+ line: b.line.lineNumber,
1341
+ messageKey: "structure.contradiction",
1342
+ messageParams: {
1343
+ snippet,
1344
+ lineA: String(a.line.lineNumber),
1345
+ lineB: String(b.line.lineNumber),
1346
+ fileA: a.file
1347
+ },
1348
+ suggestion: `Contradicting rules: "${snippet}" (${a.file} line ${a.line.lineNumber}) conflicts with line ${b.line.lineNumber}.`,
1349
+ autoFixable: false
1350
+ });
1351
+ }
1352
+ }
1353
+ return findings;
1354
+ }
1355
+
1356
+ // src/detectors/stale-refs.ts
1357
+ var import_fs7 = require("fs");
1358
+ var import_path6 = require("path");
1359
+ function collectLines(instructions) {
1360
+ const sources = [
1361
+ instructions.rootFile,
1362
+ ...instructions.subFiles,
1363
+ ...instructions.rules
1364
+ ];
1365
+ const result = [];
1366
+ for (const file of sources) {
1367
+ for (const line of file.lines) {
1368
+ if (line.type === "blank" || line.type === "code") continue;
1369
+ if (line.referencedPaths.length === 0) continue;
1370
+ result.push({ line, file: file.path });
1371
+ }
1372
+ }
1373
+ return result;
1374
+ }
1375
+ function detectStaleRefs(instructions, projectRoot) {
1376
+ const findings = [];
1377
+ for (const { line, file } of collectLines(instructions)) {
1378
+ for (const refPath of line.referencedPaths) {
1379
+ if (refPath.endsWith("/") || refPath.includes("*")) continue;
1380
+ const absolutePath = (0, import_path6.join)(projectRoot, refPath);
1381
+ if (!(0, import_fs7.existsSync)(absolutePath)) {
1382
+ findings.push({
1383
+ severity: "warning",
1384
+ category: "stale-ref",
1385
+ file,
1386
+ line: line.lineNumber,
1387
+ messageKey: "structure.staleRef",
1388
+ messageParams: { path: refPath },
1389
+ suggestion: `Stale reference: "${refPath}" does not exist.`,
1390
+ autoFixable: true
1391
+ });
1392
+ }
1393
+ }
1394
+ }
1395
+ return findings;
1396
+ }
1397
+
1398
+ // src/detectors/scope-classifier.ts
1399
+ var PATH_REF_PATTERN = /\b(?:src|tests?|lib|dist)\//i;
1400
+ var HOOK_PATTERN = /\b(?:never|don't|do\s+not|forbid)\b.*\b(?:commit|push|merge|build|run)\b/i;
1401
+ function classifyScope(instructions) {
1402
+ const findings = [];
1403
+ const rootFile = instructions.rootFile;
1404
+ for (const line of rootFile.lines) {
1405
+ if (line.type !== "rule") continue;
1406
+ if (HOOK_PATTERN.test(line.text)) {
1407
+ const snippet = line.text.length > 60 ? `${line.text.slice(0, 60)}...` : line.text;
1408
+ findings.push({
1409
+ severity: "info",
1410
+ category: "structure",
1411
+ file: rootFile.path,
1412
+ line: line.lineNumber,
1413
+ messageKey: "structure.scopeHook",
1414
+ messageParams: { line: String(line.lineNumber), snippet },
1415
+ suggestion: `Rule at line ${line.lineNumber} could be a git hook: "${snippet}"`,
1416
+ autoFixable: false
1417
+ });
1418
+ continue;
1419
+ }
1420
+ if (PATH_REF_PATTERN.test(line.text)) {
1421
+ const snippet = line.text.length > 60 ? `${line.text.slice(0, 60)}...` : line.text;
1422
+ findings.push({
1423
+ severity: "info",
1424
+ category: "structure",
1425
+ file: rootFile.path,
1426
+ line: line.lineNumber,
1427
+ messageKey: "structure.scopePathScoped",
1428
+ messageParams: { line: String(line.lineNumber), snippet },
1429
+ suggestion: `Rule at line ${line.lineNumber} references a specific path \u2014 consider a path-scoped rule file: "${snippet}"`,
1430
+ autoFixable: false
1431
+ });
1432
+ }
1433
+ }
1434
+ return findings;
1435
+ }
1436
+
1437
+ // src/analyzers/structure.ts
1438
+ function analyzeStructure(instructions, projectRoot) {
1439
+ return {
1440
+ findings: [
1441
+ ...detectContradictions(instructions),
1442
+ ...detectStaleRefs(instructions, projectRoot),
1443
+ ...classifyScope(instructions)
1444
+ ]
1445
+ };
1446
+ }
1447
+
1448
+ // src/core/scorer.ts
1449
+ var CONTEXT_WINDOW2 = 2e5;
1450
+ var CRITICAL_DEDUCTION = 10;
1451
+ var WARNING_DEDUCTION = 5;
1452
+ var INFO_DEDUCTION = 1;
1453
+ var MAX_CRITICAL_DEDUCTION = 40;
1454
+ var MAX_WARNING_DEDUCTION = 30;
1455
+ var MAX_INFO_DEDUCTION = 10;
1456
+ function gradeFromScore(score) {
1457
+ if (score >= 90) return "A";
1458
+ if (score >= 80) return "B";
1459
+ if (score >= 70) return "C";
1460
+ if (score >= 60) return "D";
1461
+ return "F";
1462
+ }
1463
+ function calculateScore(findings, budget) {
1464
+ const criticals = findings.filter((f) => f.severity === "critical").length;
1465
+ const warnings = findings.filter((f) => f.severity === "warning").length;
1466
+ const infos = findings.filter((f) => f.severity === "info").length;
1467
+ const criticalDeduction = Math.min(criticals * CRITICAL_DEDUCTION, MAX_CRITICAL_DEDUCTION);
1468
+ const warningDeduction = Math.min(warnings * WARNING_DEDUCTION, MAX_WARNING_DEDUCTION);
1469
+ const infoDeduction = Math.min(infos * INFO_DEDUCTION, MAX_INFO_DEDUCTION);
1470
+ const baselinePct = budget.totalBaseline / CONTEXT_WINDOW2;
1471
+ let budgetDeduction = 0;
1472
+ if (baselinePct > 0.5) budgetDeduction = 15;
1473
+ else if (baselinePct > 0.25) budgetDeduction = 5;
1474
+ const score = Math.max(
1475
+ 0,
1476
+ 100 - criticalDeduction - warningDeduction - infoDeduction - budgetDeduction
1477
+ );
1478
+ return { score, grade: gradeFromScore(score) };
1479
+ }
1480
+ function buildActionPlan(findings) {
1481
+ const priorityOf = (f) => {
1482
+ if (f.severity === "critical") return 1;
1483
+ if (f.severity === "warning") return 2;
1484
+ return 3;
1485
+ };
1486
+ const seen = /* @__PURE__ */ new Set();
1487
+ return findings.filter((f) => {
1488
+ if (seen.has(f.suggestion)) return false;
1489
+ seen.add(f.suggestion);
1490
+ return true;
1491
+ }).map((f) => ({
1492
+ priority: priorityOf(f),
1493
+ description: f.suggestion,
1494
+ category: f.category
1495
+ })).sort((a, b) => a.priority - b.priority);
1496
+ }
1497
+
1498
+ // src/core/reporter.ts
1499
+ var import_chalk2 = __toESM(require("chalk"), 1);
1500
+
1501
+ // src/commands/budget-command.ts
1502
+ var import_chalk = __toESM(require("chalk"), 1);
1503
+
1504
+ // src/i18n/en.json
1505
+ var en_default = {
1506
+ "label.tokenBudget": "TOKEN BUDGET",
1507
+ "label.deadRules": "DEAD RULES",
1508
+ "label.structure": "STRUCTURE",
1509
+ "label.actionPlan": "ACTION PLAN",
1510
+ "label.fixSummary": "FIX SUMMARY",
1511
+ "label.systemPrompt": "System prompt",
1512
+ "label.rootFile": "Root file",
1513
+ "label.ruleFiles": "Rule files",
1514
+ "label.skillFiles": "Skill files",
1515
+ "label.subDirFiles": "Sub-dir files",
1516
+ "label.mcpServers": "MCP servers",
1517
+ "label.baselineTotal": "Baseline total",
1518
+ "label.available": "Available",
1519
+ "label.redundantByConfig": "Redundant (enforced by config)",
1520
+ "label.duplicates": "Duplicates",
1521
+ "label.contradictions": "Contradictions",
1522
+ "label.staleReferences": "Stale References",
1523
+ "label.refactoringOpportunities": "Refactoring Opportunities",
1524
+ "label.tool": "Tool:",
1525
+ "label.score": "Score:",
1526
+ "status.noBudgetIssues": "\u2713 No budget issues found",
1527
+ "status.noDeadRules": "\u2713 No dead rules found",
1528
+ "status.noStructuralIssues": "\u2713 No structural issues found",
1529
+ "status.noAutoFixable": "\u2713 No auto-fixable issues found",
1530
+ "status.perfectScore": "\u2713 Perfect score \u2014 no issues found",
1531
+ "status.fixedIssues": "\u2713 Fixed {{count}} issue{{s}} \u2014 run `git diff` to review changes",
1532
+ "error.unknownTool": "No agent instruction files found. Run this command in a project that uses Claude Code, Codex, or Cursor.",
1533
+ "error.missingRootFile": "Found {{tool}} configuration but no root instruction file.",
1534
+ "error.dirtyWorkingTree": "Working tree is dirty. Commit or stash your changes before running --fix, or use --force to skip this check.",
1535
+ "fix.removedDeadRules": "Removed {{count}} redundant rule{{s}}",
1536
+ "fix.removedStaleRefs": "Removed {{count}} stale reference{{s}}",
1537
+ "fix.removedDuplicates": "Removed {{count}} exact duplicate{{s}}",
1538
+ "summary.redundantRules": "{{count}} redundant rule{{s}}",
1539
+ "summary.duplicates": "{{count}} duplicate{{s}}",
1540
+ "summary.contradictions": "{{count}} contradiction{{s}}",
1541
+ "summary.staleRefs": "{{count}} stale ref{{s}}",
1542
+ "summary.refactoringSuggestions": "{{count}} refactoring suggestion{{s}}",
1543
+ "summary.found": "{{parts}} found",
1544
+ "tokens.measured": "{{count}} tokens",
1545
+ "tokens.estimated": "~{{count}} tokens (estimated)",
1546
+ "actionPlan.andMore": "\u2026 and {{count}} more",
1547
+ "severity.critical": "{{count}} critical",
1548
+ "severity.warnings": "{{count}} warning{{s}}",
1549
+ "severity.suggestions": "{{count}} suggestion{{s}}",
1550
+ "budget.rootFileCritical": "Root instruction file is {{lines}} lines \u2014 agent compliance drops significantly above 200 lines",
1551
+ "budget.rootFileWarning": "Root instruction file is {{lines}} lines (recommended: < 200)",
1552
+ "budget.mcpLargeServer": "MCP server '{{name}}' consumes ~{{tokens}} tokens",
1553
+ "budget.baselineHigh": "Baseline context consumption is {{pct}}% of window",
1554
+ "deadRule.configOverlap": 'Rule "{{rule}}" is already enforced by {{config}}',
1555
+ "deadRule.exactDuplicate": "Exact duplicate of line {{otherLine}} in {{otherFile}}",
1556
+ "deadRule.nearDuplicate": "Very similar to line {{otherLine}} in {{otherFile}} ({{similarity}}% similar)",
1557
+ "structure.contradiction": 'Contradicting rules: "{{snippet}}" ({{fileA}} line {{lineA}}) conflicts with line {{lineB}}.',
1558
+ "structure.staleRef": 'Stale reference: "{{path}}" does not exist.',
1559
+ "structure.scopeHook": 'Rule at line {{line}} could be a git hook: "{{snippet}}"',
1560
+ "structure.scopePathScoped": 'Rule at line {{line}} references a specific path \u2014 consider a path-scoped rule file: "{{snippet}}"',
1561
+ "label.budget": "BUDGET",
1562
+ "label.findings": "FINDINGS",
1563
+ "label.topIssues": "TOP ISSUES",
1564
+ "label.category": "Category",
1565
+ "compact.budgetLine": "{{used}} / {{window}} tokens ({{pct}}%)",
1566
+ "compact.andMore": "\u2026 and {{count}} more (run instrlint <command> for details)",
1567
+ "compact.budget": "Budget",
1568
+ "compact.deadRules": "Dead rules",
1569
+ "compact.duplicates": "Duplicates",
1570
+ "compact.contradictions": "Contradictions",
1571
+ "compact.staleRefs": "Stale refs",
1572
+ "compact.structure": "Structure",
1573
+ "markdown.title": "instrlint Health Report \u2014 {{project}}",
1574
+ "markdown.scoreLine": "**Score: {{score}}/100 ({{grade}})** \xB7 Tool: `{{tool}}`",
1575
+ "markdown.summary": "## Summary",
1576
+ "markdown.severity": "Severity",
1577
+ "markdown.count": "Count",
1578
+ "markdown.critical": "\u{1F534} Critical",
1579
+ "markdown.warning": "\u{1F7E1} Warning",
1580
+ "markdown.info": "\u2139\uFE0F Info",
1581
+ "markdown.contradictions": "Contradictions",
1582
+ "markdown.staleReferences": "Stale References",
1583
+ "markdown.deadRules": "Dead Rules",
1584
+ "markdown.duplicateRules": "Duplicates",
1585
+ "markdown.budgetIssues": "Budget Issues",
1586
+ "markdown.refactoringOpportunities": "Refactoring Opportunities",
1587
+ "markdown.lineRef": "(line {{line}})",
1588
+ "markdown.actionPlan": "## Action Plan",
1589
+ "markdown.attribution": "*Generated by [instrlint](https://github.com/jed1978/instrlint)*",
1590
+ "ci.passed": "\u2713 instrlint passed score={{score}} grade={{grade}}",
1591
+ "ci.failed": "\u2716 instrlint failed score={{score}} grade={{grade}}",
1592
+ "ci.writtenTo": "\u2192 written to {{file}}",
1593
+ "initCi.created": "\u2713 Created {{path}}",
1594
+ "initCi.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1595
+ "install.installed": "\u2713 Installed to {{path}}",
1596
+ "install.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
1597
+ "install.unknownTarget": "Specify --claude-code or --codex",
1598
+ "fix.manualActions": "MANUAL ACTIONS NEEDED",
1599
+ "fix.hookCreate": "Add to .claude/settings.json:",
1600
+ "fix.hookWarning": "\u26A0 Hook executes shell commands \u2014 review carefully before adding",
1601
+ "fix.pathScopedCreate": "Create {{path}}:",
1602
+ "fix.thenRemoveLine": "Then remove line {{line}} from {{file}}"
1603
+ };
1604
+
1605
+ // src/i18n/zh-TW.json
1606
+ var zh_TW_default = {
1607
+ "label.tokenBudget": "TOKEN \u9810\u7B97",
1608
+ "label.deadRules": "\u5197\u9918\u898F\u5247",
1609
+ "label.structure": "\u7D50\u69CB\u5206\u6790",
1610
+ "label.actionPlan": "\u884C\u52D5\u8A08\u756B",
1611
+ "label.fixSummary": "\u4FEE\u5FA9\u6458\u8981",
1612
+ "label.systemPrompt": "\u7CFB\u7D71\u63D0\u793A",
1613
+ "label.rootFile": "\u6839\u6307\u4EE4\u6A94",
1614
+ "label.ruleFiles": "\u898F\u5247\u6A94",
1615
+ "label.skillFiles": "\u6280\u80FD\u6A94",
1616
+ "label.subDirFiles": "\u5B50\u76EE\u9304\u6A94",
1617
+ "label.mcpServers": "MCP \u4F3A\u670D\u5668",
1618
+ "label.baselineTotal": "\u521D\u59CB\u7E3D\u91CF",
1619
+ "label.available": "\u53EF\u7528",
1620
+ "label.redundantByConfig": "\u5197\u9918\uFF08\u5DF2\u7531\u914D\u7F6E\u5F37\u5236\u57F7\u884C\uFF09",
1621
+ "label.duplicates": "\u91CD\u8907\u9805\u76EE",
1622
+ "label.contradictions": "\u898F\u5247\u77DB\u76FE",
1623
+ "label.staleReferences": "\u904E\u6642\u53C3\u7167",
1624
+ "label.refactoringOpportunities": "\u91CD\u69CB\u5EFA\u8B70",
1625
+ "label.tool": "\u5DE5\u5177\uFF1A",
1626
+ "label.score": "\u5206\u6578\uFF1A",
1627
+ "status.noBudgetIssues": "\u2713 \u7121 Token \u9810\u7B97\u554F\u984C",
1628
+ "status.noDeadRules": "\u2713 \u7121\u5197\u9918\u898F\u5247",
1629
+ "status.noStructuralIssues": "\u2713 \u7121\u7D50\u69CB\u6027\u554F\u984C",
1630
+ "status.noAutoFixable": "\u2713 \u7121\u53EF\u81EA\u52D5\u4FEE\u5FA9\u7684\u554F\u984C",
1631
+ "status.perfectScore": "\u2713 \u6EFF\u5206 \u2014 \u7121\u4EFB\u4F55\u554F\u984C",
1632
+ "status.fixedIssues": "\u2713 \u5DF2\u4FEE\u5FA9 {{count}} \u500B\u554F\u984C \u2014 \u57F7\u884C `git diff` \u67E5\u770B\u8B8A\u66F4",
1633
+ "error.unknownTool": "\u627E\u4E0D\u5230\u4EE3\u7406\u6307\u4EE4\u6A94\u3002\u8ACB\u5728\u4F7F\u7528 Claude Code\u3001Codex \u6216 Cursor \u7684\u5C08\u6848\u4E2D\u57F7\u884C\u6B64\u6307\u4EE4\u3002",
1634
+ "error.missingRootFile": "\u627E\u5230 {{tool}} \u914D\u7F6E\uFF0C\u4F46\u627E\u4E0D\u5230\u6839\u6307\u4EE4\u6A94\u3002",
1635
+ "error.dirtyWorkingTree": "\u5DE5\u4F5C\u76EE\u9304\u6709\u672A\u63D0\u4EA4\u7684\u8B8A\u66F4\u3002\u8ACB\u5148\u63D0\u4EA4\u6216\u5132\u85CF\u8B8A\u66F4\u518D\u57F7\u884C --fix\uFF0C\u6216\u4F7F\u7528 --force \u8DF3\u904E\u6B64\u6AA2\u67E5\u3002",
1636
+ "fix.removedDeadRules": "\u5DF2\u79FB\u9664 {{count}} \u500B\u5197\u9918\u898F\u5247",
1637
+ "fix.removedStaleRefs": "\u5DF2\u79FB\u9664 {{count}} \u500B\u904E\u6642\u53C3\u7167",
1638
+ "fix.removedDuplicates": "\u5DF2\u79FB\u9664 {{count}} \u500B\u91CD\u8907\u9805\u76EE",
1639
+ "summary.redundantRules": "{{count}} \u500B\u5197\u9918\u898F\u5247",
1640
+ "summary.duplicates": "{{count}} \u500B\u91CD\u8907\u9805\u76EE",
1641
+ "summary.contradictions": "{{count}} \u500B\u898F\u5247\u77DB\u76FE",
1642
+ "summary.staleRefs": "{{count}} \u500B\u904E\u6642\u53C3\u7167",
1643
+ "summary.refactoringSuggestions": "{{count}} \u500B\u91CD\u69CB\u5EFA\u8B70",
1644
+ "summary.found": "\u767C\u73FE {{parts}}",
1645
+ "tokens.measured": "{{count}} tokens",
1646
+ "tokens.estimated": "~{{count}} tokens\uFF08\u4F30\u8A08\u503C\uFF09",
1647
+ "actionPlan.andMore": "\u2026 \u9084\u6709 {{count}} \u500B",
1648
+ "severity.critical": "{{count}} \u500B\u56B4\u91CD\u554F\u984C",
1649
+ "severity.warnings": "{{count}} \u500B\u8B66\u544A",
1650
+ "severity.suggestions": "{{count}} \u500B\u5EFA\u8B70",
1651
+ "budget.rootFileCritical": "\u6839\u6307\u4EE4\u6A94\u6709 {{lines}} \u884C \u2014 \u8D85\u904E 200 \u884C\u5F8C\u4EE3\u7406\u9075\u5FAA\u7387\u986F\u8457\u4E0B\u964D",
1652
+ "budget.rootFileWarning": "\u6839\u6307\u4EE4\u6A94\u6709 {{lines}} \u884C\uFF08\u5EFA\u8B70\uFF1A< 200 \u884C\uFF09",
1653
+ "budget.mcpLargeServer": "MCP \u4F3A\u670D\u5668\u300C{{name}}\u300D\u6D88\u8017\u7D04 {{tokens}} tokens",
1654
+ "budget.baselineHigh": "\u57FA\u7DDA\u60C5\u5883\u6D88\u8017\u4F54\u8996\u7A97 {{pct}}%",
1655
+ "deadRule.configOverlap": "\u898F\u5247\u300C{{rule}}\u300D\u5DF2\u7531 {{config}} \u5F37\u5236\u57F7\u884C",
1656
+ "deadRule.exactDuplicate": "\u8207 {{otherFile}} \u7B2C {{otherLine}} \u884C\u5B8C\u5168\u91CD\u8907",
1657
+ "deadRule.nearDuplicate": "\u8207 {{otherFile}} \u7B2C {{otherLine}} \u884C\u9AD8\u5EA6\u76F8\u4F3C\uFF08{{similarity}}% \u76F8\u4F3C\u5EA6\uFF09",
1658
+ "structure.contradiction": "\u898F\u5247\u77DB\u76FE\uFF1A\u300C{{snippet}}\u300D\uFF08{{fileA}} \u7B2C {{lineA}} \u884C\uFF09\u8207\u7B2C {{lineB}} \u884C\u885D\u7A81\u3002",
1659
+ "structure.staleRef": "\u904E\u6642\u53C3\u7167\uFF1A\u300C{{path}}\u300D\u4E0D\u5B58\u5728\u3002",
1660
+ "structure.scopeHook": "\u7B2C {{line}} \u884C\u7684\u898F\u5247\u9069\u5408\u505A\u6210 git hook\uFF1A\u300C{{snippet}}\u300D",
1661
+ "structure.scopePathScoped": "\u7B2C {{line}} \u884C\u7684\u898F\u5247\u53C3\u7167\u4E86\u7279\u5B9A\u8DEF\u5F91 \u2014 \u5EFA\u8B70\u5EFA\u7ACB\u8DEF\u5F91\u7BC4\u570D\u7684\u898F\u5247\u6A94\uFF1A\u300C{{snippet}}\u300D",
1662
+ "label.budget": "\u9810\u7B97",
1663
+ "label.findings": "\u554F\u984C\u7E3D\u89BD",
1664
+ "label.topIssues": "\u9996\u8981\u554F\u984C",
1665
+ "label.category": "Category",
1666
+ "compact.budgetLine": "{{used}} / {{window}} tokens ({{pct}}%)",
1667
+ "compact.andMore": "\u2026 \u9084\u6709 {{count}} \u500B\uFF08\u57F7\u884C instrlint <command> \u67E5\u770B\u8A73\u60C5\uFF09",
1668
+ "compact.budget": "Budget",
1669
+ "compact.deadRules": "Dead rules",
1670
+ "compact.duplicates": "Duplicates",
1671
+ "compact.contradictions": "Contradictions",
1672
+ "compact.staleRefs": "Stale refs",
1673
+ "compact.structure": "Structure",
1674
+ "markdown.title": "instrlint \u5065\u5EB7\u5831\u544A \u2014 {{project}}",
1675
+ "markdown.scoreLine": "**\u5206\u6578\uFF1A{{score}}/100 ({{grade}})** \xB7 \u5DE5\u5177\uFF1A`{{tool}}`",
1676
+ "markdown.summary": "## \u6458\u8981",
1677
+ "markdown.severity": "\u56B4\u91CD\u7A0B\u5EA6",
1678
+ "markdown.count": "\u6578\u91CF",
1679
+ "markdown.critical": "\u{1F534} \u56B4\u91CD",
1680
+ "markdown.warning": "\u{1F7E1} \u8B66\u544A",
1681
+ "markdown.info": "\u2139\uFE0F \u8CC7\u8A0A",
1682
+ "markdown.contradictions": "\u898F\u5247\u77DB\u76FE",
1683
+ "markdown.staleReferences": "\u904E\u6642\u53C3\u7167",
1684
+ "markdown.deadRules": "\u5197\u9918\u898F\u5247",
1685
+ "markdown.duplicateRules": "\u91CD\u8907\u9805\u76EE",
1686
+ "markdown.budgetIssues": "Token \u9810\u7B97\u554F\u984C",
1687
+ "markdown.refactoringOpportunities": "\u91CD\u69CB\u5EFA\u8B70",
1688
+ "markdown.lineRef": "\uFF08\u7B2C {{line}} \u884C\uFF09",
1689
+ "markdown.actionPlan": "## \u884C\u52D5\u8A08\u756B",
1690
+ "markdown.attribution": "*\u7531 [instrlint](https://github.com/jed1978/instrlint) \u751F\u6210*",
1691
+ "ci.passed": "\u2713 instrlint \u901A\u904E \u5206\u6578={{score}} \u7B49\u7D1A={{grade}}",
1692
+ "ci.failed": "\u2716 instrlint \u672A\u901A\u904E \u5206\u6578={{score}} \u7B49\u7D1A={{grade}}",
1693
+ "ci.writtenTo": "\u2192 \u5DF2\u5BEB\u5165 {{file}}",
1694
+ "initCi.created": "\u2713 \u5DF2\u5EFA\u7ACB {{path}}",
1695
+ "initCi.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1696
+ "install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}",
1697
+ "install.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
1698
+ "install.unknownTarget": "\u8ACB\u6307\u5B9A --claude-code \u6216 --codex",
1699
+ "fix.manualActions": "\u9700\u8981\u624B\u52D5\u64CD\u4F5C",
1700
+ "fix.hookCreate": "\u52A0\u5165 .claude/settings.json\uFF1A",
1701
+ "fix.hookWarning": "\u26A0 Hook \u6703\u57F7\u884C shell command\uFF0C\u8ACB\u4ED4\u7D30\u78BA\u8A8D\u5F8C\u518D\u52A0\u5165",
1702
+ "fix.pathScopedCreate": "\u5EFA\u7ACB {{path}}\uFF1A",
1703
+ "fix.thenRemoveLine": "\u642C\u79FB\u5F8C\u8ACB\u5F9E {{file}} \u7B2C {{line}} \u884C\u522A\u9664\u539F\u898F\u5247"
1704
+ };
1705
+
1706
+ // src/i18n/index.ts
1707
+ var LOCALE_MAP = {
1708
+ en: en_default,
1709
+ "zh-TW": zh_TW_default
1710
+ };
1711
+ var _locale = "en";
1712
+ var _messages = LOCALE_MAP["en"];
1713
+ function detectLocale() {
1714
+ const env = process.env["INSTRLINT_LANG"];
1715
+ if (env === "zh-TW" || env === "en") return env;
1716
+ try {
1717
+ const sys = Intl.DateTimeFormat().resolvedOptions().locale;
1718
+ if (sys.startsWith("zh")) return "zh-TW";
1719
+ } catch {
1720
+ }
1721
+ return "en";
1722
+ }
1723
+ function initLocale(lang) {
1724
+ const valid = ["en", "zh-TW"];
1725
+ const resolved = valid.includes(lang) ? lang : detectLocale();
1726
+ _locale = resolved;
1727
+ _messages = LOCALE_MAP[resolved];
1728
+ }
1729
+ function getLocale() {
1730
+ return _locale;
1731
+ }
1732
+ function t(key, params) {
1733
+ const template = _messages[key] ?? key;
1734
+ if (!params) return template;
1735
+ return template.replace(
1736
+ /\{\{(\w+)\}\}/g,
1737
+ (_, k) => params[k] ?? `{{${k}}}`
1738
+ );
1739
+ }
1740
+ function plural(count) {
1741
+ return count === 1 ? "" : "s";
1742
+ }
1743
+
1744
+ // src/commands/budget-command.ts
1745
+ function getFmt() {
1746
+ return new Intl.NumberFormat(getLocale());
1747
+ }
1748
+ function formatTokens(count, method) {
1749
+ const formatted = getFmt().format(count);
1750
+ if (method === "measured") return t("tokens.measured", { count: formatted });
1751
+ return t("tokens.estimated", { count: formatted });
1752
+ }
1753
+ function bar(fraction, width = 24) {
1754
+ const filled = Math.round(Math.min(1, Math.max(0, fraction)) * width);
1755
+ const empty = width - filled;
1756
+ return import_chalk.default.cyan("\u2588".repeat(filled)) + import_chalk.default.gray("\u2591".repeat(empty));
1757
+ }
1758
+ function pct(fraction) {
1759
+ return `${Math.round(fraction * 100)}%`;
1760
+ }
1761
+ function printBudgetTerminal(summary, findings, output = console) {
1762
+ const total = summary.totalBaseline;
1763
+ const window = total + summary.availableTokens;
1764
+ output.log("");
1765
+ output.log(import_chalk.default.bold.white(` ${t("label.tokenBudget")}`));
1766
+ output.log(import_chalk.default.gray(" \u2500".repeat(30)));
1767
+ const rows = [
1768
+ {
1769
+ labelKey: "label.systemPrompt",
1770
+ tokens: summary.systemPromptTokens,
1771
+ method: "estimated"
1772
+ },
1773
+ {
1774
+ labelKey: "label.rootFile",
1775
+ tokens: summary.rootFileTokens,
1776
+ method: summary.rootFileMethod
1777
+ },
1778
+ {
1779
+ labelKey: "label.ruleFiles",
1780
+ tokens: summary.rulesTokens,
1781
+ method: summary.rulesMethod
1782
+ },
1783
+ {
1784
+ labelKey: "label.skillFiles",
1785
+ tokens: summary.skillsTokens,
1786
+ method: summary.skillsMethod
1787
+ },
1788
+ {
1789
+ labelKey: "label.subDirFiles",
1790
+ tokens: summary.subFilesTokens,
1791
+ method: summary.subFilesMethod
1792
+ },
1793
+ {
1794
+ labelKey: "label.mcpServers",
1795
+ tokens: summary.mcpTokens,
1796
+ method: "estimated"
1797
+ }
1798
+ ];
1799
+ for (const row of rows) {
1800
+ if (row.tokens === 0) continue;
1801
+ const fraction = row.tokens / window;
1802
+ const label = t(row.labelKey).padEnd(14);
1803
+ const tokenStr = formatTokens(row.tokens, row.method).padStart(28);
1804
+ output.log(
1805
+ ` ${import_chalk.default.white(label)} ${bar(fraction)} ${import_chalk.default.yellow(tokenStr)}`
1806
+ );
1807
+ }
1808
+ output.log(import_chalk.default.gray(" \u2500".repeat(30)));
1809
+ const baselineFraction = total / window;
1810
+ const baselineStr = formatTokens(total, summary.tokenMethod).padStart(28);
1811
+ output.log(
1812
+ ` ${t("label.baselineTotal").padEnd(14)} ${bar(baselineFraction)} ${import_chalk.default.bold.yellow(baselineStr)} ${import_chalk.default.gray(pct(baselineFraction))}`
1813
+ );
1814
+ const availStr = formatTokens(summary.availableTokens, "estimated").padStart(
1815
+ 28
1816
+ );
1817
+ output.log(
1818
+ ` ${t("label.available").padEnd(14)} ${"".padEnd(26)} ${import_chalk.default.green(availStr)}`
1819
+ );
1820
+ output.log("");
1821
+ if (findings.length === 0) {
1822
+ output.log(import_chalk.default.green(` ${t("status.noBudgetIssues")}`));
1823
+ } else {
1824
+ for (const f of findings) {
1825
+ const icon = f.severity === "critical" ? import_chalk.default.red(" \u2716") : f.severity === "warning" ? import_chalk.default.yellow(" \u26A0") : import_chalk.default.blue(" \u2139");
1826
+ output.log(`${icon} ${t(f.messageKey, f.messageParams)}`);
1827
+ }
1828
+ }
1829
+ output.log("");
1830
+ }
1831
+ async function runBudget(opts, output = console) {
1832
+ initLocale(opts.lang);
1833
+ await ensureInitialized();
1834
+ const projectRoot = opts.projectRoot ?? process.cwd();
1835
+ const scan = scanProject(projectRoot, opts.tool);
1836
+ if (scan.tool === "unknown") {
1837
+ output.error(t("error.unknownTool"));
1838
+ return { exitCode: 1, errorMessage: "unknown tool" };
1839
+ }
1840
+ if (scan.rootFilePath === null) {
1841
+ output.error(t("error.missingRootFile", { tool: scan.tool }));
1842
+ return { exitCode: 1, errorMessage: "missing root file" };
1843
+ }
1844
+ const instructions = loadProject(projectRoot, scan.tool);
1845
+ const { findings, summary } = analyzeBudget(instructions);
1846
+ if (opts.format === "json") {
1847
+ output.log(JSON.stringify({ findings, summary }, null, 2));
1848
+ return { exitCode: 0 };
1849
+ }
1850
+ printBudgetTerminal(summary, findings, output);
1851
+ return { exitCode: 0 };
1852
+ }
1853
+
1854
+ // src/core/reporter.ts
1855
+ var BOX_W = 50;
1856
+ var ANSI_RE = /\x1b\[[0-9;]*m/g;
1857
+ function visLen(s) {
1858
+ return s.replace(ANSI_RE, "").length;
1859
+ }
1860
+ function padR(s, w) {
1861
+ return s + " ".repeat(Math.max(0, w - visLen(s)));
1862
+ }
1863
+ function gradeColor(grade) {
1864
+ if (grade === "A") return import_chalk2.default.green;
1865
+ if (grade === "B") return import_chalk2.default.cyan;
1866
+ if (grade === "C") return import_chalk2.default.yellow;
1867
+ if (grade === "D") return import_chalk2.default.magenta;
1868
+ return import_chalk2.default.red;
1869
+ }
1870
+ function gradeBadge(grade) {
1871
+ if (grade === "A") return import_chalk2.default.bgGreen(import_chalk2.default.bold.black(` ${grade} `));
1872
+ if (grade === "B") return import_chalk2.default.bgCyan(import_chalk2.default.bold.black(` ${grade} `));
1873
+ if (grade === "C") return import_chalk2.default.bgYellow(import_chalk2.default.bold.black(` ${grade} `));
1874
+ if (grade === "D") return import_chalk2.default.bgMagenta(import_chalk2.default.bold.white(` ${grade} `));
1875
+ return import_chalk2.default.bgRed(import_chalk2.default.bold.white(` ${grade} `));
1876
+ }
1877
+ function scoreBar(score, grade, width = 30) {
1878
+ const filled = Math.round(score / 100 * width);
1879
+ const empty = width - filled;
1880
+ return gradeColor(grade)("\u2588".repeat(filled)) + import_chalk2.default.gray("\u2591".repeat(empty));
1881
+ }
1882
+ function sectionHeader(title, width = BOX_W) {
1883
+ const inner = ` ${title} `;
1884
+ const remaining = Math.max(0, width - 2 - inner.length);
1885
+ return import_chalk2.default.gray(` \u2500\u2500${import_chalk2.default.bold.white(inner)}${"\u2500".repeat(remaining)}\u2500\u2500`);
1886
+ }
1887
+ function printCompactBudget(summary, output) {
1888
+ const total = summary.totalBaseline;
1889
+ const window = total + summary.availableTokens;
1890
+ const fraction = total / window;
1891
+ const fmt = new Intl.NumberFormat(getLocale());
1892
+ const usedPrefix = summary.tokenMethod === "estimated" ? "~" : "";
1893
+ const budgetLine = t("compact.budgetLine", {
1894
+ used: `${usedPrefix}${fmt.format(total)}`,
1895
+ window: fmt.format(window),
1896
+ pct: String(Math.round(fraction * 100))
1897
+ });
1898
+ output.log(` ${import_chalk2.default.yellow(budgetLine)} ${bar(fraction, 14)}`);
1899
+ }
1900
+ var SEVERITY_ORDER = {
1901
+ critical: 0,
1902
+ warning: 1,
1903
+ info: 2
1904
+ };
1905
+ function printFindingsTable(findings, output) {
1906
+ const categories = [
1907
+ { key: "contradiction", label: t("compact.contradictions") },
1908
+ { key: "budget", label: t("compact.budget") },
1909
+ { key: "dead-rule", label: t("compact.deadRules") },
1910
+ { key: "duplicate", label: t("compact.duplicates") },
1911
+ { key: "stale-ref", label: t("compact.staleRefs") },
1912
+ { key: "structure", label: t("compact.structure") }
1913
+ ];
1914
+ const rows = categories.map(({ key, label }) => {
1915
+ const group = findings.filter((f) => f.category === key);
1916
+ return {
1917
+ label,
1918
+ critical: group.filter((f) => f.severity === "critical").length,
1919
+ warning: group.filter((f) => f.severity === "warning").length,
1920
+ info: group.filter((f) => f.severity === "info").length
1921
+ };
1922
+ }).filter((r) => r.critical + r.warning + r.info > 0);
1923
+ if (rows.length === 0) return;
1924
+ output.log(sectionHeader(t("label.findings")));
1925
+ for (const row of rows) {
1926
+ const parts = [];
1927
+ if (row.critical > 0) parts.push(import_chalk2.default.red(`\u2716 ${row.critical}`));
1928
+ if (row.warning > 0) parts.push(import_chalk2.default.yellow(`\u26A0 ${row.warning}`));
1929
+ if (row.info > 0) parts.push(import_chalk2.default.blue(`\u2139 ${row.info}`));
1930
+ output.log(
1931
+ ` ${import_chalk2.default.whiteBright(row.label.padEnd(18))}${parts.join(" ")}`
1932
+ );
1933
+ }
1934
+ }
1935
+ function printTopIssues(findings, output) {
1936
+ if (findings.length === 0) return;
1937
+ const sorted = [...findings].sort(
1938
+ (a, b) => (SEVERITY_ORDER[a.severity] ?? 3) - (SEVERITY_ORDER[b.severity] ?? 3)
1939
+ );
1940
+ const top = sorted.slice(0, 5);
1941
+ output.log("");
1942
+ output.log(sectionHeader(t("label.topIssues")));
1943
+ for (let i = 0; i < top.length; i++) {
1944
+ const f = top[i];
1945
+ const icon = f.severity === "critical" ? import_chalk2.default.red("\u2716") : f.severity === "warning" ? import_chalk2.default.yellow("\u26A0") : import_chalk2.default.blue("\u2139");
1946
+ const msg = t(f.messageKey, f.messageParams);
1947
+ const truncated = msg.length > 68 ? `${msg.slice(0, 68)}\u2026` : msg;
1948
+ output.log(` ${import_chalk2.default.white(`${i + 1}.`)} ${icon} ${truncated}`);
1949
+ }
1950
+ if (sorted.length > 5) {
1951
+ output.log(
1952
+ import_chalk2.default.white(
1953
+ ` ${t("compact.andMore", { count: String(sorted.length - 5) })}`
1954
+ )
1955
+ );
1956
+ }
1957
+ }
1958
+ function printCombinedTerminal(report, output = console) {
1959
+ const { project, tool, score, grade, tokenMethod } = report;
1960
+ const border = "\u2500".repeat(BOX_W);
1961
+ output.log("");
1962
+ const B = import_chalk2.default.green;
1963
+ output.log(B(` \u256D${border}\u256E`));
1964
+ const line1 = ` ${import_chalk2.default.bold.white("instrlint")} ${B("\u2500")} ${import_chalk2.default.cyan(project)}`;
1965
+ output.log(` ${B("\u2502")}${padR(line1, BOX_W)}${B("\u2502")}`);
1966
+ const line2 = ` ${import_chalk2.default.white(tool)} ${B("\xB7")} ${import_chalk2.default.white(tokenMethod)}`;
1967
+ output.log(` ${B("\u2502")}${padR(line2, BOX_W)}${B("\u2502")}`);
1968
+ output.log(B(` \u251C${border}\u2524`));
1969
+ const scoreLine = ` ${scoreBar(score, grade)} ${import_chalk2.default.bold.white(String(score))}/100 ${gradeBadge(grade)}`;
1970
+ output.log(` ${B("\u2502")}${padR(scoreLine, BOX_W)}${B("\u2502")}`);
1971
+ output.log(B(` \u2570${border}\u256F`));
1972
+ output.log("");
1973
+ output.log(sectionHeader(t("label.budget")));
1974
+ printCompactBudget(report.budget, output);
1975
+ if (report.findings.length > 0) {
1976
+ output.log("");
1977
+ printFindingsTable(report.findings, output);
1978
+ printTopIssues(report.findings, output);
1979
+ }
1980
+ output.log("");
1981
+ if (report.findings.length === 0) {
1982
+ output.log(import_chalk2.default.green(` ${t("status.perfectScore")}`));
1983
+ } else {
1984
+ const criticals = report.findings.filter(
1985
+ (f) => f.severity === "critical"
1986
+ ).length;
1987
+ const warnings = report.findings.filter(
1988
+ (f) => f.severity === "warning"
1989
+ ).length;
1990
+ const infos = report.findings.filter((f) => f.severity === "info").length;
1991
+ const parts = [];
1992
+ if (criticals > 0)
1993
+ parts.push(
1994
+ import_chalk2.default.red(t("severity.critical", { count: String(criticals) }))
1995
+ );
1996
+ if (warnings > 0)
1997
+ parts.push(
1998
+ import_chalk2.default.yellow(
1999
+ t("severity.warnings", {
2000
+ count: String(warnings),
2001
+ s: plural(warnings)
2002
+ })
2003
+ )
2004
+ );
2005
+ if (infos > 0)
2006
+ parts.push(
2007
+ import_chalk2.default.blue(
2008
+ t("severity.suggestions", { count: String(infos), s: plural(infos) })
2009
+ )
2010
+ );
2011
+ const summary = parts.join(import_chalk2.default.gray(" \xB7 "));
2012
+ const summaryVisible = summary.replace(ANSI_RE, "");
2013
+ const pad = Math.max(0, BOX_W - 2 - summaryVisible.length);
2014
+ output.log(
2015
+ import_chalk2.default.gray(` \u2500\u2500`) + ` ${summary} ` + import_chalk2.default.gray("\u2500".repeat(pad))
2016
+ );
2017
+ }
2018
+ output.log("");
2019
+ }
2020
+ function reportJson(report) {
2021
+ return JSON.stringify(report, null, 2);
2022
+ }
2023
+ function mdSeverityIcon(f) {
2024
+ if (f.severity === "critical") return "\u{1F534}";
2025
+ if (f.severity === "warning") return "\u{1F7E1}";
2026
+ return "\u2139\uFE0F";
2027
+ }
2028
+ function reportMarkdown(report, extraSections = []) {
2029
+ const { project, tool, score, grade, findings } = report;
2030
+ const criticals = findings.filter((f) => f.severity === "critical").length;
2031
+ const warnings = findings.filter((f) => f.severity === "warning").length;
2032
+ const infos = findings.filter((f) => f.severity === "info").length;
2033
+ const lines = [
2034
+ `# ${t("markdown.title", { project })}`,
2035
+ "",
2036
+ t("markdown.scoreLine", { score: String(score), grade, tool }),
2037
+ "",
2038
+ t("markdown.summary"),
2039
+ "",
2040
+ `| ${t("markdown.severity")} | ${t("markdown.count")} |`,
2041
+ "|----------|-------|",
2042
+ `| ${t("markdown.critical")} | ${criticals} |`,
2043
+ `| ${t("markdown.warning")} | ${warnings} |`,
2044
+ `| ${t("markdown.info")} | ${infos} |`,
2045
+ ""
2046
+ ];
2047
+ const categories = [
2048
+ {
2049
+ labelKey: "markdown.contradictions",
2050
+ filter: (f) => f.category === "contradiction"
2051
+ },
2052
+ {
2053
+ labelKey: "markdown.staleReferences",
2054
+ filter: (f) => f.category === "stale-ref"
2055
+ },
2056
+ {
2057
+ labelKey: "markdown.deadRules",
2058
+ filter: (f) => f.category === "dead-rule"
2059
+ },
2060
+ {
2061
+ labelKey: "markdown.duplicateRules",
2062
+ filter: (f) => f.category === "duplicate"
2063
+ },
2064
+ {
2065
+ labelKey: "markdown.budgetIssues",
2066
+ filter: (f) => f.category === "budget"
2067
+ },
2068
+ {
2069
+ labelKey: "markdown.refactoringOpportunities",
2070
+ filter: (f) => f.category === "structure"
2071
+ }
2072
+ ];
2073
+ for (const { labelKey, filter } of categories) {
2074
+ const group = findings.filter(filter);
2075
+ if (group.length === 0) continue;
2076
+ lines.push(`## ${t(labelKey)}`, "");
2077
+ for (const f of group) {
2078
+ const loc = f.line != null ? ` ${t("markdown.lineRef", { line: String(f.line) })}` : "";
2079
+ lines.push(
2080
+ `- ${mdSeverityIcon(f)} ${t(f.messageKey, f.messageParams)}${loc}`
2081
+ );
2082
+ }
2083
+ lines.push("");
2084
+ }
2085
+ if (report.actionPlan.length > 0) {
2086
+ lines.push(t("markdown.actionPlan"), "");
2087
+ for (let i = 0; i < Math.min(report.actionPlan.length, 10); i++) {
2088
+ const item = report.actionPlan[i];
2089
+ lines.push(`${i + 1}. ${item.description}`);
2090
+ }
2091
+ lines.push("");
2092
+ }
2093
+ if (extraSections.length > 0) {
2094
+ lines.push(...extraSections);
2095
+ }
2096
+ lines.push("---", t("markdown.attribution"));
2097
+ return lines.join("\n");
2098
+ }
2099
+
2100
+ // src/fixers/line-remover.ts
2101
+ var import_fs8 = require("fs");
2102
+ function removeLines(findings, categories) {
2103
+ const catSet = new Set(categories);
2104
+ const fixable = findings.filter(
2105
+ (f) => catSet.has(f.category) && f.autoFixable && f.line != null
2106
+ );
2107
+ const byFile = /* @__PURE__ */ new Map();
2108
+ for (const f of fixable) {
2109
+ const arr = byFile.get(f.file) ?? [];
2110
+ arr.push(f.line);
2111
+ byFile.set(f.file, arr);
2112
+ }
2113
+ let totalFixed = 0;
2114
+ for (const [filePath, lineNumbers] of byFile) {
2115
+ const uniqueSorted = [...new Set(lineNumbers)].sort((a, b) => b - a);
2116
+ const content = (0, import_fs8.readFileSync)(filePath, "utf8");
2117
+ const lines = content.split("\n");
2118
+ for (const lineNum of uniqueSorted) {
2119
+ const idx = lineNum - 1;
2120
+ if (idx >= 0 && idx < lines.length) {
2121
+ lines.splice(idx, 1);
2122
+ }
2123
+ }
2124
+ (0, import_fs8.writeFileSync)(filePath, lines.join("\n"));
2125
+ totalFixed += uniqueSorted.length;
2126
+ }
2127
+ return totalFixed;
2128
+ }
2129
+
2130
+ // src/fixers/remove-dead.ts
2131
+ function removeDeadRules(findings) {
2132
+ return removeLines(findings, ["dead-rule"]);
2133
+ }
2134
+
2135
+ // src/fixers/remove-stale.ts
2136
+ function removeStaleRefs(findings) {
2137
+ return removeLines(findings, ["stale-ref"]);
2138
+ }
2139
+
2140
+ // src/fixers/deduplicate.ts
2141
+ function deduplicateRules(findings) {
2142
+ return removeLines(findings, ["duplicate"]);
2143
+ }
2144
+
2145
+ // src/fixers/structure-suggestions.ts
2146
+ var import_fs9 = require("fs");
2147
+ var import_path7 = require("path");
2148
+ var import_chalk3 = __toESM(require("chalk"), 1);
2149
+ function readFileLine(filePath, lineNumber) {
2150
+ try {
2151
+ const content = (0, import_fs9.readFileSync)(filePath, "utf8");
2152
+ return content.split("\n")[lineNumber - 1]?.trim() ?? "";
2153
+ } catch {
2154
+ return "";
2155
+ }
2156
+ }
2157
+ var PATH_DIR_RE = /\b(src|tests?|lib|dist)\//i;
2158
+ function extractPathDir(text) {
2159
+ const m = PATH_DIR_RE.exec(text);
2160
+ return m?.[1]?.toLowerCase();
2161
+ }
2162
+ function buildHookSnippet(ruleText) {
2163
+ const comment = ruleText.length > 80 ? `${ruleText.slice(0, 80)}\u2026` : ruleText;
2164
+ return JSON.stringify(
2165
+ {
2166
+ hooks: {
2167
+ PreToolUse: [
2168
+ {
2169
+ matcher: "Bash",
2170
+ hooks: [
2171
+ {
2172
+ type: "command",
2173
+ command: `# TODO: implement enforcement of:
2174
+ # ${comment}`
2175
+ }
2176
+ ]
2177
+ }
2178
+ ]
2179
+ }
2180
+ },
2181
+ null,
2182
+ 2
2183
+ );
2184
+ }
2185
+ function buildPathScopedFile(pathDir, ruleText) {
2186
+ const filePath = `.claude/rules/${pathDir}.md`;
2187
+ const content = `---
2188
+ globs:
2189
+ - "${pathDir}/**"
2190
+ ---
2191
+
2192
+ ${ruleText}
2193
+ `;
2194
+ return { filePath, content };
2195
+ }
2196
+ function buildStructureSuggestions(findings) {
2197
+ const suggestions = [];
2198
+ for (const finding of findings) {
2199
+ if (finding.category !== "structure") continue;
2200
+ if (finding.messageKey !== "structure.scopeHook" && finding.messageKey !== "structure.scopePathScoped") {
2201
+ continue;
2202
+ }
2203
+ const ruleText = finding.line != null && finding.line > 0 ? readFileLine(finding.file, finding.line) || (finding.messageParams?.snippet ?? "") : finding.messageParams?.snippet ?? "";
2204
+ if (finding.messageKey === "structure.scopeHook") {
2205
+ suggestions.push({ finding, ruleText, type: "hook" });
2206
+ } else {
2207
+ const pathDir = extractPathDir(ruleText) || extractPathDir(finding.messageParams?.snippet ?? "") || "src";
2208
+ suggestions.push({ finding, ruleText, type: "path-scoped", pathDir });
2209
+ }
2210
+ }
2211
+ return suggestions;
2212
+ }
2213
+ function terminalCodeBlock(code, output) {
2214
+ const lines = code.split("\n");
2215
+ output.log(import_chalk3.default.gray(" \u250C" + "\u2500".repeat(62)));
2216
+ for (const line of lines) {
2217
+ output.log(` ${import_chalk3.default.gray("\u2502")} ${import_chalk3.default.white(line)}`);
2218
+ }
2219
+ output.log(import_chalk3.default.gray(" \u2514" + "\u2500".repeat(62)));
2220
+ }
2221
+ function printStructureSuggestions(suggestions, projectRoot, output) {
2222
+ if (suggestions.length === 0) return;
2223
+ output.log("");
2224
+ output.log(import_chalk3.default.bold.white(` ${t("fix.manualActions")}`));
2225
+ output.log(import_chalk3.default.gray(" \u2500".repeat(30)));
2226
+ for (const s of suggestions) {
2227
+ const relFile = (0, import_path7.relative)(projectRoot, s.finding.file);
2228
+ const lineNum = s.finding.line ?? 0;
2229
+ output.log("");
2230
+ output.log(
2231
+ ` ${import_chalk3.default.blue("\u2139")} ${import_chalk3.default.white(t(s.finding.messageKey, s.finding.messageParams))}`
2232
+ );
2233
+ output.log("");
2234
+ if (s.type === "hook") {
2235
+ output.log(` ${import_chalk3.default.cyan(t("fix.hookCreate"))}`);
2236
+ terminalCodeBlock(buildHookSnippet(s.ruleText), output);
2237
+ output.log(` ${import_chalk3.default.yellow(t("fix.hookWarning"))}`);
2238
+ if (lineNum > 0) {
2239
+ output.log(
2240
+ ` ${import_chalk3.default.gray(t("fix.thenRemoveLine", { line: String(lineNum), file: relFile }))}`
2241
+ );
2242
+ }
2243
+ } else {
2244
+ const dir = s.pathDir ?? "src";
2245
+ const { filePath, content } = buildPathScopedFile(dir, s.ruleText);
2246
+ output.log(` ${import_chalk3.default.cyan(t("fix.pathScopedCreate", { path: filePath }))}`);
2247
+ terminalCodeBlock(content, output);
2248
+ if (lineNum > 0) {
2249
+ output.log(
2250
+ ` ${import_chalk3.default.gray(t("fix.thenRemoveLine", { line: String(lineNum), file: relFile }))}`
2251
+ );
2252
+ }
2253
+ }
2254
+ }
2255
+ output.log("");
2256
+ }
2257
+ function markdownStructureSuggestions(suggestions, projectRoot) {
2258
+ if (suggestions.length === 0) return [];
2259
+ const lines = [`## ${t("fix.manualActions")}`, ""];
2260
+ for (const s of suggestions) {
2261
+ const relFile = (0, import_path7.relative)(projectRoot, s.finding.file);
2262
+ const lineNum = s.finding.line ?? 0;
2263
+ const icon = s.finding.severity === "critical" ? "\u{1F534}" : s.finding.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
2264
+ lines.push(
2265
+ `### ${icon} ${t(s.finding.messageKey, s.finding.messageParams)}`,
2266
+ ""
2267
+ );
2268
+ if (s.type === "hook") {
2269
+ lines.push(t("fix.hookCreate"), "", "```json", buildHookSnippet(s.ruleText), "```", "");
2270
+ lines.push(`> ${t("fix.hookWarning")}`, "");
2271
+ if (lineNum > 0) {
2272
+ lines.push(
2273
+ `_${t("fix.thenRemoveLine", { line: String(lineNum), file: relFile })}_`,
2274
+ ""
2275
+ );
2276
+ }
2277
+ } else {
2278
+ const dir = s.pathDir ?? "src";
2279
+ const { filePath, content } = buildPathScopedFile(dir, s.ruleText);
2280
+ lines.push(t("fix.pathScopedCreate", { path: filePath }), "");
2281
+ lines.push("```markdown", content, "```", "");
2282
+ if (lineNum > 0) {
2283
+ lines.push(
2284
+ `_${t("fix.thenRemoveLine", { line: String(lineNum), file: relFile })}_`,
2285
+ ""
2286
+ );
2287
+ }
2288
+ }
2289
+ }
2290
+ return lines;
2291
+ }
2292
+
2293
+ // src/commands/run-command.ts
2294
+ function isGitClean(cwd) {
2295
+ try {
2296
+ const out = (0, import_child_process.execSync)("git status --porcelain", { cwd, encoding: "utf8" });
2297
+ return out.trim().length === 0;
2298
+ } catch {
2299
+ return true;
2300
+ }
2301
+ }
2302
+ async function runAll(opts, output = console) {
2303
+ initLocale(opts.lang);
2304
+ await ensureInitialized();
2305
+ const projectRoot = opts.projectRoot ?? process.cwd();
2306
+ const scan = scanProject(projectRoot, opts.tool);
2307
+ if (scan.tool === "unknown") {
2308
+ output.error(t("error.unknownTool"));
2309
+ return { exitCode: 1, errorMessage: "unknown tool" };
2310
+ }
2311
+ if (scan.rootFilePath === null) {
2312
+ output.error(t("error.missingRootFile", { tool: scan.tool }));
2313
+ return { exitCode: 1, errorMessage: "missing root file" };
2314
+ }
2315
+ if (opts.fix && !opts.force && !isGitClean(projectRoot)) {
2316
+ output.error(t("error.dirtyWorkingTree"));
2317
+ return { exitCode: 1, errorMessage: "dirty working tree" };
2318
+ }
2319
+ const instructions = loadProject(projectRoot, scan.tool);
2320
+ const { findings: budgetFindings, summary } = analyzeBudget(instructions);
2321
+ const { findings: deadRuleFindings } = analyzeDeadRules(
2322
+ instructions,
2323
+ projectRoot
2324
+ );
2325
+ const { findings: structureFindings } = analyzeStructure(
2326
+ instructions,
2327
+ projectRoot
2328
+ );
2329
+ const allFindings = [
2330
+ ...budgetFindings,
2331
+ ...deadRuleFindings,
2332
+ ...structureFindings
2333
+ ];
2334
+ const { score, grade } = calculateScore(allFindings, summary);
2335
+ const actionPlan = buildActionPlan(allFindings);
2336
+ const report = {
2337
+ project: (0, import_path8.basename)(projectRoot),
2338
+ tool: instructions.tool,
2339
+ score,
2340
+ grade,
2341
+ locale: getLocale(),
2342
+ tokenMethod: summary.tokenMethod,
2343
+ findings: allFindings,
2344
+ budget: summary,
2345
+ actionPlan
2346
+ };
2347
+ if (opts.fix) {
2348
+ const suggestions = buildStructureSuggestions(allFindings);
2349
+ const deadFixed = removeDeadRules(allFindings);
2350
+ const staleFixed = removeStaleRefs(allFindings);
2351
+ const dupeFixed = deduplicateRules(allFindings);
2352
+ const total = deadFixed + staleFixed + dupeFixed;
2353
+ if (total === 0) {
2354
+ output.log(import_chalk4.default.green(` ${t("status.noAutoFixable")}`));
2355
+ } else {
2356
+ output.log("");
2357
+ output.log(import_chalk4.default.bold.white(` ${t("label.fixSummary")}`));
2358
+ output.log(import_chalk4.default.gray(" \u2500".repeat(30)));
2359
+ if (deadFixed > 0)
2360
+ output.log(
2361
+ ` ${import_chalk4.default.yellow("\u26A0")} ${t("fix.removedDeadRules", { count: String(deadFixed), s: plural(deadFixed) })}`
2362
+ );
2363
+ if (staleFixed > 0)
2364
+ output.log(
2365
+ ` ${import_chalk4.default.yellow("\u26A0")} ${t("fix.removedStaleRefs", { count: String(staleFixed), s: plural(staleFixed) })}`
2366
+ );
2367
+ if (dupeFixed > 0)
2368
+ output.log(
2369
+ ` ${import_chalk4.default.yellow("\u26A0")} ${t("fix.removedDuplicates", { count: String(dupeFixed), s: plural(dupeFixed) })}`
2370
+ );
2371
+ output.log(import_chalk4.default.gray(" \u2500".repeat(30)));
2372
+ output.log(
2373
+ import_chalk4.default.green(
2374
+ ` ${t("status.fixedIssues", { count: String(total), s: plural(total) })}`
2375
+ )
2376
+ );
2377
+ output.log("");
2378
+ }
2379
+ printStructureSuggestions(suggestions, projectRoot, output);
2380
+ return { exitCode: 0 };
2381
+ }
2382
+ if (opts.format === "json") {
2383
+ output.log(reportJson(report));
2384
+ return { exitCode: 0 };
2385
+ }
2386
+ if (opts.format === "markdown") {
2387
+ const mdSuggestions = buildStructureSuggestions(allFindings);
2388
+ const mdExtra = markdownStructureSuggestions(mdSuggestions, projectRoot);
2389
+ output.log(reportMarkdown(report, mdExtra));
2390
+ return { exitCode: 0 };
2391
+ }
2392
+ printCombinedTerminal(report, output);
2393
+ return { exitCode: 0 };
2394
+ }
2395
+
2396
+ // src/commands/deadrules-command.ts
2397
+ var import_chalk5 = __toESM(require("chalk"), 1);
2398
+ function printDeadRulesTerminal(findings, output = console) {
2399
+ const overlaps = findings.filter((f) => f.category === "dead-rule");
2400
+ const duplicates = findings.filter((f) => f.category === "duplicate");
2401
+ output.log("");
2402
+ output.log(import_chalk5.default.bold.white(` ${t("label.deadRules")}`));
2403
+ output.log(import_chalk5.default.gray(" \u2500".repeat(30)));
2404
+ if (overlaps.length > 0) {
2405
+ output.log(import_chalk5.default.bold(` ${t("label.redundantByConfig")}`));
2406
+ for (const f of overlaps) {
2407
+ output.log(` ${import_chalk5.default.yellow("\u26A0")} ${t(f.messageKey, f.messageParams)}`);
2408
+ }
2409
+ output.log("");
2410
+ }
2411
+ if (duplicates.length > 0) {
2412
+ output.log(import_chalk5.default.bold(` ${t("label.duplicates")}`));
2413
+ for (const f of duplicates) {
2414
+ const icon = f.severity === "warning" ? import_chalk5.default.yellow("\u26A0") : import_chalk5.default.blue("\u2139");
2415
+ output.log(` ${icon} ${t(f.messageKey, f.messageParams)}`);
2416
+ }
2417
+ output.log("");
2418
+ }
2419
+ output.log(import_chalk5.default.gray(" \u2500".repeat(30)));
2420
+ if (findings.length === 0) {
2421
+ output.log(import_chalk5.default.green(` ${t("status.noDeadRules")}`));
2422
+ } else {
2423
+ const sep = getLocale() === "zh-TW" ? "\u3001" : ", ";
2424
+ const parts = [];
2425
+ if (overlaps.length > 0)
2426
+ parts.push(
2427
+ t("summary.redundantRules", {
2428
+ count: String(overlaps.length),
2429
+ s: plural(overlaps.length)
2430
+ })
2431
+ );
2432
+ if (duplicates.length > 0)
2433
+ parts.push(
2434
+ t("summary.duplicates", {
2435
+ count: String(duplicates.length),
2436
+ s: plural(duplicates.length)
2437
+ })
2438
+ );
2439
+ output.log(
2440
+ import_chalk5.default.yellow(` ${t("summary.found", { parts: parts.join(sep) })}`)
2441
+ );
2442
+ }
2443
+ output.log("");
2444
+ }
2445
+ async function runDeadRules(opts, output = console) {
2446
+ initLocale(opts.lang);
2447
+ await ensureInitialized();
2448
+ const projectRoot = opts.projectRoot ?? process.cwd();
2449
+ const scan = scanProject(projectRoot, opts.tool);
2450
+ if (scan.tool === "unknown") {
2451
+ output.error(t("error.unknownTool"));
2452
+ return { exitCode: 1, errorMessage: "unknown tool" };
2453
+ }
2454
+ if (scan.rootFilePath === null) {
2455
+ output.error(t("error.missingRootFile", { tool: scan.tool }));
2456
+ return { exitCode: 1, errorMessage: "missing root file" };
2457
+ }
2458
+ const instructions = loadProject(projectRoot, scan.tool);
2459
+ const { findings } = analyzeDeadRules(instructions, projectRoot);
2460
+ if (opts.format === "json") {
2461
+ output.log(JSON.stringify({ findings }, null, 2));
2462
+ return { exitCode: 0 };
2463
+ }
2464
+ printDeadRulesTerminal(findings, output);
2465
+ return { exitCode: 0 };
2466
+ }
2467
+
2468
+ // src/commands/structure-command.ts
2469
+ var import_chalk6 = __toESM(require("chalk"), 1);
2470
+ function printStructureTerminal(findings, output = console) {
2471
+ const contradictions = findings.filter((f) => f.category === "contradiction");
2472
+ const staleRefs = findings.filter((f) => f.category === "stale-ref");
2473
+ const scoped = findings.filter((f) => f.category === "structure");
2474
+ output.log("");
2475
+ output.log(import_chalk6.default.bold.white(` ${t("label.structure")}`));
2476
+ output.log(import_chalk6.default.gray(" \u2500".repeat(30)));
2477
+ if (contradictions.length > 0) {
2478
+ output.log(import_chalk6.default.bold(` ${t("label.contradictions")}`));
2479
+ for (const f of contradictions) {
2480
+ output.log(` ${import_chalk6.default.red("\u2716")} ${t(f.messageKey, f.messageParams)}`);
2481
+ }
2482
+ output.log("");
2483
+ }
2484
+ if (staleRefs.length > 0) {
2485
+ output.log(import_chalk6.default.bold(` ${t("label.staleReferences")}`));
2486
+ for (const f of staleRefs) {
2487
+ output.log(` ${import_chalk6.default.yellow("\u26A0")} ${t(f.messageKey, f.messageParams)}`);
2488
+ }
2489
+ output.log("");
2490
+ }
2491
+ if (scoped.length > 0) {
2492
+ output.log(import_chalk6.default.bold(` ${t("label.refactoringOpportunities")}`));
2493
+ for (const f of scoped) {
2494
+ output.log(` ${import_chalk6.default.blue("\u2139")} ${t(f.messageKey, f.messageParams)}`);
2495
+ }
2496
+ output.log("");
2497
+ }
2498
+ output.log(import_chalk6.default.gray(" \u2500".repeat(30)));
2499
+ if (findings.length === 0) {
2500
+ output.log(import_chalk6.default.green(` ${t("status.noStructuralIssues")}`));
2501
+ } else {
2502
+ const sep = getLocale() === "zh-TW" ? "\u3001" : ", ";
2503
+ const parts = [];
2504
+ if (contradictions.length > 0)
2505
+ parts.push(
2506
+ t("summary.contradictions", {
2507
+ count: String(contradictions.length),
2508
+ s: plural(contradictions.length)
2509
+ })
2510
+ );
2511
+ if (staleRefs.length > 0)
2512
+ parts.push(
2513
+ t("summary.staleRefs", {
2514
+ count: String(staleRefs.length),
2515
+ s: plural(staleRefs.length)
2516
+ })
2517
+ );
2518
+ if (scoped.length > 0)
2519
+ parts.push(
2520
+ t("summary.refactoringSuggestions", {
2521
+ count: String(scoped.length),
2522
+ s: plural(scoped.length)
2523
+ })
2524
+ );
2525
+ output.log(
2526
+ import_chalk6.default.yellow(` ${t("summary.found", { parts: parts.join(sep) })}`)
2527
+ );
2528
+ }
2529
+ output.log("");
2530
+ }
2531
+ async function runStructure(opts, output = console) {
2532
+ initLocale(opts.lang);
2533
+ await ensureInitialized();
2534
+ const projectRoot = opts.projectRoot ?? process.cwd();
2535
+ const scan = scanProject(projectRoot, opts.tool);
2536
+ if (scan.tool === "unknown") {
2537
+ output.error(t("error.unknownTool"));
2538
+ return { exitCode: 1, errorMessage: "unknown tool" };
2539
+ }
2540
+ if (scan.rootFilePath === null) {
2541
+ output.error(t("error.missingRootFile", { tool: scan.tool }));
2542
+ return { exitCode: 1, errorMessage: "missing root file" };
2543
+ }
2544
+ const instructions = loadProject(projectRoot, scan.tool);
2545
+ const { findings } = analyzeStructure(instructions, projectRoot);
2546
+ if (opts.format === "json") {
2547
+ output.log(JSON.stringify({ findings }, null, 2));
2548
+ return { exitCode: 0 };
2549
+ }
2550
+ printStructureTerminal(findings, output);
2551
+ return { exitCode: 0 };
2552
+ }
2553
+
2554
+ // src/commands/ci-command.ts
2555
+ var import_fs10 = require("fs");
2556
+ var import_path9 = require("path");
2557
+
2558
+ // src/reporters/sarif.ts
2559
+ function severityToLevel(severity) {
2560
+ if (severity === "critical") return "error";
2561
+ if (severity === "warning") return "warning";
2562
+ return "note";
2563
+ }
2564
+ function findingToRuleId(f) {
2565
+ return `instrlint/${f.category}/${f.messageKey.replace(/\./g, "/")}`;
2566
+ }
2567
+ function buildRules(findings) {
2568
+ const seen = /* @__PURE__ */ new Set();
2569
+ const rules = [];
2570
+ for (const f of findings) {
2571
+ const id = findingToRuleId(f);
2572
+ if (seen.has(id)) continue;
2573
+ seen.add(id);
2574
+ rules.push({
2575
+ id,
2576
+ name: f.messageKey,
2577
+ shortDescription: { text: `${f.category}: ${f.messageKey}` }
2578
+ });
2579
+ }
2580
+ return rules;
2581
+ }
2582
+ function reportSarif(report) {
2583
+ const rules = buildRules(report.findings);
2584
+ const results = report.findings.map((f) => ({
2585
+ ruleId: findingToRuleId(f),
2586
+ level: severityToLevel(f.severity),
2587
+ message: { text: f.suggestion },
2588
+ locations: [
2589
+ {
2590
+ physicalLocation: {
2591
+ artifactLocation: {
2592
+ uri: f.file.replace(/\\/g, "/"),
2593
+ uriBaseId: "%SRCROOT%"
2594
+ },
2595
+ ...f.line != null ? { region: { startLine: f.line } } : {}
2596
+ }
2597
+ }
2598
+ ]
2599
+ }));
2600
+ const log = {
2601
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
2602
+ version: "2.1.0",
2603
+ runs: [
2604
+ {
2605
+ tool: {
2606
+ driver: {
2607
+ name: "instrlint",
2608
+ version: "0.1.0",
2609
+ informationUri: "https://github.com/jed1978/instrlint",
2610
+ rules
2611
+ }
2612
+ },
2613
+ results
2614
+ }
2615
+ ]
2616
+ };
2617
+ return JSON.stringify(log, null, 2);
2618
+ }
2619
+
2620
+ // src/commands/ci-command.ts
2621
+ function shouldFail(findings, failOn) {
2622
+ if (failOn === "info") return findings.length > 0;
2623
+ if (failOn === "warning")
2624
+ return findings.some(
2625
+ (f) => f.severity === "critical" || f.severity === "warning"
2626
+ );
2627
+ return findings.some((f) => f.severity === "critical");
2628
+ }
2629
+ async function runCi(opts, output = console) {
2630
+ initLocale(opts.lang);
2631
+ await ensureInitialized();
2632
+ const projectRoot = opts.projectRoot ?? process.cwd();
2633
+ const failOn = opts.failOn ?? "critical";
2634
+ const format = opts.format ?? "terminal";
2635
+ const scan = scanProject(projectRoot, opts.tool);
2636
+ if (scan.tool === "unknown") {
2637
+ output.error(t("error.unknownTool"));
2638
+ return { exitCode: 1, errorMessage: "unknown tool" };
2639
+ }
2640
+ if (scan.rootFilePath === null) {
2641
+ output.error(t("error.missingRootFile", { tool: scan.tool }));
2642
+ return { exitCode: 1, errorMessage: "missing root file" };
2643
+ }
2644
+ const instructions = loadProject(projectRoot, scan.tool);
2645
+ const { findings: budgetFindings, summary } = analyzeBudget(instructions);
2646
+ const { findings: deadRuleFindings } = analyzeDeadRules(
2647
+ instructions,
2648
+ projectRoot
2649
+ );
2650
+ const { findings: structureFindings } = analyzeStructure(
2651
+ instructions,
2652
+ projectRoot
2653
+ );
2654
+ const allFindings = [
2655
+ ...budgetFindings,
2656
+ ...deadRuleFindings,
2657
+ ...structureFindings
2658
+ ];
2659
+ const { score, grade } = calculateScore(allFindings, summary);
2660
+ const actionPlan = buildActionPlan(allFindings);
2661
+ const report = {
2662
+ project: (0, import_path9.basename)(projectRoot),
2663
+ tool: instructions.tool,
2664
+ score,
2665
+ grade,
2666
+ locale: getLocale(),
2667
+ tokenMethod: summary.tokenMethod,
2668
+ findings: allFindings,
2669
+ budget: summary,
2670
+ actionPlan
2671
+ };
2672
+ let formatted;
2673
+ if (format === "sarif") {
2674
+ formatted = reportSarif(report);
2675
+ } else if (format === "json") {
2676
+ formatted = reportJson(report);
2677
+ } else if (format === "markdown") {
2678
+ formatted = reportMarkdown(report);
2679
+ } else {
2680
+ formatted = reportJson(report);
2681
+ }
2682
+ if (opts.output != null) {
2683
+ (0, import_fs10.writeFileSync)(opts.output, formatted, "utf8");
2684
+ const pass = !shouldFail(allFindings, failOn);
2685
+ const statusKey = pass ? "ci.passed" : "ci.failed";
2686
+ output.error(
2687
+ `${t(statusKey, { score: String(score), grade })} ${t("ci.writtenTo", { file: opts.output })}`
2688
+ );
2689
+ } else {
2690
+ output.log(formatted);
2691
+ }
2692
+ const failed = shouldFail(allFindings, failOn);
2693
+ return { exitCode: failed ? 1 : 0 };
2694
+ }
2695
+
2696
+ // src/commands/init-ci-command.ts
2697
+ var import_fs11 = require("fs");
2698
+ var import_path10 = require("path");
2699
+ function githubWorkflow() {
2700
+ return `name: instrlint
2701
+
2702
+ on:
2703
+ push:
2704
+ paths:
2705
+ - 'CLAUDE.md'
2706
+ - '.claude/**'
2707
+ - 'AGENTS.md'
2708
+ - '.agents/**'
2709
+ - '.cursorrules'
2710
+ - '.cursor/**'
2711
+ pull_request:
2712
+ paths:
2713
+ - 'CLAUDE.md'
2714
+ - '.claude/**'
2715
+ - 'AGENTS.md'
2716
+ - '.agents/**'
2717
+ - '.cursorrules'
2718
+ - '.cursor/**'
2719
+
2720
+ jobs:
2721
+ instrlint:
2722
+ name: Lint instruction files
2723
+ runs-on: ubuntu-latest
2724
+ permissions:
2725
+ contents: read
2726
+ security-events: write
2727
+
2728
+ steps:
2729
+ - uses: actions/checkout@v4
2730
+
2731
+ - uses: actions/setup-node@v4
2732
+ with:
2733
+ node-version: '20'
2734
+
2735
+ - name: Run instrlint
2736
+ run: npx instrlint@latest ci --fail-on warning --format sarif --output instrlint.sarif
2737
+
2738
+ - name: Upload SARIF
2739
+ if: always()
2740
+ uses: github/codeql-action/upload-sarif@v3
2741
+ with:
2742
+ sarif_file: instrlint.sarif
2743
+ category: instrlint
2744
+ `;
2745
+ }
2746
+ function gitlabSnippet() {
2747
+ return `# Add this to your .gitlab-ci.yml
2748
+ instrlint:
2749
+ image: node:20
2750
+ stage: test
2751
+ rules:
2752
+ - changes:
2753
+ - CLAUDE.md
2754
+ - .claude/**/*
2755
+ - AGENTS.md
2756
+ - .agents/**/*
2757
+ - .cursorrules
2758
+ - .cursor/**/*
2759
+ script:
2760
+ - npx instrlint@latest ci --fail-on warning --format json
2761
+ allow_failure: false
2762
+ `;
2763
+ }
2764
+ function runInitCi(opts, output = console) {
2765
+ const projectRoot = opts.projectRoot ?? process.cwd();
2766
+ if (opts.github) {
2767
+ const workflowDir = (0, import_path10.join)(projectRoot, ".github", "workflows");
2768
+ const workflowPath = (0, import_path10.join)(workflowDir, "instrlint.yml");
2769
+ if ((0, import_fs11.existsSync)(workflowPath) && !opts.force) {
2770
+ output.error(t("initCi.alreadyExists", { path: workflowPath }));
2771
+ return { exitCode: 1, errorMessage: "file already exists" };
2772
+ }
2773
+ (0, import_fs11.mkdirSync)(workflowDir, { recursive: true });
2774
+ (0, import_fs11.writeFileSync)(workflowPath, githubWorkflow(), "utf8");
2775
+ output.log(t("initCi.created", { path: workflowPath }));
2776
+ return { exitCode: 0 };
2777
+ }
2778
+ if (opts.gitlab) {
2779
+ output.log(gitlabSnippet());
2780
+ return { exitCode: 0 };
2781
+ }
2782
+ output.error("init-ci: specify --github or --gitlab");
2783
+ return { exitCode: 1, errorMessage: "no target specified" };
2784
+ }
2785
+
2786
+ // src/commands/install-command.ts
2787
+ var import_fs12 = require("fs");
2788
+ var import_path11 = require("path");
2789
+ var import_os = require("os");
2790
+ var import_url = require("url");
2791
+ function resolveSkillFile(target) {
2792
+ const thisFile = (0, import_url.fileURLToPath)(importMetaUrl);
2793
+ const packageRoot = (0, import_path11.join)(thisFile, "..", "..", "..");
2794
+ const skillPath = (0, import_path11.join)(
2795
+ packageRoot,
2796
+ "skills",
2797
+ target === "claude-code" ? "claude-code" : "codex",
2798
+ "SKILL.md"
2799
+ );
2800
+ return skillPath;
2801
+ }
2802
+ function readSkillContent(target) {
2803
+ const skillPath = resolveSkillFile(target);
2804
+ try {
2805
+ return (0, import_fs12.readFileSync)(skillPath, "utf8");
2806
+ } catch {
2807
+ throw new Error(
2808
+ `Could not read skill file at ${skillPath}. Make sure the package is properly installed.`
2809
+ );
2810
+ }
2811
+ }
2812
+ function installClaudeCode(content, projectRoot, isProject, force, output) {
2813
+ const targetDir = isProject ? (0, import_path11.join)(projectRoot, ".claude", "skills", "instrlint") : (0, import_path11.join)((0, import_os.homedir)(), ".claude", "skills", "instrlint");
2814
+ const targetPath = (0, import_path11.join)(targetDir, "SKILL.md");
2815
+ if ((0, import_fs12.existsSync)(targetPath) && !force) {
2816
+ output.error(t("install.alreadyExists", { path: targetPath }));
2817
+ return { exitCode: 1, errorMessage: "file already exists" };
2818
+ }
2819
+ (0, import_fs12.mkdirSync)(targetDir, { recursive: true });
2820
+ (0, import_fs12.writeFileSync)(targetPath, content, "utf8");
2821
+ output.log(t("install.installed", { path: targetPath }));
2822
+ return { exitCode: 0 };
2823
+ }
2824
+ function installCodex(content, projectRoot, force, output) {
2825
+ const targetDir = (0, import_path11.join)(projectRoot, ".agents", "skills", "instrlint");
2826
+ const targetPath = (0, import_path11.join)(targetDir, "SKILL.md");
2827
+ if ((0, import_fs12.existsSync)(targetPath) && !force) {
2828
+ output.error(t("install.alreadyExists", { path: targetPath }));
2829
+ return { exitCode: 1, errorMessage: "file already exists" };
2830
+ }
2831
+ (0, import_fs12.mkdirSync)(targetDir, { recursive: true });
2832
+ (0, import_fs12.writeFileSync)(targetPath, content, "utf8");
2833
+ output.log(t("install.installed", { path: targetPath }));
2834
+ return { exitCode: 0 };
2835
+ }
2836
+ function runInstall(opts, output = console) {
2837
+ const projectRoot = opts.projectRoot ?? process.cwd();
2838
+ const force = opts.force ?? false;
2839
+ if (opts.claudeCode) {
2840
+ let content;
2841
+ try {
2842
+ content = readSkillContent("claude-code");
2843
+ } catch (err) {
2844
+ output.error(String(err));
2845
+ return { exitCode: 1, errorMessage: String(err) };
2846
+ }
2847
+ return installClaudeCode(
2848
+ content,
2849
+ projectRoot,
2850
+ opts.project ?? false,
2851
+ force,
2852
+ output
2853
+ );
2854
+ }
2855
+ if (opts.codex) {
2856
+ let content;
2857
+ try {
2858
+ content = readSkillContent("codex");
2859
+ } catch (err) {
2860
+ output.error(String(err));
2861
+ return { exitCode: 1, errorMessage: String(err) };
2862
+ }
2863
+ return installCodex(content, projectRoot, force, output);
2864
+ }
2865
+ output.error(t("install.unknownTarget"));
2866
+ return { exitCode: 1, errorMessage: "no target specified" };
2867
+ }
2868
+
2869
+ // src/cli.ts
2870
+ var program = new import_commander.Command();
2871
+ program.enablePositionalOptions().name("instrlint").description(
2872
+ "Lint and optimize your CLAUDE.md / AGENTS.md \u2014 find dead rules, token waste, and structural issues"
2873
+ ).version("0.1.0").option(
2874
+ "--format <type>",
2875
+ "output format (terminal|json|markdown)",
2876
+ "terminal"
2877
+ ).option("--lang <locale>", "output language (en|zh-TW)", "en").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").option("--fix", "auto-fix safe issues (dead rules, stale refs, dupes)").option("--force", "skip git clean check when using --fix").action(async function() {
2878
+ const opts = this.opts();
2879
+ const result = await runAll(opts);
2880
+ if (result.exitCode !== 0) process.exit(result.exitCode);
2881
+ });
2882
+ program.command("budget").description("Token budget analysis only").option(
2883
+ "--format <type>",
2884
+ "output format (terminal|json|markdown)",
2885
+ "terminal"
2886
+ ).option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2887
+ const opts = this.opts();
2888
+ const lang = this.parent?.opts()?.lang;
2889
+ const result = await runBudget({
2890
+ ...opts,
2891
+ ...lang !== void 0 && { lang }
2892
+ });
2893
+ if (result.exitCode !== 0) process.exit(result.exitCode);
2894
+ });
2895
+ program.command("deadrules").description("Dead rule detection only").option("--format <type>", "output format (terminal|json)", "terminal").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2896
+ const opts = this.opts();
2897
+ const lang = this.parent?.opts()?.lang;
2898
+ const result = await runDeadRules({
2899
+ ...opts,
2900
+ ...lang !== void 0 && { lang }
2901
+ });
2902
+ if (result.exitCode !== 0) process.exit(result.exitCode);
2903
+ });
2904
+ program.command("structure").description("Structural analysis only").option("--format <type>", "output format (terminal|json)", "terminal").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2905
+ const opts = this.opts();
2906
+ const lang = this.parent?.opts()?.lang;
2907
+ const result = await runStructure({
2908
+ ...opts,
2909
+ ...lang !== void 0 && { lang }
2910
+ });
2911
+ if (result.exitCode !== 0) process.exit(result.exitCode);
2912
+ });
2913
+ program.command("ci").description(
2914
+ "CI mode: run full analysis and exit 1 if findings exceed threshold"
2915
+ ).option(
2916
+ "--fail-on <level>",
2917
+ "failure threshold (critical|warning|info)",
2918
+ "critical"
2919
+ ).option("--format <type>", "output format (json|markdown|sarif)", "json").option("--output <file>", "write output to file instead of stdout").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
2920
+ const opts = this.opts();
2921
+ const lang = this.parent?.opts()?.lang;
2922
+ initLocale(lang);
2923
+ const result = await runCi({
2924
+ failOn: opts.failOn,
2925
+ format: opts.format,
2926
+ ...opts.output !== void 0 && { output: opts.output },
2927
+ ...opts.tool !== void 0 && { tool: opts.tool },
2928
+ ...lang !== void 0 && { lang }
2929
+ });
2930
+ if (result.exitCode !== 0) process.exit(result.exitCode);
2931
+ });
2932
+ program.command("init-ci").description("Generate CI configuration for instrlint").option("--github", "Generate GitHub Actions workflow").option("--gitlab", "Generate GitLab CI snippet (prints to stdout)").option("--force", "overwrite existing files").action(function() {
2933
+ const opts = this.opts();
2934
+ const result = runInitCi(opts);
2935
+ if (result.exitCode !== 0) process.exit(result.exitCode);
2936
+ });
2937
+ program.command("install").description("Install instrlint as a skill").option("--claude-code", "Install as Claude Code skill").option("--codex", "Install as Codex skill").option("--project", "Install into current project (instead of global)").option("--force", "overwrite existing skill file").action(function() {
2938
+ const opts = this.opts();
2939
+ const result = runInstall(opts);
2940
+ if (result.exitCode !== 0) process.exit(result.exitCode);
2941
+ });
2942
+ program.parse();
2943
+ //# sourceMappingURL=cli.cjs.map