@yawlabs/ctxlint 0.2.2 → 0.3.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.
@@ -93,43 +93,76 @@ function getAllProjectFiles(projectRoot) {
93
93
 
94
94
  // src/core/scanner.ts
95
95
  var CONTEXT_FILE_PATTERNS = [
96
+ // Claude Code
96
97
  "CLAUDE.md",
97
98
  "CLAUDE.local.md",
99
+ ".claude/rules/*.md",
100
+ // AGENTS.md (AAIF / Linux Foundation standard)
98
101
  "AGENTS.md",
102
+ "AGENT.md",
103
+ "AGENTS.override.md",
104
+ // Cursor
99
105
  ".cursorrules",
100
106
  ".cursor/rules/*.md",
101
107
  ".cursor/rules/*.mdc",
102
- "copilot-instructions.md",
108
+ ".cursor/rules/*/RULE.md",
109
+ // GitHub Copilot
103
110
  ".github/copilot-instructions.md",
104
111
  ".github/instructions/*.md",
112
+ ".github/git-commit-instructions.md",
113
+ // Windsurf
105
114
  ".windsurfrules",
106
115
  ".windsurf/rules/*.md",
116
+ // Gemini CLI
107
117
  "GEMINI.md",
108
- "JULES.md",
118
+ // Cline
109
119
  ".clinerules",
110
- "CONVENTIONS.md",
111
- "CODEX.md",
120
+ // Aider — note: .aiderules has no file extension; this is the intended format
112
121
  ".aiderules",
122
+ // Aide / Codestory
113
123
  ".aide/rules/*.md",
124
+ // Amazon Q Developer
114
125
  ".amazonq/rules/*.md",
115
- ".goose/instructions.md"
126
+ // Goose (Block)
127
+ ".goose/instructions.md",
128
+ ".goosehints",
129
+ // JetBrains Junie
130
+ ".junie/guidelines.md",
131
+ ".junie/AGENTS.md",
132
+ // JetBrains AI Assistant
133
+ ".aiassistant/rules/*.md",
134
+ // Continue
135
+ ".continuerules",
136
+ ".continue/rules/*.md",
137
+ // Zed
138
+ ".rules",
139
+ // Replit
140
+ "replit.md"
116
141
  ];
117
142
  var IGNORED_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor"]);
118
- async function scanForContextFiles(projectRoot) {
143
+ async function scanForContextFiles(projectRoot, options = {}) {
144
+ const maxDepth = options.depth ?? 2;
145
+ const patterns = [...CONTEXT_FILE_PATTERNS, ...options.extraPatterns || []];
119
146
  const found = [];
120
147
  const seen = /* @__PURE__ */ new Set();
121
148
  const dirsToScan = [projectRoot];
122
- try {
123
- const entries = fs2.readdirSync(projectRoot, { withFileTypes: true });
124
- for (const entry of entries) {
125
- if (entry.isDirectory() && !IGNORED_DIRS2.has(entry.name) && !entry.name.startsWith(".")) {
126
- dirsToScan.push(path2.join(projectRoot, entry.name));
149
+ function collectDirs(dir, currentDepth) {
150
+ if (currentDepth >= maxDepth) return;
151
+ try {
152
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
153
+ for (const entry of entries) {
154
+ if (entry.isDirectory() && !IGNORED_DIRS2.has(entry.name) && !entry.name.startsWith(".")) {
155
+ const fullPath = path2.join(dir, entry.name);
156
+ dirsToScan.push(fullPath);
157
+ collectDirs(fullPath, currentDepth + 1);
158
+ }
127
159
  }
160
+ } catch {
128
161
  }
129
- } catch {
130
162
  }
163
+ collectDirs(projectRoot, 0);
131
164
  for (const dir of dirsToScan) {
132
- for (const pattern of CONTEXT_FILE_PATTERNS) {
165
+ for (const pattern of patterns) {
133
166
  const matches = await glob(pattern, {
134
167
  cwd: dir,
135
168
  absolute: true,
@@ -567,6 +600,7 @@ import * as fs3 from "fs";
567
600
  import * as path4 from "path";
568
601
  var NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
569
602
  var MAKE_PATTERN = /^make\s+(\S+)/;
603
+ var NPX_PATTERN = /^npx\s+(\S+)/;
570
604
  async function checkCommands(file, projectRoot) {
571
605
  const issues = [];
572
606
  const pkgJson = loadPackageJson(projectRoot);
@@ -588,7 +622,9 @@ async function checkCommands(file, projectRoot) {
588
622
  }
589
623
  continue;
590
624
  }
591
- const shorthandMatch = cmd.match(/^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/);
625
+ const shorthandMatch = cmd.match(
626
+ /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/
627
+ );
592
628
  if (shorthandMatch && pkgJson) {
593
629
  const scriptName = shorthandMatch[2];
594
630
  if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
@@ -601,6 +637,30 @@ async function checkCommands(file, projectRoot) {
601
637
  }
602
638
  continue;
603
639
  }
640
+ const npxMatch = cmd.match(NPX_PATTERN);
641
+ if (npxMatch && pkgJson) {
642
+ const pkgName = npxMatch[1];
643
+ if (pkgName.startsWith("-")) continue;
644
+ const allDeps = {
645
+ ...pkgJson.dependencies,
646
+ ...pkgJson.devDependencies
647
+ };
648
+ if (!(pkgName in allDeps)) {
649
+ const binPath = path4.join(projectRoot, "node_modules", ".bin", pkgName);
650
+ try {
651
+ fs3.accessSync(binPath);
652
+ } catch {
653
+ issues.push({
654
+ severity: "warning",
655
+ check: "commands",
656
+ line: ref.line,
657
+ message: `"${cmd}" \u2014 "${pkgName}" not found in dependencies`,
658
+ suggestion: "If this is a global tool, consider adding it to devDependencies for reproducibility"
659
+ });
660
+ }
661
+ }
662
+ continue;
663
+ }
604
664
  const makeMatch = cmd.match(MAKE_PATTERN);
605
665
  if (makeMatch) {
606
666
  const target = makeMatch[1];
@@ -813,6 +873,26 @@ var PACKAGE_TECH_MAP = {
813
873
  cypress: ["Cypress"],
814
874
  puppeteer: ["Puppeteer"]
815
875
  };
876
+ function compilePatterns(allDeps) {
877
+ const compiled = [];
878
+ for (const [pkg, mentions] of Object.entries(PACKAGE_TECH_MAP)) {
879
+ if (!allDeps.has(pkg)) continue;
880
+ for (const mention of mentions) {
881
+ const escaped = escapeRegex(mention);
882
+ compiled.push({
883
+ pkg,
884
+ mention,
885
+ patterns: [
886
+ new RegExp(`\\b(?:use|using|built with|powered by|written in)\\s+${escaped}\\b`, "i"),
887
+ new RegExp(`\\bwe\\s+use\\s+${escaped}\\b`, "i"),
888
+ new RegExp(`\\b${escaped}\\s+(?:project|app|application|codebase)\\b`, "i"),
889
+ new RegExp(`\\bThis is a\\s+${escaped}\\b`, "i")
890
+ ]
891
+ });
892
+ }
893
+ }
894
+ return compiled;
895
+ }
816
896
  async function checkRedundancy(file, projectRoot) {
817
897
  const issues = [];
818
898
  const pkgJson = loadPackageJson(projectRoot);
@@ -821,38 +901,28 @@ async function checkRedundancy(file, projectRoot) {
821
901
  ...Object.keys(pkgJson.dependencies || {}),
822
902
  ...Object.keys(pkgJson.devDependencies || {})
823
903
  ]);
904
+ const compiledPatterns = compilePatterns(allDeps);
824
905
  const lines2 = file.content.split("\n");
825
906
  for (let i = 0; i < lines2.length; i++) {
826
907
  const line = lines2[i];
827
- for (const [pkg, mentions] of Object.entries(PACKAGE_TECH_MAP)) {
828
- if (!allDeps.has(pkg)) continue;
829
- for (const mention of mentions) {
830
- const patterns = [
831
- new RegExp(
832
- `\\b(?:use|using|built with|powered by|written in)\\s+${escapeRegex(mention)}\\b`,
833
- "i"
834
- ),
835
- new RegExp(`\\bwe\\s+use\\s+${escapeRegex(mention)}\\b`, "i"),
836
- new RegExp(
837
- `\\b${escapeRegex(mention)}\\s+(?:project|app|application|codebase)\\b`,
838
- "i"
839
- ),
840
- new RegExp(`\\bThis is a\\s+${escapeRegex(mention)}\\b`, "i")
841
- ];
842
- for (const pattern of patterns) {
843
- if (pattern.test(line)) {
844
- const wastedTokens = countTokens(line.trim());
845
- issues.push({
846
- severity: "info",
847
- check: "redundancy",
848
- line: i + 1,
849
- message: `"${mention}" is in package.json ${pkgJson.dependencies?.[pkg] ? "dependencies" : "devDependencies"} \u2014 agent can infer this`,
850
- suggestion: `~${wastedTokens} tokens could be saved`
851
- });
852
- break;
853
- }
908
+ for (const { pkg, mention, patterns } of compiledPatterns) {
909
+ let matched = false;
910
+ for (const pattern of patterns) {
911
+ if (pattern.test(line)) {
912
+ matched = true;
913
+ break;
854
914
  }
855
915
  }
916
+ if (matched) {
917
+ const wastedTokens = countTokens(line.trim());
918
+ issues.push({
919
+ severity: "info",
920
+ check: "redundancy",
921
+ line: i + 1,
922
+ message: `"${mention}" is in package.json ${pkgJson.dependencies?.[pkg] ? "dependencies" : "devDependencies"} \u2014 agent can infer this`,
923
+ suggestion: `~${wastedTokens} tokens could be saved`
924
+ });
925
+ }
856
926
  }
857
927
  }
858
928
  }
@@ -914,6 +984,427 @@ function escapeRegex(str) {
914
984
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
915
985
  }
916
986
 
987
+ // src/core/checks/contradictions.ts
988
+ var DIRECTIVE_CATEGORIES = [
989
+ {
990
+ name: "testing framework",
991
+ options: [
992
+ {
993
+ label: "Jest",
994
+ patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest.*with\s+jest\b/i]
995
+ },
996
+ {
997
+ label: "Vitest",
998
+ patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest.*with\s+vitest\b/i]
999
+ },
1000
+ {
1001
+ label: "Mocha",
1002
+ patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest.*with\s+mocha\b/i]
1003
+ },
1004
+ {
1005
+ label: "pytest",
1006
+ patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest.*with\s+pytest\b/i]
1007
+ },
1008
+ {
1009
+ label: "Playwright",
1010
+ patterns: [/\buse\s+playwright\b/i, /\bplaywright\s+for\s+(?:e2e|test)/i]
1011
+ },
1012
+ { label: "Cypress", patterns: [/\buse\s+cypress\b/i, /\bcypress\s+for\s+(?:e2e|test)/i] }
1013
+ ]
1014
+ },
1015
+ {
1016
+ name: "package manager",
1017
+ options: [
1018
+ {
1019
+ label: "npm",
1020
+ patterns: [
1021
+ /\buse\s+npm\b/i,
1022
+ /\bnpm\s+as\s+(?:the\s+)?package\s+manager/i,
1023
+ /\balways\s+use\s+npm\b/i
1024
+ ]
1025
+ },
1026
+ {
1027
+ label: "pnpm",
1028
+ patterns: [
1029
+ /\buse\s+pnpm\b/i,
1030
+ /\bpnpm\s+as\s+(?:the\s+)?package\s+manager/i,
1031
+ /\balways\s+use\s+pnpm\b/i
1032
+ ]
1033
+ },
1034
+ {
1035
+ label: "yarn",
1036
+ patterns: [
1037
+ /\buse\s+yarn\b/i,
1038
+ /\byarn\s+as\s+(?:the\s+)?package\s+manager/i,
1039
+ /\balways\s+use\s+yarn\b/i
1040
+ ]
1041
+ },
1042
+ {
1043
+ label: "bun",
1044
+ patterns: [
1045
+ /\buse\s+bun\b/i,
1046
+ /\bbun\s+as\s+(?:the\s+)?package\s+manager/i,
1047
+ /\balways\s+use\s+bun\b/i
1048
+ ]
1049
+ }
1050
+ ]
1051
+ },
1052
+ {
1053
+ name: "indentation style",
1054
+ options: [
1055
+ {
1056
+ label: "tabs",
1057
+ patterns: [/\buse\s+tabs\b/i, /\btab\s+indentation\b/i, /\bindent\s+with\s+tabs\b/i]
1058
+ },
1059
+ {
1060
+ label: "2 spaces",
1061
+ patterns: [
1062
+ /\b2[\s-]?space\s+indent/i,
1063
+ /\bindent\s+with\s+2\s+spaces/i,
1064
+ /\b2[\s-]?space\s+tabs?\b/i
1065
+ ]
1066
+ },
1067
+ {
1068
+ label: "4 spaces",
1069
+ patterns: [
1070
+ /\b4[\s-]?space\s+indent/i,
1071
+ /\bindent\s+with\s+4\s+spaces/i,
1072
+ /\b4[\s-]?space\s+tabs?\b/i
1073
+ ]
1074
+ }
1075
+ ]
1076
+ },
1077
+ {
1078
+ name: "semicolons",
1079
+ options: [
1080
+ {
1081
+ label: "semicolons",
1082
+ patterns: [
1083
+ /\buse\s+semicolons\b/i,
1084
+ /\balways\s+(?:use\s+)?semicolons\b/i,
1085
+ /\bsemicolons:\s*(?:true|yes)\b/i
1086
+ ]
1087
+ },
1088
+ {
1089
+ label: "no semicolons",
1090
+ patterns: [
1091
+ /\bno\s+semicolons\b/i,
1092
+ /\bavoid\s+semicolons\b/i,
1093
+ /\bomit\s+semicolons\b/i,
1094
+ /\bsemicolons:\s*(?:false|no)\b/i
1095
+ ]
1096
+ }
1097
+ ]
1098
+ },
1099
+ {
1100
+ name: "quote style",
1101
+ options: [
1102
+ {
1103
+ label: "single quotes",
1104
+ patterns: [
1105
+ /\bsingle\s+quotes?\b/i,
1106
+ /\buse\s+(?:single\s+)?['']single['']?\s+quotes?\b/i,
1107
+ /\bprefer\s+single\s+quotes?\b/i
1108
+ ]
1109
+ },
1110
+ {
1111
+ label: "double quotes",
1112
+ patterns: [
1113
+ /\bdouble\s+quotes?\b/i,
1114
+ /\buse\s+(?:double\s+)?[""]double[""]?\s+quotes?\b/i,
1115
+ /\bprefer\s+double\s+quotes?\b/i
1116
+ ]
1117
+ }
1118
+ ]
1119
+ },
1120
+ {
1121
+ name: "naming convention",
1122
+ options: [
1123
+ {
1124
+ label: "camelCase",
1125
+ patterns: [/\bcamelCase\b/, /\bcamel[\s-]?case\s+(?:for|naming|convention)/i]
1126
+ },
1127
+ {
1128
+ label: "snake_case",
1129
+ patterns: [/\bsnake_case\b/, /\bsnake[\s-]?case\s+(?:for|naming|convention)/i]
1130
+ },
1131
+ {
1132
+ label: "PascalCase",
1133
+ patterns: [/\bPascalCase\b/, /\bpascal[\s-]?case\s+(?:for|naming|convention)/i]
1134
+ },
1135
+ {
1136
+ label: "kebab-case",
1137
+ patterns: [/\bkebab-case\b/, /\bkebab[\s-]?case\s+(?:for|naming|convention)/i]
1138
+ }
1139
+ ]
1140
+ },
1141
+ {
1142
+ name: "CSS approach",
1143
+ options: [
1144
+ { label: "Tailwind", patterns: [/\buse\s+tailwind/i, /\btailwind\s+for\s+styl/i] },
1145
+ {
1146
+ label: "CSS Modules",
1147
+ patterns: [/\buse\s+css\s+modules\b/i, /\bcss\s+modules\s+for\s+styl/i]
1148
+ },
1149
+ {
1150
+ label: "styled-components",
1151
+ patterns: [/\buse\s+styled[\s-]?components\b/i, /\bstyled[\s-]?components\s+for\s+styl/i]
1152
+ },
1153
+ { label: "CSS-in-JS", patterns: [/\buse\s+css[\s-]?in[\s-]?js\b/i] }
1154
+ ]
1155
+ },
1156
+ {
1157
+ name: "state management",
1158
+ options: [
1159
+ { label: "Redux", patterns: [/\buse\s+redux\b/i, /\bredux\s+for\s+state/i] },
1160
+ { label: "Zustand", patterns: [/\buse\s+zustand\b/i, /\bzustand\s+for\s+state/i] },
1161
+ { label: "MobX", patterns: [/\buse\s+mobx\b/i, /\bmobx\s+for\s+state/i] },
1162
+ { label: "Jotai", patterns: [/\buse\s+jotai\b/i, /\bjotai\s+for\s+state/i] },
1163
+ { label: "Recoil", patterns: [/\buse\s+recoil\b/i, /\brecoil\s+for\s+state/i] }
1164
+ ]
1165
+ }
1166
+ ];
1167
+ function detectDirectives(file) {
1168
+ const directives = [];
1169
+ const lines = file.content.split("\n");
1170
+ for (let i = 0; i < lines.length; i++) {
1171
+ const line = lines[i];
1172
+ for (const category of DIRECTIVE_CATEGORIES) {
1173
+ for (const option of category.options) {
1174
+ for (const pattern of option.patterns) {
1175
+ if (pattern.test(line)) {
1176
+ directives.push({
1177
+ file: file.relativePath,
1178
+ category: category.name,
1179
+ label: option.label,
1180
+ line: i + 1,
1181
+ text: line.trim()
1182
+ });
1183
+ break;
1184
+ }
1185
+ }
1186
+ }
1187
+ }
1188
+ }
1189
+ return directives;
1190
+ }
1191
+ function checkContradictions(files) {
1192
+ if (files.length < 2) return [];
1193
+ const issues = [];
1194
+ const allDirectives = [];
1195
+ for (const file of files) {
1196
+ allDirectives.push(...detectDirectives(file));
1197
+ }
1198
+ const byCategory = /* @__PURE__ */ new Map();
1199
+ for (const d of allDirectives) {
1200
+ const existing = byCategory.get(d.category) || [];
1201
+ existing.push(d);
1202
+ byCategory.set(d.category, existing);
1203
+ }
1204
+ for (const [category, directives] of byCategory) {
1205
+ const byFile = /* @__PURE__ */ new Map();
1206
+ for (const d of directives) {
1207
+ const existing = byFile.get(d.file) || [];
1208
+ existing.push(d);
1209
+ byFile.set(d.file, existing);
1210
+ }
1211
+ const labels = new Set(directives.map((d) => d.label));
1212
+ if (labels.size <= 1) continue;
1213
+ const fileLabels = /* @__PURE__ */ new Map();
1214
+ for (const d of directives) {
1215
+ const existing = fileLabels.get(d.file) || /* @__PURE__ */ new Set();
1216
+ existing.add(d.label);
1217
+ fileLabels.set(d.file, existing);
1218
+ }
1219
+ const fileEntries = [...fileLabels.entries()];
1220
+ for (let i = 0; i < fileEntries.length; i++) {
1221
+ for (let j = i + 1; j < fileEntries.length; j++) {
1222
+ const [fileA, labelsA] = fileEntries[i];
1223
+ const [fileB, labelsB] = fileEntries[j];
1224
+ for (const labelA of labelsA) {
1225
+ for (const labelB of labelsB) {
1226
+ if (labelA !== labelB) {
1227
+ const directiveA = directives.find((d) => d.file === fileA && d.label === labelA);
1228
+ const directiveB = directives.find((d) => d.file === fileB && d.label === labelB);
1229
+ issues.push({
1230
+ severity: "warning",
1231
+ check: "contradictions",
1232
+ line: directiveA.line,
1233
+ message: `${category} conflict: "${directiveA.label}" in ${fileA} vs "${directiveB.label}" in ${fileB}`,
1234
+ suggestion: `Align on one ${category} across all context files`,
1235
+ detail: `${fileA}:${directiveA.line} says "${directiveA.text}" but ${fileB}:${directiveB.line} says "${directiveB.text}"`
1236
+ });
1237
+ }
1238
+ }
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+ return issues;
1244
+ }
1245
+
1246
+ // src/core/checks/frontmatter.ts
1247
+ function parseFrontmatter(content) {
1248
+ const lines = content.split("\n");
1249
+ if (lines[0]?.trim() !== "---") {
1250
+ return { found: false, fields: {}, endLine: 0 };
1251
+ }
1252
+ const fields = {};
1253
+ let endLine = 0;
1254
+ for (let i = 1; i < lines.length; i++) {
1255
+ const line = lines[i].trim();
1256
+ if (line === "---") {
1257
+ endLine = i + 1;
1258
+ break;
1259
+ }
1260
+ const match = line.match(/^(\w+)\s*:\s*(.*)$/);
1261
+ if (match) {
1262
+ fields[match[1]] = match[2].trim();
1263
+ }
1264
+ }
1265
+ if (endLine === 0) {
1266
+ return { found: true, fields, endLine: lines.length };
1267
+ }
1268
+ return { found: true, fields, endLine };
1269
+ }
1270
+ function isCursorMdc(file) {
1271
+ return file.relativePath.endsWith(".mdc");
1272
+ }
1273
+ function isCopilotInstructions(file) {
1274
+ return file.relativePath.includes(".github/instructions/") && file.relativePath.endsWith(".md");
1275
+ }
1276
+ function isWindsurfRule(file) {
1277
+ return file.relativePath.includes(".windsurf/rules/") && file.relativePath.endsWith(".md");
1278
+ }
1279
+ var VALID_WINDSURF_TRIGGERS = ["always_on", "glob", "manual", "model"];
1280
+ async function checkFrontmatter(file, _projectRoot) {
1281
+ const issues = [];
1282
+ if (isCursorMdc(file)) {
1283
+ issues.push(...validateCursorMdc(file));
1284
+ } else if (isCopilotInstructions(file)) {
1285
+ issues.push(...validateCopilotInstructions(file));
1286
+ } else if (isWindsurfRule(file)) {
1287
+ issues.push(...validateWindsurfRule(file));
1288
+ }
1289
+ return issues;
1290
+ }
1291
+ function validateCursorMdc(file) {
1292
+ const issues = [];
1293
+ const fm = parseFrontmatter(file.content);
1294
+ if (!fm.found) {
1295
+ issues.push({
1296
+ severity: "warning",
1297
+ check: "frontmatter",
1298
+ line: 1,
1299
+ message: "Cursor .mdc file is missing frontmatter",
1300
+ suggestion: "Add YAML frontmatter with description, globs, and alwaysApply fields"
1301
+ });
1302
+ return issues;
1303
+ }
1304
+ if (!fm.fields["description"]) {
1305
+ issues.push({
1306
+ severity: "warning",
1307
+ check: "frontmatter",
1308
+ line: 1,
1309
+ message: 'Missing "description" field in Cursor .mdc frontmatter',
1310
+ suggestion: "Add a description so Cursor knows when to apply this rule"
1311
+ });
1312
+ }
1313
+ if (!("alwaysApply" in fm.fields) && !("globs" in fm.fields)) {
1314
+ issues.push({
1315
+ severity: "info",
1316
+ check: "frontmatter",
1317
+ line: 1,
1318
+ message: 'No "alwaysApply" or "globs" field \u2014 rule may not be applied automatically',
1319
+ suggestion: "Set alwaysApply: true or specify globs for targeted activation"
1320
+ });
1321
+ }
1322
+ if ("alwaysApply" in fm.fields) {
1323
+ const val = fm.fields["alwaysApply"].toLowerCase();
1324
+ if (!["true", "false"].includes(val)) {
1325
+ issues.push({
1326
+ severity: "error",
1327
+ check: "frontmatter",
1328
+ line: 1,
1329
+ message: `Invalid alwaysApply value: "${fm.fields["alwaysApply"]}"`,
1330
+ suggestion: "alwaysApply must be true or false"
1331
+ });
1332
+ }
1333
+ }
1334
+ if ("globs" in fm.fields) {
1335
+ const val = fm.fields["globs"];
1336
+ if (val && !val.startsWith("[") && !val.startsWith('"') && !val.includes("*") && !val.includes("/")) {
1337
+ issues.push({
1338
+ severity: "warning",
1339
+ check: "frontmatter",
1340
+ line: 1,
1341
+ message: `Possibly invalid globs value: "${val}"`,
1342
+ suggestion: 'globs should be a glob pattern like "src/**/*.ts" or an array like ["*.ts", "*.tsx"]'
1343
+ });
1344
+ }
1345
+ }
1346
+ return issues;
1347
+ }
1348
+ function validateCopilotInstructions(file) {
1349
+ const issues = [];
1350
+ const fm = parseFrontmatter(file.content);
1351
+ if (!fm.found) {
1352
+ issues.push({
1353
+ severity: "info",
1354
+ check: "frontmatter",
1355
+ line: 1,
1356
+ message: "Copilot instructions file has no frontmatter",
1357
+ suggestion: "Add applyTo frontmatter to target specific file patterns"
1358
+ });
1359
+ return issues;
1360
+ }
1361
+ if (!fm.fields["applyTo"]) {
1362
+ issues.push({
1363
+ severity: "warning",
1364
+ check: "frontmatter",
1365
+ line: 1,
1366
+ message: 'Missing "applyTo" field in Copilot instructions frontmatter',
1367
+ suggestion: 'Add applyTo to specify which files this instruction applies to (e.g., applyTo: "**/*.ts")'
1368
+ });
1369
+ }
1370
+ return issues;
1371
+ }
1372
+ function validateWindsurfRule(file) {
1373
+ const issues = [];
1374
+ const fm = parseFrontmatter(file.content);
1375
+ if (!fm.found) {
1376
+ issues.push({
1377
+ severity: "info",
1378
+ check: "frontmatter",
1379
+ line: 1,
1380
+ message: "Windsurf rule file has no frontmatter",
1381
+ suggestion: "Add YAML frontmatter with a trigger field (always_on, glob, manual, model)"
1382
+ });
1383
+ return issues;
1384
+ }
1385
+ if (!fm.fields["trigger"]) {
1386
+ issues.push({
1387
+ severity: "warning",
1388
+ check: "frontmatter",
1389
+ line: 1,
1390
+ message: 'Missing "trigger" field in Windsurf rule frontmatter',
1391
+ suggestion: `Set trigger to one of: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
1392
+ });
1393
+ } else {
1394
+ const trigger = fm.fields["trigger"].replace(/['"]/g, "");
1395
+ if (!VALID_WINDSURF_TRIGGERS.includes(trigger)) {
1396
+ issues.push({
1397
+ severity: "error",
1398
+ check: "frontmatter",
1399
+ line: 1,
1400
+ message: `Invalid trigger value: "${trigger}"`,
1401
+ suggestion: `Valid triggers: ${VALID_WINDSURF_TRIGGERS.join(", ")}`
1402
+ });
1403
+ }
1404
+ }
1405
+ return issues;
1406
+ }
1407
+
917
1408
  // src/core/fixer.ts
918
1409
  import * as fs4 from "fs";
919
1410
  import chalk from "chalk";
@@ -961,7 +1452,7 @@ import * as path7 from "path";
961
1452
 
962
1453
  // src/version.ts
963
1454
  function loadVersion() {
964
- if (true) return "0.2.2";
1455
+ if (true) return "0.3.0";
965
1456
  const fs5 = __require("fs");
966
1457
  const path8 = __require("path");
967
1458
  const pkgPath = path8.resolve(__dirname, "../package.json");
@@ -971,17 +1462,34 @@ function loadVersion() {
971
1462
  var VERSION = loadVersion();
972
1463
 
973
1464
  // src/mcp/server.ts
974
- var ALL_CHECKS = ["paths", "commands", "staleness", "tokens", "redundancy"];
1465
+ var ALL_CHECKS = [
1466
+ "paths",
1467
+ "commands",
1468
+ "staleness",
1469
+ "tokens",
1470
+ "redundancy",
1471
+ "contradictions",
1472
+ "frontmatter"
1473
+ ];
1474
+ var checkEnum = z.enum([
1475
+ "paths",
1476
+ "commands",
1477
+ "staleness",
1478
+ "tokens",
1479
+ "redundancy",
1480
+ "contradictions",
1481
+ "frontmatter"
1482
+ ]);
975
1483
  var server = new McpServer({
976
1484
  name: "ctxlint",
977
1485
  version: VERSION
978
1486
  });
979
1487
  server.tool(
980
1488
  "ctxlint_audit",
981
- "Audit all AI agent context files (CLAUDE.md, AGENTS.md, etc.) in the project for stale references, invalid commands, redundant content, and token waste.",
1489
+ "Audit all AI agent context files (CLAUDE.md, AGENTS.md, etc.) in the project for stale references, invalid commands, redundant content, contradictions, frontmatter issues, and token waste.",
982
1490
  {
983
1491
  projectPath: z.string().optional().describe("Path to the project root. Defaults to current working directory."),
984
- checks: z.array(z.enum(["paths", "commands", "staleness", "tokens", "redundancy"])).optional().describe("Which checks to run. Defaults to all.")
1492
+ checks: z.array(checkEnum).optional().describe("Which checks to run. Defaults to all.")
985
1493
  },
986
1494
  async ({ projectPath, checks }) => {
987
1495
  const root = path7.resolve(projectPath || process.cwd());
@@ -1059,7 +1567,11 @@ server.tool(
1059
1567
  content: [
1060
1568
  {
1061
1569
  type: "text",
1062
- text: JSON.stringify({ files, totalTokens }, null, 2)
1570
+ text: JSON.stringify(
1571
+ { files, totalTokens, note: "Token counts use GPT-4 cl100k_base tokenizer" },
1572
+ null,
1573
+ 2
1574
+ )
1063
1575
  }
1064
1576
  ]
1065
1577
  };
@@ -1079,7 +1591,7 @@ server.tool(
1079
1591
  "Run the linter with --fix mode to auto-correct broken file paths in context files using git history and fuzzy matching. Returns a summary of what was fixed.",
1080
1592
  {
1081
1593
  projectPath: z.string().optional().describe("Path to the project root. Defaults to current working directory."),
1082
- checks: z.array(z.enum(["paths", "commands", "staleness", "tokens", "redundancy"])).optional().describe("Which checks to run before fixing. Defaults to all.")
1594
+ checks: z.array(checkEnum).optional().describe("Which checks to run before fixing. Defaults to all.")
1083
1595
  },
1084
1596
  async ({ projectPath, checks }) => {
1085
1597
  const root = path7.resolve(projectPath || process.cwd());
@@ -1128,6 +1640,8 @@ async function runAudit(projectRoot, activeChecks) {
1128
1640
  if (activeChecks.includes("tokens")) issues.push(...await checkTokens(file, projectRoot));
1129
1641
  if (activeChecks.includes("redundancy"))
1130
1642
  issues.push(...await checkRedundancy(file, projectRoot));
1643
+ if (activeChecks.includes("frontmatter"))
1644
+ issues.push(...await checkFrontmatter(file, projectRoot));
1131
1645
  fileResults.push({
1132
1646
  path: file.relativePath,
1133
1647
  isSymlink: file.isSymlink,
@@ -1147,6 +1661,11 @@ async function runAudit(projectRoot, activeChecks) {
1147
1661
  const dupIssues = checkDuplicateContent(parsed);
1148
1662
  if (dupIssues.length > 0 && fileResults.length > 0) fileResults[0].issues.push(...dupIssues);
1149
1663
  }
1664
+ if (activeChecks.includes("contradictions")) {
1665
+ const contradictionIssues = checkContradictions(parsed);
1666
+ if (contradictionIssues.length > 0 && fileResults.length > 0)
1667
+ fileResults[0].issues.push(...contradictionIssues);
1668
+ }
1150
1669
  let estimatedWaste = 0;
1151
1670
  for (const fr of fileResults) {
1152
1671
  for (const issue of fr.issues) {