@yawlabs/ctxlint 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1562 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/utils/fs.ts
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ function loadPackageJson(projectRoot) {
13
+ try {
14
+ const content = fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8");
15
+ return JSON.parse(content);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function fileExists(filePath) {
21
+ try {
22
+ fs.accessSync(filePath);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ function isDirectory(filePath) {
29
+ try {
30
+ return fs.statSync(filePath).isDirectory();
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+ function isSymlink(filePath) {
36
+ try {
37
+ return fs.lstatSync(filePath).isSymbolicLink();
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+ function readSymlinkTarget(filePath) {
43
+ try {
44
+ return fs.readlinkSync(filePath);
45
+ } catch {
46
+ return void 0;
47
+ }
48
+ }
49
+ function readFileContent(filePath) {
50
+ return fs.readFileSync(filePath, "utf-8");
51
+ }
52
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
53
+ "node_modules",
54
+ ".git",
55
+ "dist",
56
+ "build",
57
+ "vendor",
58
+ ".next",
59
+ ".nuxt",
60
+ "coverage",
61
+ "__pycache__"
62
+ ]);
63
+ function getAllProjectFiles(projectRoot) {
64
+ const files = [];
65
+ function walk(dir, depth) {
66
+ if (depth > 10) return;
67
+ try {
68
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
69
+ for (const entry of entries) {
70
+ if (IGNORED_DIRS.has(entry.name)) continue;
71
+ const fullPath = path.join(dir, entry.name);
72
+ if (entry.isDirectory()) {
73
+ walk(fullPath, depth + 1);
74
+ } else {
75
+ files.push(path.relative(projectRoot, fullPath));
76
+ }
77
+ }
78
+ } catch {
79
+ }
80
+ }
81
+ walk(projectRoot, 0);
82
+ return files;
83
+ }
84
+
85
+ // src/core/scanner.ts
86
+ import * as fs2 from "fs";
87
+ import * as path2 from "path";
88
+ import { glob } from "glob";
89
+ var CONTEXT_FILE_PATTERNS = [
90
+ // Claude Code
91
+ "CLAUDE.md",
92
+ "CLAUDE.local.md",
93
+ ".claude/rules/*.md",
94
+ // AGENTS.md (AAIF / Linux Foundation standard)
95
+ "AGENTS.md",
96
+ "AGENT.md",
97
+ "AGENTS.override.md",
98
+ // Cursor
99
+ ".cursorrules",
100
+ ".cursor/rules/*.md",
101
+ ".cursor/rules/*.mdc",
102
+ ".cursor/rules/*/RULE.md",
103
+ // GitHub Copilot
104
+ ".github/copilot-instructions.md",
105
+ ".github/instructions/*.md",
106
+ ".github/git-commit-instructions.md",
107
+ // Windsurf
108
+ ".windsurfrules",
109
+ ".windsurf/rules/*.md",
110
+ // Gemini CLI
111
+ "GEMINI.md",
112
+ // Cline
113
+ ".clinerules",
114
+ // Aider — note: .aiderules has no file extension; this is the intended format
115
+ ".aiderules",
116
+ // Aide / Codestory
117
+ ".aide/rules/*.md",
118
+ // Amazon Q Developer
119
+ ".amazonq/rules/*.md",
120
+ // Goose (Block)
121
+ ".goose/instructions.md",
122
+ ".goosehints",
123
+ // JetBrains Junie
124
+ ".junie/guidelines.md",
125
+ ".junie/AGENTS.md",
126
+ // JetBrains AI Assistant
127
+ ".aiassistant/rules/*.md",
128
+ // Continue
129
+ ".continuerules",
130
+ ".continue/rules/*.md",
131
+ // Zed
132
+ ".rules",
133
+ // Replit
134
+ "replit.md"
135
+ ];
136
+ var IGNORED_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor"]);
137
+ async function scanForContextFiles(projectRoot, options = {}) {
138
+ const maxDepth = options.depth ?? 2;
139
+ const patterns = [...CONTEXT_FILE_PATTERNS, ...options.extraPatterns || []];
140
+ const found = [];
141
+ const seen = /* @__PURE__ */ new Set();
142
+ const dirsToScan = [projectRoot];
143
+ function collectDirs(dir, currentDepth) {
144
+ if (currentDepth >= maxDepth) return;
145
+ try {
146
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
147
+ for (const entry of entries) {
148
+ if (entry.isDirectory() && !IGNORED_DIRS2.has(entry.name) && !entry.name.startsWith(".")) {
149
+ const fullPath = path2.join(dir, entry.name);
150
+ dirsToScan.push(fullPath);
151
+ collectDirs(fullPath, currentDepth + 1);
152
+ }
153
+ }
154
+ } catch {
155
+ }
156
+ }
157
+ collectDirs(projectRoot, 0);
158
+ for (const dir of dirsToScan) {
159
+ for (const pattern of patterns) {
160
+ const matches = await glob(pattern, {
161
+ cwd: dir,
162
+ absolute: true,
163
+ nodir: true,
164
+ dot: true
165
+ });
166
+ for (const match of matches) {
167
+ const normalized = path2.normalize(match);
168
+ if (seen.has(normalized)) continue;
169
+ seen.add(normalized);
170
+ const relativePath = path2.relative(projectRoot, normalized);
171
+ const symlink = isSymlink(normalized);
172
+ const target = symlink ? readSymlinkTarget(normalized) : void 0;
173
+ found.push({
174
+ absolutePath: normalized,
175
+ relativePath: relativePath.replace(/\\/g, "/"),
176
+ isSymlink: symlink,
177
+ symlinkTarget: target
178
+ });
179
+ }
180
+ }
181
+ }
182
+ return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
183
+ }
184
+
185
+ // src/utils/tokens.ts
186
+ import { encoding_for_model } from "tiktoken";
187
+ var encoder = null;
188
+ function getEncoder() {
189
+ if (!encoder) {
190
+ encoder = encoding_for_model("gpt-4");
191
+ }
192
+ return encoder;
193
+ }
194
+ function countTokens(text) {
195
+ try {
196
+ const enc = getEncoder();
197
+ const tokens = enc.encode(text);
198
+ return tokens.length;
199
+ } catch {
200
+ return Math.ceil(text.length / 4);
201
+ }
202
+ }
203
+ function freeEncoder() {
204
+ if (encoder) {
205
+ encoder.free();
206
+ encoder = null;
207
+ }
208
+ }
209
+
210
+ // src/core/parser.ts
211
+ var PATH_PATTERN = /(?:^|[\s`"'(])((\.{0,2}\/)?(?:[\w@.-]+\/)+[\w.*-]+(?:\.\w+)?)(?=[\s`"'),;:]|$)/gm;
212
+ var PATH_EXCLUDE = /^(https?:\/\/|ftp:\/\/|mailto:|n\/a|w\/o|I\/O|i\/o|e\.g\.|N\/A|\.deb\/|\.rpm[.\/]|\.tar[.\/]|\.zip[.\/])/i;
213
+ var COMMAND_PREFIXES = /^\s*[\$>]\s+(.+)$/;
214
+ var COMMON_COMMANDS = /^(npm\s+run|npx|pnpm|yarn|make|cargo|go\s+(run|build|test)|python|pytest|vitest|jest|bun|deno)\b/;
215
+ function parseContextFile(file) {
216
+ const content = readFileContent(file.absolutePath);
217
+ const lines = content.split("\n");
218
+ const sections = parseSections(lines);
219
+ const paths = extractPathReferences(lines, sections);
220
+ const commands = extractCommandReferences(lines, sections);
221
+ return {
222
+ filePath: file.absolutePath,
223
+ relativePath: file.relativePath,
224
+ isSymlink: file.isSymlink,
225
+ symlinkTarget: file.symlinkTarget,
226
+ totalTokens: countTokens(content),
227
+ totalLines: lines.length,
228
+ content,
229
+ sections,
230
+ references: {
231
+ paths,
232
+ commands
233
+ }
234
+ };
235
+ }
236
+ function parseSections(lines) {
237
+ const sections = [];
238
+ for (let i = 0; i < lines.length; i++) {
239
+ const match = lines[i].match(/^(#{1,6})\s+(.+)/);
240
+ if (match) {
241
+ if (sections.length > 0) {
242
+ const prev = sections[sections.length - 1];
243
+ if (prev.endLine === -1) {
244
+ prev.endLine = i - 1;
245
+ }
246
+ }
247
+ sections.push({
248
+ title: match[2].trim(),
249
+ startLine: i + 1,
250
+ // 1-indexed
251
+ endLine: -1,
252
+ level: match[1].length
253
+ });
254
+ }
255
+ }
256
+ if (sections.length > 0) {
257
+ const last = sections[sections.length - 1];
258
+ if (last.endLine === -1) {
259
+ last.endLine = lines.length;
260
+ }
261
+ }
262
+ return sections;
263
+ }
264
+ function getSectionForLine(line, sections) {
265
+ for (let i = sections.length - 1; i >= 0; i--) {
266
+ if (line >= sections[i].startLine) {
267
+ return sections[i].title;
268
+ }
269
+ }
270
+ return void 0;
271
+ }
272
+ function extractPathReferences(lines, sections) {
273
+ const paths = [];
274
+ let inCodeBlock = false;
275
+ let codeBlockLang = "";
276
+ for (let i = 0; i < lines.length; i++) {
277
+ const line = lines[i];
278
+ if (line.trimStart().startsWith("```")) {
279
+ if (!inCodeBlock) {
280
+ inCodeBlock = true;
281
+ codeBlockLang = line.trimStart().slice(3).trim().toLowerCase();
282
+ } else {
283
+ inCodeBlock = false;
284
+ codeBlockLang = "";
285
+ }
286
+ continue;
287
+ }
288
+ if (inCodeBlock && isExampleCodeBlock(codeBlockLang)) {
289
+ continue;
290
+ }
291
+ PATH_PATTERN.lastIndex = 0;
292
+ let match;
293
+ while ((match = PATH_PATTERN.exec(line)) !== null) {
294
+ const value = match[1];
295
+ if (PATH_EXCLUDE.test(value)) continue;
296
+ if (value.length < 3) continue;
297
+ if (/^v?\d+\.\d+\//.test(value)) continue;
298
+ const column = match.index + match[0].length - match[1].length + 1;
299
+ paths.push({
300
+ value,
301
+ line: i + 1,
302
+ // 1-indexed
303
+ column,
304
+ section: getSectionForLine(i + 1, sections)
305
+ });
306
+ }
307
+ }
308
+ return paths;
309
+ }
310
+ function isExampleCodeBlock(lang) {
311
+ return [
312
+ "javascript",
313
+ "js",
314
+ "typescript",
315
+ "ts",
316
+ "python",
317
+ "py",
318
+ "go",
319
+ "rust",
320
+ "java",
321
+ "c",
322
+ "cpp",
323
+ "ruby",
324
+ "php",
325
+ "json",
326
+ "yaml",
327
+ "yml",
328
+ "toml",
329
+ "xml",
330
+ "html",
331
+ "css",
332
+ "sql",
333
+ "graphql",
334
+ "jsx",
335
+ "tsx"
336
+ ].includes(lang);
337
+ }
338
+ function extractCommandReferences(lines, sections) {
339
+ const commands = [];
340
+ let inCodeBlock = false;
341
+ let codeBlockLang = "";
342
+ for (let i = 0; i < lines.length; i++) {
343
+ const line = lines[i];
344
+ if (line.trimStart().startsWith("```")) {
345
+ if (!inCodeBlock) {
346
+ inCodeBlock = true;
347
+ codeBlockLang = line.trimStart().slice(3).trim().toLowerCase();
348
+ } else {
349
+ inCodeBlock = false;
350
+ codeBlockLang = "";
351
+ }
352
+ continue;
353
+ }
354
+ const prefixMatch = line.match(COMMAND_PREFIXES);
355
+ if (prefixMatch) {
356
+ commands.push({
357
+ value: prefixMatch[1].trim(),
358
+ line: i + 1,
359
+ column: prefixMatch.index + prefixMatch[0].length - prefixMatch[1].length + 1,
360
+ section: getSectionForLine(i + 1, sections)
361
+ });
362
+ continue;
363
+ }
364
+ if (inCodeBlock && ["bash", "sh", "shell", "zsh", ""].includes(codeBlockLang)) {
365
+ const trimmed = line.trim();
366
+ if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("//")) {
367
+ if (COMMON_COMMANDS.test(trimmed) || trimmed.startsWith("$") || trimmed.startsWith(">")) {
368
+ const cmd = trimmed.replace(/^\s*[\$>]\s*/, "");
369
+ if (cmd) {
370
+ const cmdStart = line.indexOf(trimmed) + trimmed.indexOf(cmd);
371
+ commands.push({
372
+ value: cmd,
373
+ line: i + 1,
374
+ column: cmdStart + 1,
375
+ section: getSectionForLine(i + 1, sections)
376
+ });
377
+ }
378
+ }
379
+ }
380
+ continue;
381
+ }
382
+ const inlineMatches = line.matchAll(/`([^`]+)`/g);
383
+ for (const m of inlineMatches) {
384
+ const cmd = m[1].trim();
385
+ if (COMMON_COMMANDS.test(cmd)) {
386
+ commands.push({
387
+ value: cmd,
388
+ line: i + 1,
389
+ column: (m.index ?? 0) + 2,
390
+ section: getSectionForLine(i + 1, sections)
391
+ });
392
+ }
393
+ }
394
+ }
395
+ return commands;
396
+ }
397
+
398
+ // src/utils/git.ts
399
+ import simpleGit from "simple-git";
400
+ var gitInstance = null;
401
+ var gitProjectRoot = null;
402
+ function getGit(projectRoot) {
403
+ if (!gitInstance || gitProjectRoot !== projectRoot) {
404
+ gitInstance = simpleGit(projectRoot);
405
+ gitProjectRoot = projectRoot;
406
+ }
407
+ return gitInstance;
408
+ }
409
+ function resetGit() {
410
+ gitInstance = null;
411
+ gitProjectRoot = null;
412
+ }
413
+ async function isGitRepo(projectRoot) {
414
+ try {
415
+ const git = simpleGit(projectRoot);
416
+ await git.revparse(["--is-inside-work-tree"]);
417
+ return true;
418
+ } catch {
419
+ return false;
420
+ }
421
+ }
422
+ async function getFileLastModified(projectRoot, filePath) {
423
+ try {
424
+ const git = getGit(projectRoot);
425
+ const log = await git.log({ file: filePath, maxCount: 1 });
426
+ if (log.latest?.date) {
427
+ return new Date(log.latest.date);
428
+ }
429
+ return null;
430
+ } catch {
431
+ return null;
432
+ }
433
+ }
434
+ async function getCommitsSince(projectRoot, filePath, since) {
435
+ try {
436
+ const git = getGit(projectRoot);
437
+ const log = await git.log({
438
+ file: filePath,
439
+ "--since": since.toISOString()
440
+ });
441
+ return log.total;
442
+ } catch {
443
+ return 0;
444
+ }
445
+ }
446
+ async function findRenames(projectRoot, filePath) {
447
+ try {
448
+ const git = getGit(projectRoot);
449
+ const result = await git.raw([
450
+ "log",
451
+ "--diff-filter=R",
452
+ "--find-renames",
453
+ "--name-status",
454
+ "--format=%H %ai",
455
+ "-10",
456
+ "--",
457
+ filePath
458
+ ]);
459
+ if (!result.trim()) return null;
460
+ const lines = result.trim().split("\n");
461
+ for (let i = 0; i < lines.length; i++) {
462
+ const line = lines[i];
463
+ if (line.startsWith("R")) {
464
+ const parts = line.split(" ");
465
+ if (parts.length >= 3) {
466
+ const hashLine = lines[i - 1] || "";
467
+ const hashMatch = hashLine.match(/^([a-f0-9]+)\s+(.+)/);
468
+ const commitHash = hashMatch?.[1]?.substring(0, 7) || "unknown";
469
+ const dateStr = hashMatch?.[2];
470
+ const daysAgo = dateStr ? Math.floor((Date.now() - new Date(dateStr).getTime()) / (1e3 * 60 * 60 * 24)) : 0;
471
+ return {
472
+ oldPath: parts[1],
473
+ newPath: parts[2],
474
+ commitHash,
475
+ daysAgo
476
+ };
477
+ }
478
+ }
479
+ }
480
+ return null;
481
+ } catch {
482
+ return null;
483
+ }
484
+ }
485
+
486
+ // src/core/checks/paths.ts
487
+ import * as path3 from "path";
488
+ import levenshteinPkg from "fast-levenshtein";
489
+ import { glob as glob2 } from "glob";
490
+ var levenshtein = levenshteinPkg.get;
491
+ var cachedProjectFiles = null;
492
+ function getProjectFiles(projectRoot) {
493
+ if (cachedProjectFiles?.root === projectRoot) return cachedProjectFiles.files;
494
+ const files = getAllProjectFiles(projectRoot);
495
+ cachedProjectFiles = { root: projectRoot, files };
496
+ return files;
497
+ }
498
+ function resetPathsCache() {
499
+ cachedProjectFiles = null;
500
+ }
501
+ async function checkPaths(file, projectRoot) {
502
+ const issues = [];
503
+ const projectFiles = getProjectFiles(projectRoot);
504
+ const contextDir = path3.dirname(file.filePath);
505
+ for (const ref of file.references.paths) {
506
+ const baseDir = ref.value.startsWith("./") || ref.value.startsWith("../") ? contextDir : projectRoot;
507
+ const resolvedPath = path3.resolve(baseDir, ref.value);
508
+ const normalizedRef = ref.value.replace(/\\/g, "/");
509
+ if (normalizedRef.includes("*")) {
510
+ const matches = await glob2(normalizedRef, { cwd: baseDir, nodir: false });
511
+ if (matches.length === 0) {
512
+ issues.push({
513
+ severity: "error",
514
+ check: "paths",
515
+ line: ref.line,
516
+ message: `${ref.value} matches no files`,
517
+ suggestion: "Verify the glob pattern is correct"
518
+ });
519
+ }
520
+ continue;
521
+ }
522
+ const isDir = normalizedRef.endsWith("/");
523
+ if (isDir) {
524
+ const dirPath = path3.resolve(baseDir, normalizedRef);
525
+ if (!isDirectory(dirPath)) {
526
+ issues.push({
527
+ severity: "error",
528
+ check: "paths",
529
+ line: ref.line,
530
+ message: `${ref.value} directory does not exist`
531
+ });
532
+ }
533
+ continue;
534
+ }
535
+ if (fileExists(resolvedPath) || isDirectory(resolvedPath)) {
536
+ continue;
537
+ }
538
+ let suggestion;
539
+ let detail;
540
+ let fixTarget;
541
+ const rename = await findRenames(projectRoot, ref.value);
542
+ if (rename) {
543
+ fixTarget = rename.newPath;
544
+ suggestion = `Did you mean ${rename.newPath}?`;
545
+ detail = `Renamed ${rename.daysAgo} days ago in commit ${rename.commitHash}`;
546
+ } else {
547
+ const match = findClosestMatch(normalizedRef, projectFiles);
548
+ if (match) {
549
+ fixTarget = match;
550
+ suggestion = `Did you mean ${match}?`;
551
+ }
552
+ }
553
+ issues.push({
554
+ severity: "error",
555
+ check: "paths",
556
+ line: ref.line,
557
+ message: `${ref.value} does not exist`,
558
+ suggestion,
559
+ detail,
560
+ fix: fixTarget ? { file: file.filePath, line: ref.line, oldText: ref.value, newText: fixTarget } : void 0
561
+ });
562
+ }
563
+ return issues;
564
+ }
565
+ function findClosestMatch(target, files) {
566
+ const targetNorm = target.replace(/\\/g, "/");
567
+ const targetBase = path3.basename(targetNorm);
568
+ let bestMatch = null;
569
+ let bestDistance = Infinity;
570
+ for (const file of files) {
571
+ const fileNorm = file.replace(/\\/g, "/");
572
+ if (path3.basename(fileNorm) === targetBase && fileNorm !== targetNorm) {
573
+ const dist = levenshtein(targetNorm, fileNorm);
574
+ if (dist < bestDistance) {
575
+ bestDistance = dist;
576
+ bestMatch = fileNorm;
577
+ }
578
+ }
579
+ }
580
+ if (!bestMatch) {
581
+ for (const file of files) {
582
+ const fileNorm = file.replace(/\\/g, "/");
583
+ const dist = levenshtein(targetNorm, fileNorm);
584
+ if (dist < bestDistance && dist <= Math.max(targetNorm.length * 0.4, 5)) {
585
+ bestDistance = dist;
586
+ bestMatch = fileNorm;
587
+ }
588
+ }
589
+ }
590
+ return bestMatch;
591
+ }
592
+
593
+ // src/core/checks/tokens.ts
594
+ var DEFAULT_THRESHOLDS = {
595
+ info: 1e3,
596
+ warning: 3e3,
597
+ error: 8e3,
598
+ aggregate: 5e3
599
+ };
600
+ var currentThresholds = DEFAULT_THRESHOLDS;
601
+ function setTokenThresholds(overrides) {
602
+ currentThresholds = { ...DEFAULT_THRESHOLDS, ...overrides };
603
+ }
604
+ function resetTokenThresholds() {
605
+ currentThresholds = DEFAULT_THRESHOLDS;
606
+ }
607
+ async function checkTokens(file, _projectRoot) {
608
+ const issues = [];
609
+ const tokens = file.totalTokens;
610
+ if (tokens >= currentThresholds.error) {
611
+ issues.push({
612
+ severity: "error",
613
+ check: "tokens",
614
+ line: 1,
615
+ message: `${tokens.toLocaleString()} tokens \u2014 consumes significant context window space`,
616
+ suggestion: "Consider splitting into focused sections or removing redundant content."
617
+ });
618
+ } else if (tokens >= currentThresholds.warning) {
619
+ issues.push({
620
+ severity: "warning",
621
+ check: "tokens",
622
+ line: 1,
623
+ message: `${tokens.toLocaleString()} tokens \u2014 large context file`,
624
+ suggestion: "Consider trimming \u2014 research shows diminishing returns past ~300 lines."
625
+ });
626
+ } else if (tokens >= currentThresholds.info) {
627
+ issues.push({
628
+ severity: "info",
629
+ check: "tokens",
630
+ line: 1,
631
+ message: `Uses ~${tokens.toLocaleString()} tokens per session`
632
+ });
633
+ }
634
+ return issues;
635
+ }
636
+ function checkAggregateTokens(files) {
637
+ const total = files.reduce((sum, f) => sum + f.tokens, 0);
638
+ if (total > currentThresholds.aggregate && files.length > 1) {
639
+ return {
640
+ severity: "warning",
641
+ check: "tokens",
642
+ line: 0,
643
+ message: `${files.length} context files consume ${total.toLocaleString()} tokens combined`,
644
+ suggestion: "Consider consolidating or trimming to reduce per-session context cost."
645
+ };
646
+ }
647
+ return null;
648
+ }
649
+
650
+ // src/version.ts
651
+ function loadVersion() {
652
+ if (true) return "0.4.0";
653
+ const fs5 = __require("fs");
654
+ const path7 = __require("path");
655
+ const pkgPath = path7.resolve(__dirname, "../package.json");
656
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
657
+ return pkg.version;
658
+ }
659
+ var VERSION = loadVersion();
660
+
661
+ // src/core/checks/commands.ts
662
+ import * as fs3 from "fs";
663
+ import * as path4 from "path";
664
+ var NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
665
+ var MAKE_PATTERN = /^make\s+(\S+)/;
666
+ var NPX_PATTERN = /^npx\s+(\S+)/;
667
+ async function checkCommands(file, projectRoot) {
668
+ const issues = [];
669
+ const pkgJson = loadPackageJson(projectRoot);
670
+ const makefile = loadMakefile(projectRoot);
671
+ for (const ref of file.references.commands) {
672
+ const cmd = ref.value;
673
+ const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
674
+ if (scriptMatch && pkgJson) {
675
+ const scriptName = scriptMatch[1];
676
+ if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
677
+ const available = Object.keys(pkgJson.scripts).join(", ");
678
+ issues.push({
679
+ severity: "error",
680
+ check: "commands",
681
+ line: ref.line,
682
+ message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`,
683
+ suggestion: available ? `Available scripts: ${available}` : void 0
684
+ });
685
+ }
686
+ continue;
687
+ }
688
+ const shorthandMatch = cmd.match(
689
+ /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/
690
+ );
691
+ if (shorthandMatch && pkgJson) {
692
+ const scriptName = shorthandMatch[2];
693
+ if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
694
+ issues.push({
695
+ severity: "error",
696
+ check: "commands",
697
+ line: ref.line,
698
+ message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`
699
+ });
700
+ }
701
+ continue;
702
+ }
703
+ const npxMatch = cmd.match(NPX_PATTERN);
704
+ if (npxMatch && pkgJson) {
705
+ const pkgName = npxMatch[1];
706
+ if (pkgName.startsWith("-")) continue;
707
+ const allDeps = {
708
+ ...pkgJson.dependencies,
709
+ ...pkgJson.devDependencies
710
+ };
711
+ if (!(pkgName in allDeps)) {
712
+ const binPath = path4.join(projectRoot, "node_modules", ".bin", pkgName);
713
+ try {
714
+ fs3.accessSync(binPath);
715
+ } catch {
716
+ issues.push({
717
+ severity: "warning",
718
+ check: "commands",
719
+ line: ref.line,
720
+ message: `"${cmd}" \u2014 "${pkgName}" not found in dependencies`,
721
+ suggestion: "If this is a global tool, consider adding it to devDependencies for reproducibility"
722
+ });
723
+ }
724
+ }
725
+ continue;
726
+ }
727
+ const makeMatch = cmd.match(MAKE_PATTERN);
728
+ if (makeMatch) {
729
+ const target = makeMatch[1];
730
+ if (makefile && !hasMakeTarget(makefile, target)) {
731
+ issues.push({
732
+ severity: "error",
733
+ check: "commands",
734
+ line: ref.line,
735
+ message: `"${cmd}" \u2014 target "${target}" not found in Makefile`
736
+ });
737
+ } else if (!makefile) {
738
+ issues.push({
739
+ severity: "error",
740
+ check: "commands",
741
+ line: ref.line,
742
+ message: `"${cmd}" \u2014 no Makefile found in project`
743
+ });
744
+ }
745
+ continue;
746
+ }
747
+ const toolMatch = cmd.match(/^(vitest|jest|pytest|mocha|eslint|prettier|tsc)\b/);
748
+ if (toolMatch && pkgJson) {
749
+ const tool = toolMatch[1];
750
+ const allDeps = {
751
+ ...pkgJson.dependencies,
752
+ ...pkgJson.devDependencies
753
+ };
754
+ if (!(tool in allDeps)) {
755
+ const binPath = path4.join(projectRoot, "node_modules", ".bin", tool);
756
+ try {
757
+ fs3.accessSync(binPath);
758
+ } catch {
759
+ issues.push({
760
+ severity: "warning",
761
+ check: "commands",
762
+ line: ref.line,
763
+ message: `"${cmd}" \u2014 "${tool}" not found in dependencies or node_modules/.bin`
764
+ });
765
+ }
766
+ }
767
+ }
768
+ }
769
+ return issues;
770
+ }
771
+ function loadMakefile(projectRoot) {
772
+ try {
773
+ return fs3.readFileSync(path4.join(projectRoot, "Makefile"), "utf-8");
774
+ } catch {
775
+ return null;
776
+ }
777
+ }
778
+ function hasMakeTarget(makefile, target) {
779
+ const pattern = new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:`, "m");
780
+ return pattern.test(makefile);
781
+ }
782
+
783
+ // src/core/checks/staleness.ts
784
+ import * as path5 from "path";
785
+ var WARNING_DAYS = 30;
786
+ var INFO_DAYS = 14;
787
+ async function checkStaleness(file, projectRoot) {
788
+ const issues = [];
789
+ if (!await isGitRepo(projectRoot)) {
790
+ return issues;
791
+ }
792
+ const relativePath = path5.relative(projectRoot, file.filePath).replace(/\\/g, "/");
793
+ const lastModified = await getFileLastModified(projectRoot, relativePath);
794
+ if (!lastModified || isNaN(lastModified.getTime())) {
795
+ return issues;
796
+ }
797
+ const daysSinceUpdate = Math.floor((Date.now() - lastModified.getTime()) / (1e3 * 60 * 60 * 24));
798
+ if (daysSinceUpdate < INFO_DAYS) {
799
+ return issues;
800
+ }
801
+ const referencedPaths = /* @__PURE__ */ new Set();
802
+ for (const ref of file.references.paths) {
803
+ const parts = ref.value.split("/");
804
+ if (parts.length > 1) {
805
+ referencedPaths.add(parts.slice(0, -1).join("/"));
806
+ }
807
+ referencedPaths.add(ref.value);
808
+ }
809
+ let totalCommits = 0;
810
+ let mostActiveRef = "";
811
+ let mostActiveCommits = 0;
812
+ for (const refPath of referencedPaths) {
813
+ const commits = await getCommitsSince(projectRoot, refPath, lastModified);
814
+ totalCommits += commits;
815
+ if (commits > mostActiveCommits) {
816
+ mostActiveCommits = commits;
817
+ mostActiveRef = refPath;
818
+ }
819
+ }
820
+ if (totalCommits === 0) {
821
+ return issues;
822
+ }
823
+ const severity = daysSinceUpdate >= WARNING_DAYS ? "warning" : "info";
824
+ issues.push({
825
+ severity,
826
+ check: "staleness",
827
+ line: 1,
828
+ message: `Last updated ${daysSinceUpdate} days ago. ${mostActiveRef} has ${mostActiveCommits} commits since.`,
829
+ suggestion: "Review and update this context file to reflect recent changes.",
830
+ detail: `${totalCommits} total commits to referenced paths since last update.`
831
+ });
832
+ return issues;
833
+ }
834
+
835
+ // src/core/checks/redundancy.ts
836
+ import * as path6 from "path";
837
+ var PACKAGE_TECH_MAP = {
838
+ react: ["React", "react"],
839
+ "react-dom": ["React DOM", "ReactDOM"],
840
+ next: ["Next.js", "NextJS", "next.js"],
841
+ express: ["Express", "express.js"],
842
+ fastify: ["Fastify"],
843
+ typescript: ["TypeScript"],
844
+ vue: ["Vue", "Vue.js", "vue.js"],
845
+ angular: ["Angular"],
846
+ svelte: ["Svelte", "SvelteKit"],
847
+ tailwindcss: ["Tailwind", "TailwindCSS", "tailwind"],
848
+ prisma: ["Prisma"],
849
+ drizzle: ["Drizzle"],
850
+ "drizzle-orm": ["Drizzle"],
851
+ jest: ["Jest"],
852
+ vitest: ["Vitest"],
853
+ mocha: ["Mocha"],
854
+ eslint: ["ESLint"],
855
+ prettier: ["Prettier"],
856
+ webpack: ["Webpack"],
857
+ vite: ["Vite"],
858
+ esbuild: ["esbuild"],
859
+ tsup: ["tsup"],
860
+ rollup: ["Rollup"],
861
+ graphql: ["GraphQL"],
862
+ mongoose: ["Mongoose"],
863
+ sequelize: ["Sequelize"],
864
+ "socket.io": ["Socket.IO", "socket.io"],
865
+ redis: ["Redis"],
866
+ ioredis: ["Redis"],
867
+ postgres: ["PostgreSQL", "Postgres"],
868
+ pg: ["PostgreSQL", "Postgres"],
869
+ mysql2: ["MySQL"],
870
+ sqlite3: ["SQLite"],
871
+ "better-sqlite3": ["SQLite"],
872
+ zod: ["Zod"],
873
+ joi: ["Joi"],
874
+ axios: ["Axios"],
875
+ lodash: ["Lodash", "lodash"],
876
+ underscore: ["Underscore"],
877
+ moment: ["Moment", "moment.js"],
878
+ dayjs: ["Day.js", "dayjs"],
879
+ "date-fns": ["date-fns"],
880
+ docker: ["Docker"],
881
+ kubernetes: ["Kubernetes", "K8s"],
882
+ terraform: ["Terraform"],
883
+ storybook: ["Storybook"],
884
+ playwright: ["Playwright"],
885
+ cypress: ["Cypress"],
886
+ puppeteer: ["Puppeteer"]
887
+ };
888
+ function compilePatterns(allDeps) {
889
+ const compiled = [];
890
+ for (const [pkg, mentions] of Object.entries(PACKAGE_TECH_MAP)) {
891
+ if (!allDeps.has(pkg)) continue;
892
+ for (const mention of mentions) {
893
+ const escaped = escapeRegex(mention);
894
+ compiled.push({
895
+ pkg,
896
+ mention,
897
+ patterns: [
898
+ new RegExp(`\\b(?:use|using|built with|powered by|written in)\\s+${escaped}\\b`, "i"),
899
+ new RegExp(`\\bwe\\s+use\\s+${escaped}\\b`, "i"),
900
+ new RegExp(`\\b${escaped}\\s+(?:project|app|application|codebase)\\b`, "i"),
901
+ new RegExp(`\\bThis is a\\s+${escaped}\\b`, "i")
902
+ ]
903
+ });
904
+ }
905
+ }
906
+ return compiled;
907
+ }
908
+ async function checkRedundancy(file, projectRoot) {
909
+ const issues = [];
910
+ const pkgJson = loadPackageJson(projectRoot);
911
+ if (pkgJson) {
912
+ const allDeps = /* @__PURE__ */ new Set([
913
+ ...Object.keys(pkgJson.dependencies || {}),
914
+ ...Object.keys(pkgJson.devDependencies || {})
915
+ ]);
916
+ const compiledPatterns = compilePatterns(allDeps);
917
+ const lines2 = file.content.split("\n");
918
+ for (let i = 0; i < lines2.length; i++) {
919
+ const line = lines2[i];
920
+ for (const { pkg, mention, patterns } of compiledPatterns) {
921
+ let matched = false;
922
+ for (const pattern of patterns) {
923
+ if (pattern.test(line)) {
924
+ matched = true;
925
+ break;
926
+ }
927
+ }
928
+ if (matched) {
929
+ const wastedTokens = countTokens(line.trim());
930
+ issues.push({
931
+ severity: "info",
932
+ check: "redundancy",
933
+ line: i + 1,
934
+ message: `"${mention}" is in package.json ${pkgJson.dependencies?.[pkg] ? "dependencies" : "devDependencies"} \u2014 agent can infer this`,
935
+ suggestion: `~${wastedTokens} tokens could be saved`
936
+ });
937
+ }
938
+ }
939
+ }
940
+ }
941
+ const lines = file.content.split("\n");
942
+ for (let i = 0; i < lines.length; i++) {
943
+ const line = lines[i];
944
+ const dirMatch = line.match(
945
+ /(?:are|go|live|found|located|stored)\s+(?:in|at|under)\s+[`"]?(\S+\/)[`"]?/i
946
+ );
947
+ if (dirMatch) {
948
+ const dir = dirMatch[1].replace(/[`"]/g, "");
949
+ const fullPath = path6.resolve(projectRoot, dir);
950
+ if (isDirectory(fullPath)) {
951
+ issues.push({
952
+ severity: "info",
953
+ check: "redundancy",
954
+ line: i + 1,
955
+ message: `Directory "${dir}" exists and is discoverable \u2014 agent can find this by listing files`,
956
+ suggestion: "Only keep if there is non-obvious context about this directory"
957
+ });
958
+ }
959
+ }
960
+ }
961
+ return issues;
962
+ }
963
+ function checkDuplicateContent(files) {
964
+ const issues = [];
965
+ for (let i = 0; i < files.length; i++) {
966
+ for (let j = i + 1; j < files.length; j++) {
967
+ const overlap = calculateLineOverlap(files[i].content, files[j].content);
968
+ if (overlap > 0.6) {
969
+ issues.push({
970
+ severity: "warning",
971
+ check: "redundancy",
972
+ line: 1,
973
+ message: `${files[i].relativePath} and ${files[j].relativePath} have ${Math.round(overlap * 100)}% content overlap`,
974
+ suggestion: "Consider consolidating into a single context file"
975
+ });
976
+ }
977
+ }
978
+ }
979
+ return issues;
980
+ }
981
+ function calculateLineOverlap(contentA, contentB) {
982
+ const linesA = new Set(
983
+ contentA.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
984
+ );
985
+ const linesB = new Set(
986
+ contentB.split("\n").map((l) => l.trim()).filter((l) => l.length > 10)
987
+ );
988
+ if (linesA.size === 0 || linesB.size === 0) return 0;
989
+ let overlap = 0;
990
+ for (const line of linesA) {
991
+ if (linesB.has(line)) overlap++;
992
+ }
993
+ return overlap / Math.min(linesA.size, linesB.size);
994
+ }
995
+ function escapeRegex(str) {
996
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
997
+ }
998
+
999
+ // src/core/checks/contradictions.ts
1000
+ var DIRECTIVE_CATEGORIES = [
1001
+ {
1002
+ name: "testing framework",
1003
+ options: [
1004
+ {
1005
+ label: "Jest",
1006
+ patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest.*with\s+jest\b/i]
1007
+ },
1008
+ {
1009
+ label: "Vitest",
1010
+ patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest.*with\s+vitest\b/i]
1011
+ },
1012
+ {
1013
+ label: "Mocha",
1014
+ patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest.*with\s+mocha\b/i]
1015
+ },
1016
+ {
1017
+ label: "pytest",
1018
+ patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest.*with\s+pytest\b/i]
1019
+ },
1020
+ {
1021
+ label: "Playwright",
1022
+ patterns: [/\buse\s+playwright\b/i, /\bplaywright\s+for\s+(?:e2e|test)/i]
1023
+ },
1024
+ { label: "Cypress", patterns: [/\buse\s+cypress\b/i, /\bcypress\s+for\s+(?:e2e|test)/i] }
1025
+ ]
1026
+ },
1027
+ {
1028
+ name: "package manager",
1029
+ options: [
1030
+ {
1031
+ label: "npm",
1032
+ patterns: [
1033
+ /\buse\s+npm\b/i,
1034
+ /\bnpm\s+as\s+(?:the\s+)?package\s+manager/i,
1035
+ /\balways\s+use\s+npm\b/i
1036
+ ]
1037
+ },
1038
+ {
1039
+ label: "pnpm",
1040
+ patterns: [
1041
+ /\buse\s+pnpm\b/i,
1042
+ /\bpnpm\s+as\s+(?:the\s+)?package\s+manager/i,
1043
+ /\balways\s+use\s+pnpm\b/i
1044
+ ]
1045
+ },
1046
+ {
1047
+ label: "yarn",
1048
+ patterns: [
1049
+ /\buse\s+yarn\b/i,
1050
+ /\byarn\s+as\s+(?:the\s+)?package\s+manager/i,
1051
+ /\balways\s+use\s+yarn\b/i
1052
+ ]
1053
+ },
1054
+ {
1055
+ label: "bun",
1056
+ patterns: [
1057
+ /\buse\s+bun\b/i,
1058
+ /\bbun\s+as\s+(?:the\s+)?package\s+manager/i,
1059
+ /\balways\s+use\s+bun\b/i
1060
+ ]
1061
+ }
1062
+ ]
1063
+ },
1064
+ {
1065
+ name: "indentation style",
1066
+ options: [
1067
+ {
1068
+ label: "tabs",
1069
+ patterns: [/\buse\s+tabs\b/i, /\btab\s+indentation\b/i, /\bindent\s+with\s+tabs\b/i]
1070
+ },
1071
+ {
1072
+ label: "2 spaces",
1073
+ patterns: [
1074
+ /\b2[\s-]?space\s+indent/i,
1075
+ /\bindent\s+with\s+2\s+spaces/i,
1076
+ /\b2[\s-]?space\s+tabs?\b/i
1077
+ ]
1078
+ },
1079
+ {
1080
+ label: "4 spaces",
1081
+ patterns: [
1082
+ /\b4[\s-]?space\s+indent/i,
1083
+ /\bindent\s+with\s+4\s+spaces/i,
1084
+ /\b4[\s-]?space\s+tabs?\b/i
1085
+ ]
1086
+ }
1087
+ ]
1088
+ },
1089
+ {
1090
+ name: "semicolons",
1091
+ options: [
1092
+ {
1093
+ label: "semicolons",
1094
+ patterns: [
1095
+ /\buse\s+semicolons\b/i,
1096
+ /\balways\s+(?:use\s+)?semicolons\b/i,
1097
+ /\bsemicolons:\s*(?:true|yes)\b/i
1098
+ ]
1099
+ },
1100
+ {
1101
+ label: "no semicolons",
1102
+ patterns: [
1103
+ /\bno\s+semicolons\b/i,
1104
+ /\bavoid\s+semicolons\b/i,
1105
+ /\bomit\s+semicolons\b/i,
1106
+ /\bsemicolons:\s*(?:false|no)\b/i
1107
+ ]
1108
+ }
1109
+ ]
1110
+ },
1111
+ {
1112
+ name: "quote style",
1113
+ options: [
1114
+ {
1115
+ label: "single quotes",
1116
+ patterns: [
1117
+ /\bsingle\s+quotes?\b/i,
1118
+ /\buse\s+(?:single\s+)?['']single['']?\s+quotes?\b/i,
1119
+ /\bprefer\s+single\s+quotes?\b/i
1120
+ ]
1121
+ },
1122
+ {
1123
+ label: "double quotes",
1124
+ patterns: [
1125
+ /\bdouble\s+quotes?\b/i,
1126
+ /\buse\s+(?:double\s+)?[""]double[""]?\s+quotes?\b/i,
1127
+ /\bprefer\s+double\s+quotes?\b/i
1128
+ ]
1129
+ }
1130
+ ]
1131
+ },
1132
+ {
1133
+ name: "naming convention",
1134
+ options: [
1135
+ {
1136
+ label: "camelCase",
1137
+ patterns: [/\bcamelCase\b/, /\bcamel[\s-]?case\s+(?:for|naming|convention)/i]
1138
+ },
1139
+ {
1140
+ label: "snake_case",
1141
+ patterns: [/\bsnake_case\b/, /\bsnake[\s-]?case\s+(?:for|naming|convention)/i]
1142
+ },
1143
+ {
1144
+ label: "PascalCase",
1145
+ patterns: [/\bPascalCase\b/, /\bpascal[\s-]?case\s+(?:for|naming|convention)/i]
1146
+ },
1147
+ {
1148
+ label: "kebab-case",
1149
+ patterns: [/\bkebab-case\b/, /\bkebab[\s-]?case\s+(?:for|naming|convention)/i]
1150
+ }
1151
+ ]
1152
+ },
1153
+ {
1154
+ name: "CSS approach",
1155
+ options: [
1156
+ { label: "Tailwind", patterns: [/\buse\s+tailwind/i, /\btailwind\s+for\s+styl/i] },
1157
+ {
1158
+ label: "CSS Modules",
1159
+ patterns: [/\buse\s+css\s+modules\b/i, /\bcss\s+modules\s+for\s+styl/i]
1160
+ },
1161
+ {
1162
+ label: "styled-components",
1163
+ patterns: [/\buse\s+styled[\s-]?components\b/i, /\bstyled[\s-]?components\s+for\s+styl/i]
1164
+ },
1165
+ { label: "CSS-in-JS", patterns: [/\buse\s+css[\s-]?in[\s-]?js\b/i] }
1166
+ ]
1167
+ },
1168
+ {
1169
+ name: "state management",
1170
+ options: [
1171
+ { label: "Redux", patterns: [/\buse\s+redux\b/i, /\bredux\s+for\s+state/i] },
1172
+ { label: "Zustand", patterns: [/\buse\s+zustand\b/i, /\bzustand\s+for\s+state/i] },
1173
+ { label: "MobX", patterns: [/\buse\s+mobx\b/i, /\bmobx\s+for\s+state/i] },
1174
+ { label: "Jotai", patterns: [/\buse\s+jotai\b/i, /\bjotai\s+for\s+state/i] },
1175
+ { label: "Recoil", patterns: [/\buse\s+recoil\b/i, /\brecoil\s+for\s+state/i] }
1176
+ ]
1177
+ }
1178
+ ];
1179
+ function detectDirectives(file) {
1180
+ const directives = [];
1181
+ const lines = file.content.split("\n");
1182
+ for (let i = 0; i < lines.length; i++) {
1183
+ const line = lines[i];
1184
+ for (const category of DIRECTIVE_CATEGORIES) {
1185
+ for (const option of category.options) {
1186
+ for (const pattern of option.patterns) {
1187
+ if (pattern.test(line)) {
1188
+ directives.push({
1189
+ file: file.relativePath,
1190
+ category: category.name,
1191
+ label: option.label,
1192
+ line: i + 1,
1193
+ text: line.trim()
1194
+ });
1195
+ break;
1196
+ }
1197
+ }
1198
+ }
1199
+ }
1200
+ }
1201
+ return directives;
1202
+ }
1203
+ function checkContradictions(files) {
1204
+ if (files.length < 2) return [];
1205
+ const issues = [];
1206
+ const allDirectives = [];
1207
+ for (const file of files) {
1208
+ allDirectives.push(...detectDirectives(file));
1209
+ }
1210
+ const byCategory = /* @__PURE__ */ new Map();
1211
+ for (const d of allDirectives) {
1212
+ const existing = byCategory.get(d.category) || [];
1213
+ existing.push(d);
1214
+ byCategory.set(d.category, existing);
1215
+ }
1216
+ for (const [category, directives] of byCategory) {
1217
+ const byFile = /* @__PURE__ */ new Map();
1218
+ for (const d of directives) {
1219
+ const existing = byFile.get(d.file) || [];
1220
+ existing.push(d);
1221
+ byFile.set(d.file, existing);
1222
+ }
1223
+ const labels = new Set(directives.map((d) => d.label));
1224
+ if (labels.size <= 1) continue;
1225
+ const fileLabels = /* @__PURE__ */ new Map();
1226
+ for (const d of directives) {
1227
+ const existing = fileLabels.get(d.file) || /* @__PURE__ */ new Set();
1228
+ existing.add(d.label);
1229
+ fileLabels.set(d.file, existing);
1230
+ }
1231
+ const fileEntries = [...fileLabels.entries()];
1232
+ for (let i = 0; i < fileEntries.length; i++) {
1233
+ for (let j = i + 1; j < fileEntries.length; j++) {
1234
+ const [fileA, labelsA] = fileEntries[i];
1235
+ const [fileB, labelsB] = fileEntries[j];
1236
+ for (const labelA of labelsA) {
1237
+ for (const labelB of labelsB) {
1238
+ if (labelA !== labelB) {
1239
+ const directiveA = directives.find((d) => d.file === fileA && d.label === labelA);
1240
+ const directiveB = directives.find((d) => d.file === fileB && d.label === labelB);
1241
+ issues.push({
1242
+ severity: "warning",
1243
+ check: "contradictions",
1244
+ line: directiveA.line,
1245
+ message: `${category} conflict: "${directiveA.label}" in ${fileA} vs "${directiveB.label}" in ${fileB}`,
1246
+ suggestion: `Align on one ${category} across all context files`,
1247
+ detail: `${fileA}:${directiveA.line} says "${directiveA.text}" but ${fileB}:${directiveB.line} says "${directiveB.text}"`
1248
+ });
1249
+ }
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+ return issues;
1256
+ }
1257
+
1258
+ // src/core/checks/frontmatter.ts
1259
+ function parseFrontmatter(content) {
1260
+ const lines = content.split("\n");
1261
+ if (lines[0]?.trim() !== "---") {
1262
+ return { found: false, fields: {}, endLine: 0 };
1263
+ }
1264
+ const fields = {};
1265
+ let endLine = 0;
1266
+ for (let i = 1; i < lines.length; i++) {
1267
+ const line = lines[i].trim();
1268
+ if (line === "---") {
1269
+ endLine = i + 1;
1270
+ break;
1271
+ }
1272
+ const match = line.match(/^(\w+)\s*:\s*(.*)$/);
1273
+ if (match) {
1274
+ fields[match[1]] = match[2].trim();
1275
+ }
1276
+ }
1277
+ if (endLine === 0) {
1278
+ return { found: true, fields, endLine: lines.length };
1279
+ }
1280
+ return { found: true, fields, endLine };
1281
+ }
1282
+ function isCursorMdc(file) {
1283
+ return file.relativePath.endsWith(".mdc");
1284
+ }
1285
+ function isCopilotInstructions(file) {
1286
+ return file.relativePath.includes(".github/instructions/") && file.relativePath.endsWith(".md");
1287
+ }
1288
+ function isWindsurfRule(file) {
1289
+ return file.relativePath.includes(".windsurf/rules/") && file.relativePath.endsWith(".md");
1290
+ }
1291
+ var VALID_WINDSURF_TRIGGERS = ["always_on", "glob", "manual", "model"];
1292
+ async function checkFrontmatter(file, _projectRoot) {
1293
+ const issues = [];
1294
+ if (isCursorMdc(file)) {
1295
+ issues.push(...validateCursorMdc(file));
1296
+ } else if (isCopilotInstructions(file)) {
1297
+ issues.push(...validateCopilotInstructions(file));
1298
+ } else if (isWindsurfRule(file)) {
1299
+ issues.push(...validateWindsurfRule(file));
1300
+ }
1301
+ return issues;
1302
+ }
1303
+ function validateCursorMdc(file) {
1304
+ const issues = [];
1305
+ const fm = parseFrontmatter(file.content);
1306
+ if (!fm.found) {
1307
+ issues.push({
1308
+ severity: "warning",
1309
+ check: "frontmatter",
1310
+ line: 1,
1311
+ message: "Cursor .mdc file is missing frontmatter",
1312
+ suggestion: "Add YAML frontmatter with description, globs, and alwaysApply fields"
1313
+ });
1314
+ return issues;
1315
+ }
1316
+ if (!fm.fields["description"]) {
1317
+ issues.push({
1318
+ severity: "warning",
1319
+ check: "frontmatter",
1320
+ line: 1,
1321
+ message: 'Missing "description" field in Cursor .mdc frontmatter',
1322
+ suggestion: "Add a description so Cursor knows when to apply this rule"
1323
+ });
1324
+ }
1325
+ if (!("alwaysApply" in fm.fields) && !("globs" in fm.fields)) {
1326
+ issues.push({
1327
+ severity: "info",
1328
+ check: "frontmatter",
1329
+ line: 1,
1330
+ message: 'No "alwaysApply" or "globs" field \u2014 rule may not be applied automatically',
1331
+ suggestion: "Set alwaysApply: true or specify globs for targeted activation"
1332
+ });
1333
+ }
1334
+ if ("alwaysApply" in fm.fields) {
1335
+ const val = fm.fields["alwaysApply"].toLowerCase();
1336
+ if (!["true", "false"].includes(val)) {
1337
+ issues.push({
1338
+ severity: "error",
1339
+ check: "frontmatter",
1340
+ line: 1,
1341
+ message: `Invalid alwaysApply value: "${fm.fields["alwaysApply"]}"`,
1342
+ suggestion: "alwaysApply must be true or false"
1343
+ });
1344
+ }
1345
+ }
1346
+ if ("globs" in fm.fields) {
1347
+ const val = fm.fields["globs"];
1348
+ if (val && !val.startsWith("[") && !val.startsWith('"') && !val.includes("*") && !val.includes("/")) {
1349
+ issues.push({
1350
+ severity: "warning",
1351
+ check: "frontmatter",
1352
+ line: 1,
1353
+ message: `Possibly invalid globs value: "${val}"`,
1354
+ suggestion: 'globs should be a glob pattern like "src/**/*.ts" or an array like ["*.ts", "*.tsx"]'
1355
+ });
1356
+ }
1357
+ }
1358
+ return issues;
1359
+ }
1360
+ function validateCopilotInstructions(file) {
1361
+ const issues = [];
1362
+ const fm = parseFrontmatter(file.content);
1363
+ if (!fm.found) {
1364
+ issues.push({
1365
+ severity: "info",
1366
+ check: "frontmatter",
1367
+ line: 1,
1368
+ message: "Copilot instructions file has no frontmatter",
1369
+ suggestion: "Add applyTo frontmatter to target specific file patterns"
1370
+ });
1371
+ return issues;
1372
+ }
1373
+ if (!fm.fields["applyTo"]) {
1374
+ issues.push({
1375
+ severity: "warning",
1376
+ check: "frontmatter",
1377
+ line: 1,
1378
+ message: 'Missing "applyTo" field in Copilot instructions frontmatter',
1379
+ suggestion: 'Add applyTo to specify which files this instruction applies to (e.g., applyTo: "**/*.ts")'
1380
+ });
1381
+ }
1382
+ return issues;
1383
+ }
1384
+ function validateWindsurfRule(file) {
1385
+ const issues = [];
1386
+ const fm = parseFrontmatter(file.content);
1387
+ if (!fm.found) {
1388
+ issues.push({
1389
+ severity: "info",
1390
+ check: "frontmatter",
1391
+ line: 1,
1392
+ message: "Windsurf rule file has no frontmatter",
1393
+ suggestion: "Add YAML frontmatter with a trigger field (always_on, glob, manual, model)"
1394
+ });
1395
+ return issues;
1396
+ }
1397
+ if (!fm.fields["trigger"]) {
1398
+ issues.push({
1399
+ severity: "warning",
1400
+ check: "frontmatter",
1401
+ line: 1,
1402
+ message: 'Missing "trigger" field in Windsurf rule frontmatter',
1403
+ suggestion: `Set trigger to one of: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
1404
+ });
1405
+ } else {
1406
+ const trigger = fm.fields["trigger"].replace(/['"]/g, "");
1407
+ if (!VALID_WINDSURF_TRIGGERS.includes(trigger)) {
1408
+ issues.push({
1409
+ severity: "error",
1410
+ check: "frontmatter",
1411
+ line: 1,
1412
+ message: `Invalid trigger value: "${trigger}"`,
1413
+ suggestion: `Valid triggers: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
1414
+ });
1415
+ }
1416
+ }
1417
+ return issues;
1418
+ }
1419
+
1420
+ // src/core/audit.ts
1421
+ var ALL_CHECKS = [
1422
+ "paths",
1423
+ "commands",
1424
+ "staleness",
1425
+ "tokens",
1426
+ "redundancy",
1427
+ "contradictions",
1428
+ "frontmatter"
1429
+ ];
1430
+ async function runAudit(projectRoot, activeChecks, options = {}) {
1431
+ const discovered = await scanForContextFiles(projectRoot, {
1432
+ depth: options.depth,
1433
+ extraPatterns: options.extraPatterns
1434
+ });
1435
+ const parsed = discovered.map((f) => parseContextFile(f));
1436
+ const fileResults = [];
1437
+ for (const file of parsed) {
1438
+ const checkPromises = [];
1439
+ if (activeChecks.includes("paths")) checkPromises.push(checkPaths(file, projectRoot));
1440
+ if (activeChecks.includes("commands")) checkPromises.push(checkCommands(file, projectRoot));
1441
+ if (activeChecks.includes("staleness")) checkPromises.push(checkStaleness(file, projectRoot));
1442
+ if (activeChecks.includes("tokens")) checkPromises.push(checkTokens(file, projectRoot));
1443
+ if (activeChecks.includes("redundancy")) checkPromises.push(checkRedundancy(file, projectRoot));
1444
+ if (activeChecks.includes("frontmatter"))
1445
+ checkPromises.push(checkFrontmatter(file, projectRoot));
1446
+ const results = await Promise.all(checkPromises);
1447
+ const issues = results.flat();
1448
+ fileResults.push({
1449
+ path: file.relativePath,
1450
+ isSymlink: file.isSymlink,
1451
+ symlinkTarget: file.symlinkTarget,
1452
+ tokens: file.totalTokens,
1453
+ lines: file.totalLines,
1454
+ issues
1455
+ });
1456
+ }
1457
+ if (activeChecks.includes("tokens")) {
1458
+ const aggIssue = checkAggregateTokens(
1459
+ fileResults.map((f) => ({ path: f.path, tokens: f.tokens }))
1460
+ );
1461
+ if (aggIssue && fileResults.length > 0) fileResults[0].issues.push(aggIssue);
1462
+ }
1463
+ if (activeChecks.includes("redundancy")) {
1464
+ const dupIssues = checkDuplicateContent(parsed);
1465
+ if (dupIssues.length > 0 && fileResults.length > 0) fileResults[0].issues.push(...dupIssues);
1466
+ }
1467
+ if (activeChecks.includes("contradictions")) {
1468
+ const contradictionIssues = checkContradictions(parsed);
1469
+ if (contradictionIssues.length > 0 && fileResults.length > 0)
1470
+ fileResults[0].issues.push(...contradictionIssues);
1471
+ }
1472
+ let estimatedWaste = 0;
1473
+ for (const fr of fileResults) {
1474
+ for (const issue of fr.issues) {
1475
+ if (issue.check === "redundancy" && issue.suggestion) {
1476
+ const tokenMatch = issue.suggestion.match(/~(\d+)\s+tokens/);
1477
+ if (tokenMatch) estimatedWaste += parseInt(tokenMatch[1], 10);
1478
+ }
1479
+ }
1480
+ }
1481
+ return {
1482
+ version: VERSION,
1483
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
1484
+ projectRoot,
1485
+ files: fileResults,
1486
+ summary: {
1487
+ errors: fileResults.reduce(
1488
+ (sum, f) => sum + f.issues.filter((i) => i.severity === "error").length,
1489
+ 0
1490
+ ),
1491
+ warnings: fileResults.reduce(
1492
+ (sum, f) => sum + f.issues.filter((i) => i.severity === "warning").length,
1493
+ 0
1494
+ ),
1495
+ info: fileResults.reduce(
1496
+ (sum, f) => sum + f.issues.filter((i) => i.severity === "info").length,
1497
+ 0
1498
+ ),
1499
+ totalTokens: fileResults.reduce((sum, f) => sum + f.tokens, 0),
1500
+ estimatedWaste
1501
+ }
1502
+ };
1503
+ }
1504
+
1505
+ // src/core/fixer.ts
1506
+ import * as fs4 from "fs";
1507
+ import chalk from "chalk";
1508
+ function applyFixes(result) {
1509
+ const fixesByFile = /* @__PURE__ */ new Map();
1510
+ for (const file of result.files) {
1511
+ for (const issue of file.issues) {
1512
+ if (issue.fix) {
1513
+ const existing = fixesByFile.get(issue.fix.file) || [];
1514
+ existing.push(issue.fix);
1515
+ fixesByFile.set(issue.fix.file, existing);
1516
+ }
1517
+ }
1518
+ }
1519
+ let totalFixes = 0;
1520
+ const filesModified = [];
1521
+ for (const [filePath, fixes] of fixesByFile) {
1522
+ const content = fs4.readFileSync(filePath, "utf-8");
1523
+ const lines = content.split("\n");
1524
+ let modified = false;
1525
+ const sortedFixes = [...fixes].sort((a, b) => b.line - a.line);
1526
+ for (const fix of sortedFixes) {
1527
+ const lineIdx = fix.line - 1;
1528
+ if (lineIdx < 0 || lineIdx >= lines.length) continue;
1529
+ const line = lines[lineIdx];
1530
+ if (line.includes(fix.oldText)) {
1531
+ lines[lineIdx] = line.replace(fix.oldText, fix.newText);
1532
+ modified = true;
1533
+ totalFixes++;
1534
+ console.log(
1535
+ chalk.green(" Fixed") + ` Line ${fix.line}: ${chalk.dim(fix.oldText)} ${chalk.dim("\u2192")} ${fix.newText}`
1536
+ );
1537
+ }
1538
+ }
1539
+ if (modified) {
1540
+ fs4.writeFileSync(filePath, lines.join("\n"), "utf-8");
1541
+ filesModified.push(filePath);
1542
+ }
1543
+ }
1544
+ return { totalFixes, filesModified };
1545
+ }
1546
+
1547
+ export {
1548
+ fileExists,
1549
+ isDirectory,
1550
+ scanForContextFiles,
1551
+ freeEncoder,
1552
+ parseContextFile,
1553
+ resetGit,
1554
+ findRenames,
1555
+ resetPathsCache,
1556
+ setTokenThresholds,
1557
+ resetTokenThresholds,
1558
+ VERSION,
1559
+ ALL_CHECKS,
1560
+ runAudit,
1561
+ applyFixes
1562
+ };