@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.
- package/README.md +109 -24
- package/dist/chunk-WEYNMCAH.js +1562 -0
- package/dist/cli-VYWAONGX.js +354 -0
- package/dist/index.js +5 -1275
- package/dist/mcp/server.js +669 -123
- package/dist/server-7C2IQ7VV.js +202 -0
- package/package.json +11 -3
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
|
|
118
|
+
// Cline
|
|
109
119
|
".clinerules",
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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(
|
|
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
|
|
828
|
-
|
|
829
|
-
for (const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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,523 @@ 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
|
+
|
|
1408
|
+
// src/version.ts
|
|
1409
|
+
function loadVersion() {
|
|
1410
|
+
if (true) return "0.4.0";
|
|
1411
|
+
const fs5 = __require("fs");
|
|
1412
|
+
const path8 = __require("path");
|
|
1413
|
+
const pkgPath = path8.resolve(__dirname, "../package.json");
|
|
1414
|
+
const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
1415
|
+
return pkg.version;
|
|
1416
|
+
}
|
|
1417
|
+
var VERSION = loadVersion();
|
|
1418
|
+
|
|
1419
|
+
// src/core/audit.ts
|
|
1420
|
+
var ALL_CHECKS = [
|
|
1421
|
+
"paths",
|
|
1422
|
+
"commands",
|
|
1423
|
+
"staleness",
|
|
1424
|
+
"tokens",
|
|
1425
|
+
"redundancy",
|
|
1426
|
+
"contradictions",
|
|
1427
|
+
"frontmatter"
|
|
1428
|
+
];
|
|
1429
|
+
async function runAudit(projectRoot, activeChecks, options = {}) {
|
|
1430
|
+
const discovered = await scanForContextFiles(projectRoot, {
|
|
1431
|
+
depth: options.depth,
|
|
1432
|
+
extraPatterns: options.extraPatterns
|
|
1433
|
+
});
|
|
1434
|
+
const parsed = discovered.map((f) => parseContextFile(f));
|
|
1435
|
+
const fileResults = [];
|
|
1436
|
+
for (const file of parsed) {
|
|
1437
|
+
const checkPromises = [];
|
|
1438
|
+
if (activeChecks.includes("paths")) checkPromises.push(checkPaths(file, projectRoot));
|
|
1439
|
+
if (activeChecks.includes("commands")) checkPromises.push(checkCommands(file, projectRoot));
|
|
1440
|
+
if (activeChecks.includes("staleness")) checkPromises.push(checkStaleness(file, projectRoot));
|
|
1441
|
+
if (activeChecks.includes("tokens")) checkPromises.push(checkTokens(file, projectRoot));
|
|
1442
|
+
if (activeChecks.includes("redundancy")) checkPromises.push(checkRedundancy(file, projectRoot));
|
|
1443
|
+
if (activeChecks.includes("frontmatter"))
|
|
1444
|
+
checkPromises.push(checkFrontmatter(file, projectRoot));
|
|
1445
|
+
const results = await Promise.all(checkPromises);
|
|
1446
|
+
const issues = results.flat();
|
|
1447
|
+
fileResults.push({
|
|
1448
|
+
path: file.relativePath,
|
|
1449
|
+
isSymlink: file.isSymlink,
|
|
1450
|
+
symlinkTarget: file.symlinkTarget,
|
|
1451
|
+
tokens: file.totalTokens,
|
|
1452
|
+
lines: file.totalLines,
|
|
1453
|
+
issues
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
if (activeChecks.includes("tokens")) {
|
|
1457
|
+
const aggIssue = checkAggregateTokens(
|
|
1458
|
+
fileResults.map((f) => ({ path: f.path, tokens: f.tokens }))
|
|
1459
|
+
);
|
|
1460
|
+
if (aggIssue && fileResults.length > 0) fileResults[0].issues.push(aggIssue);
|
|
1461
|
+
}
|
|
1462
|
+
if (activeChecks.includes("redundancy")) {
|
|
1463
|
+
const dupIssues = checkDuplicateContent(parsed);
|
|
1464
|
+
if (dupIssues.length > 0 && fileResults.length > 0) fileResults[0].issues.push(...dupIssues);
|
|
1465
|
+
}
|
|
1466
|
+
if (activeChecks.includes("contradictions")) {
|
|
1467
|
+
const contradictionIssues = checkContradictions(parsed);
|
|
1468
|
+
if (contradictionIssues.length > 0 && fileResults.length > 0)
|
|
1469
|
+
fileResults[0].issues.push(...contradictionIssues);
|
|
1470
|
+
}
|
|
1471
|
+
let estimatedWaste = 0;
|
|
1472
|
+
for (const fr of fileResults) {
|
|
1473
|
+
for (const issue of fr.issues) {
|
|
1474
|
+
if (issue.check === "redundancy" && issue.suggestion) {
|
|
1475
|
+
const tokenMatch = issue.suggestion.match(/~(\d+)\s+tokens/);
|
|
1476
|
+
if (tokenMatch) estimatedWaste += parseInt(tokenMatch[1], 10);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
version: VERSION,
|
|
1482
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1483
|
+
projectRoot,
|
|
1484
|
+
files: fileResults,
|
|
1485
|
+
summary: {
|
|
1486
|
+
errors: fileResults.reduce(
|
|
1487
|
+
(sum, f) => sum + f.issues.filter((i) => i.severity === "error").length,
|
|
1488
|
+
0
|
|
1489
|
+
),
|
|
1490
|
+
warnings: fileResults.reduce(
|
|
1491
|
+
(sum, f) => sum + f.issues.filter((i) => i.severity === "warning").length,
|
|
1492
|
+
0
|
|
1493
|
+
),
|
|
1494
|
+
info: fileResults.reduce(
|
|
1495
|
+
(sum, f) => sum + f.issues.filter((i) => i.severity === "info").length,
|
|
1496
|
+
0
|
|
1497
|
+
),
|
|
1498
|
+
totalTokens: fileResults.reduce((sum, f) => sum + f.tokens, 0),
|
|
1499
|
+
estimatedWaste
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
|
|
917
1504
|
// src/core/fixer.ts
|
|
918
1505
|
import * as fs4 from "fs";
|
|
919
1506
|
import chalk from "chalk";
|
|
@@ -958,30 +1545,31 @@ function applyFixes(result) {
|
|
|
958
1545
|
|
|
959
1546
|
// src/mcp/server.ts
|
|
960
1547
|
import * as path7 from "path";
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
var VERSION = loadVersion();
|
|
972
|
-
|
|
973
|
-
// src/mcp/server.ts
|
|
974
|
-
var ALL_CHECKS = ["paths", "commands", "staleness", "tokens", "redundancy"];
|
|
1548
|
+
var checkEnum = z.enum([
|
|
1549
|
+
"paths",
|
|
1550
|
+
"commands",
|
|
1551
|
+
"staleness",
|
|
1552
|
+
"tokens",
|
|
1553
|
+
"redundancy",
|
|
1554
|
+
"contradictions",
|
|
1555
|
+
"frontmatter"
|
|
1556
|
+
]);
|
|
975
1557
|
var server = new McpServer({
|
|
976
1558
|
name: "ctxlint",
|
|
977
1559
|
version: VERSION
|
|
978
1560
|
});
|
|
979
1561
|
server.tool(
|
|
980
1562
|
"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.",
|
|
1563
|
+
"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
1564
|
{
|
|
983
1565
|
projectPath: z.string().optional().describe("Path to the project root. Defaults to current working directory."),
|
|
984
|
-
checks: z.array(
|
|
1566
|
+
checks: z.array(checkEnum).optional().describe("Which checks to run. Defaults to all.")
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
readOnlyHint: true,
|
|
1570
|
+
destructiveHint: false,
|
|
1571
|
+
idempotentHint: true,
|
|
1572
|
+
openWorldHint: false
|
|
985
1573
|
},
|
|
986
1574
|
async ({ projectPath, checks }) => {
|
|
987
1575
|
const root = path7.resolve(projectPath || process.cwd());
|
|
@@ -1008,6 +1596,12 @@ server.tool(
|
|
|
1008
1596
|
path: z.string().describe("The file path to validate"),
|
|
1009
1597
|
projectPath: z.string().optional().describe("Project root. Defaults to cwd.")
|
|
1010
1598
|
},
|
|
1599
|
+
{
|
|
1600
|
+
readOnlyHint: true,
|
|
1601
|
+
destructiveHint: false,
|
|
1602
|
+
idempotentHint: true,
|
|
1603
|
+
openWorldHint: false
|
|
1604
|
+
},
|
|
1011
1605
|
async ({ path: filePath, projectPath }) => {
|
|
1012
1606
|
try {
|
|
1013
1607
|
const root = path7.resolve(projectPath || process.cwd());
|
|
@@ -1043,6 +1637,12 @@ server.tool(
|
|
|
1043
1637
|
{
|
|
1044
1638
|
projectPath: z.string().optional().describe("Project root. Defaults to cwd.")
|
|
1045
1639
|
},
|
|
1640
|
+
{
|
|
1641
|
+
readOnlyHint: true,
|
|
1642
|
+
destructiveHint: false,
|
|
1643
|
+
idempotentHint: true,
|
|
1644
|
+
openWorldHint: false
|
|
1645
|
+
},
|
|
1046
1646
|
async ({ projectPath }) => {
|
|
1047
1647
|
const root = path7.resolve(projectPath || process.cwd());
|
|
1048
1648
|
try {
|
|
@@ -1059,7 +1659,11 @@ server.tool(
|
|
|
1059
1659
|
content: [
|
|
1060
1660
|
{
|
|
1061
1661
|
type: "text",
|
|
1062
|
-
text: JSON.stringify(
|
|
1662
|
+
text: JSON.stringify(
|
|
1663
|
+
{ files, totalTokens, note: "Token counts use GPT-4 cl100k_base tokenizer" },
|
|
1664
|
+
null,
|
|
1665
|
+
2
|
|
1666
|
+
)
|
|
1063
1667
|
}
|
|
1064
1668
|
]
|
|
1065
1669
|
};
|
|
@@ -1079,7 +1683,13 @@ server.tool(
|
|
|
1079
1683
|
"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
1684
|
{
|
|
1081
1685
|
projectPath: z.string().optional().describe("Path to the project root. Defaults to current working directory."),
|
|
1082
|
-
checks: z.array(
|
|
1686
|
+
checks: z.array(checkEnum).optional().describe("Which checks to run before fixing. Defaults to all.")
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
readOnlyHint: false,
|
|
1690
|
+
destructiveHint: false,
|
|
1691
|
+
idempotentHint: true,
|
|
1692
|
+
openWorldHint: false
|
|
1083
1693
|
},
|
|
1084
1694
|
async ({ projectPath, checks }) => {
|
|
1085
1695
|
const root = path7.resolve(projectPath || process.cwd());
|
|
@@ -1115,69 +1725,5 @@ server.tool(
|
|
|
1115
1725
|
}
|
|
1116
1726
|
}
|
|
1117
1727
|
);
|
|
1118
|
-
async function runAudit(projectRoot, activeChecks) {
|
|
1119
|
-
const discovered = await scanForContextFiles(projectRoot);
|
|
1120
|
-
const parsed = discovered.map((f) => parseContextFile(f));
|
|
1121
|
-
const fileResults = [];
|
|
1122
|
-
for (const file of parsed) {
|
|
1123
|
-
const issues = [];
|
|
1124
|
-
if (activeChecks.includes("paths")) issues.push(...await checkPaths(file, projectRoot));
|
|
1125
|
-
if (activeChecks.includes("commands")) issues.push(...await checkCommands(file, projectRoot));
|
|
1126
|
-
if (activeChecks.includes("staleness"))
|
|
1127
|
-
issues.push(...await checkStaleness(file, projectRoot));
|
|
1128
|
-
if (activeChecks.includes("tokens")) issues.push(...await checkTokens(file, projectRoot));
|
|
1129
|
-
if (activeChecks.includes("redundancy"))
|
|
1130
|
-
issues.push(...await checkRedundancy(file, projectRoot));
|
|
1131
|
-
fileResults.push({
|
|
1132
|
-
path: file.relativePath,
|
|
1133
|
-
isSymlink: file.isSymlink,
|
|
1134
|
-
symlinkTarget: file.symlinkTarget,
|
|
1135
|
-
tokens: file.totalTokens,
|
|
1136
|
-
lines: file.totalLines,
|
|
1137
|
-
issues
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
if (activeChecks.includes("tokens")) {
|
|
1141
|
-
const aggIssue = checkAggregateTokens(
|
|
1142
|
-
fileResults.map((f) => ({ path: f.path, tokens: f.tokens }))
|
|
1143
|
-
);
|
|
1144
|
-
if (aggIssue && fileResults.length > 0) fileResults[0].issues.push(aggIssue);
|
|
1145
|
-
}
|
|
1146
|
-
if (activeChecks.includes("redundancy")) {
|
|
1147
|
-
const dupIssues = checkDuplicateContent(parsed);
|
|
1148
|
-
if (dupIssues.length > 0 && fileResults.length > 0) fileResults[0].issues.push(...dupIssues);
|
|
1149
|
-
}
|
|
1150
|
-
let estimatedWaste = 0;
|
|
1151
|
-
for (const fr of fileResults) {
|
|
1152
|
-
for (const issue of fr.issues) {
|
|
1153
|
-
if (issue.check === "redundancy" && issue.suggestion) {
|
|
1154
|
-
const tokenMatch = issue.suggestion.match(/~(\d+)\s+tokens/);
|
|
1155
|
-
if (tokenMatch) estimatedWaste += parseInt(tokenMatch[1], 10);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
return {
|
|
1160
|
-
version: VERSION,
|
|
1161
|
-
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1162
|
-
projectRoot,
|
|
1163
|
-
files: fileResults,
|
|
1164
|
-
summary: {
|
|
1165
|
-
errors: fileResults.reduce(
|
|
1166
|
-
(sum, f) => sum + f.issues.filter((i) => i.severity === "error").length,
|
|
1167
|
-
0
|
|
1168
|
-
),
|
|
1169
|
-
warnings: fileResults.reduce(
|
|
1170
|
-
(sum, f) => sum + f.issues.filter((i) => i.severity === "warning").length,
|
|
1171
|
-
0
|
|
1172
|
-
),
|
|
1173
|
-
info: fileResults.reduce(
|
|
1174
|
-
(sum, f) => sum + f.issues.filter((i) => i.severity === "info").length,
|
|
1175
|
-
0
|
|
1176
|
-
),
|
|
1177
|
-
totalTokens: fileResults.reduce((sum, f) => sum + f.tokens, 0),
|
|
1178
|
-
estimatedWaste
|
|
1179
|
-
}
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
1728
|
var transport = new StdioServerTransport();
|
|
1183
1729
|
await server.connect(transport);
|