agent-context-lint 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/cli.js +525 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +493 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +461 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
interface LintFinding {
|
|
2
|
+
file: string;
|
|
3
|
+
rule: string;
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
severity: 'error' | 'warning';
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
interface FileResult {
|
|
10
|
+
file: string;
|
|
11
|
+
findings: LintFinding[];
|
|
12
|
+
score: number;
|
|
13
|
+
}
|
|
14
|
+
interface LintResult {
|
|
15
|
+
files: FileResult[];
|
|
16
|
+
totalFindings: number;
|
|
17
|
+
errors: number;
|
|
18
|
+
warnings: number;
|
|
19
|
+
}
|
|
20
|
+
interface CLIOptions {
|
|
21
|
+
files: string[];
|
|
22
|
+
format: 'text' | 'json';
|
|
23
|
+
fix: boolean;
|
|
24
|
+
cwd: string;
|
|
25
|
+
}
|
|
26
|
+
interface Config {
|
|
27
|
+
tokenBudget: {
|
|
28
|
+
warn: number;
|
|
29
|
+
error: number;
|
|
30
|
+
};
|
|
31
|
+
requiredSections: string[];
|
|
32
|
+
staleDateYears: number;
|
|
33
|
+
vaguePatterns: string[];
|
|
34
|
+
ignore: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
declare function discoverContextFiles(cwd: string): string[];
|
|
38
|
+
|
|
39
|
+
interface ParsedFile {
|
|
40
|
+
content: string;
|
|
41
|
+
lines: string[];
|
|
42
|
+
paths: PathReference[];
|
|
43
|
+
commands: CommandReference[];
|
|
44
|
+
sections: string[];
|
|
45
|
+
codeBlocks: CodeBlock[];
|
|
46
|
+
inlineCode: InlineCode[];
|
|
47
|
+
}
|
|
48
|
+
interface PathReference {
|
|
49
|
+
value: string;
|
|
50
|
+
line: number;
|
|
51
|
+
column: number;
|
|
52
|
+
}
|
|
53
|
+
interface CommandReference {
|
|
54
|
+
value: string;
|
|
55
|
+
line: number;
|
|
56
|
+
column: number;
|
|
57
|
+
}
|
|
58
|
+
interface CodeBlock {
|
|
59
|
+
content: string;
|
|
60
|
+
lang: string;
|
|
61
|
+
line: number;
|
|
62
|
+
}
|
|
63
|
+
interface InlineCode {
|
|
64
|
+
content: string;
|
|
65
|
+
line: number;
|
|
66
|
+
column: number;
|
|
67
|
+
}
|
|
68
|
+
declare function parseFile(filePath: string): ParsedFile;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Computes a 0–100 quality score for a file based on its findings.
|
|
72
|
+
*
|
|
73
|
+
* Starts at 100 and deducts:
|
|
74
|
+
* - 15 points per error
|
|
75
|
+
* - 5 points per warning
|
|
76
|
+
*
|
|
77
|
+
* Minimum score is 0.
|
|
78
|
+
*/
|
|
79
|
+
declare function computeScore(findings: LintFinding[]): number;
|
|
80
|
+
|
|
81
|
+
declare function loadConfig(cwd: string): Config;
|
|
82
|
+
|
|
83
|
+
declare function formatText(result: LintResult, cwd: string): string;
|
|
84
|
+
declare function formatJson(result: LintResult, cwd: string): string;
|
|
85
|
+
|
|
86
|
+
declare function lintFile(filePath: string, cwd: string): FileResult;
|
|
87
|
+
declare function lint(cwd: string, files?: string[]): LintResult;
|
|
88
|
+
|
|
89
|
+
export { type CLIOptions, type Config, type FileResult, type LintFinding, type LintResult, computeScore, discoverContextFiles, formatJson, formatText, lint, lintFile, loadConfig, parseFile };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
interface LintFinding {
|
|
2
|
+
file: string;
|
|
3
|
+
rule: string;
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
severity: 'error' | 'warning';
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
interface FileResult {
|
|
10
|
+
file: string;
|
|
11
|
+
findings: LintFinding[];
|
|
12
|
+
score: number;
|
|
13
|
+
}
|
|
14
|
+
interface LintResult {
|
|
15
|
+
files: FileResult[];
|
|
16
|
+
totalFindings: number;
|
|
17
|
+
errors: number;
|
|
18
|
+
warnings: number;
|
|
19
|
+
}
|
|
20
|
+
interface CLIOptions {
|
|
21
|
+
files: string[];
|
|
22
|
+
format: 'text' | 'json';
|
|
23
|
+
fix: boolean;
|
|
24
|
+
cwd: string;
|
|
25
|
+
}
|
|
26
|
+
interface Config {
|
|
27
|
+
tokenBudget: {
|
|
28
|
+
warn: number;
|
|
29
|
+
error: number;
|
|
30
|
+
};
|
|
31
|
+
requiredSections: string[];
|
|
32
|
+
staleDateYears: number;
|
|
33
|
+
vaguePatterns: string[];
|
|
34
|
+
ignore: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
declare function discoverContextFiles(cwd: string): string[];
|
|
38
|
+
|
|
39
|
+
interface ParsedFile {
|
|
40
|
+
content: string;
|
|
41
|
+
lines: string[];
|
|
42
|
+
paths: PathReference[];
|
|
43
|
+
commands: CommandReference[];
|
|
44
|
+
sections: string[];
|
|
45
|
+
codeBlocks: CodeBlock[];
|
|
46
|
+
inlineCode: InlineCode[];
|
|
47
|
+
}
|
|
48
|
+
interface PathReference {
|
|
49
|
+
value: string;
|
|
50
|
+
line: number;
|
|
51
|
+
column: number;
|
|
52
|
+
}
|
|
53
|
+
interface CommandReference {
|
|
54
|
+
value: string;
|
|
55
|
+
line: number;
|
|
56
|
+
column: number;
|
|
57
|
+
}
|
|
58
|
+
interface CodeBlock {
|
|
59
|
+
content: string;
|
|
60
|
+
lang: string;
|
|
61
|
+
line: number;
|
|
62
|
+
}
|
|
63
|
+
interface InlineCode {
|
|
64
|
+
content: string;
|
|
65
|
+
line: number;
|
|
66
|
+
column: number;
|
|
67
|
+
}
|
|
68
|
+
declare function parseFile(filePath: string): ParsedFile;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Computes a 0–100 quality score for a file based on its findings.
|
|
72
|
+
*
|
|
73
|
+
* Starts at 100 and deducts:
|
|
74
|
+
* - 15 points per error
|
|
75
|
+
* - 5 points per warning
|
|
76
|
+
*
|
|
77
|
+
* Minimum score is 0.
|
|
78
|
+
*/
|
|
79
|
+
declare function computeScore(findings: LintFinding[]): number;
|
|
80
|
+
|
|
81
|
+
declare function loadConfig(cwd: string): Config;
|
|
82
|
+
|
|
83
|
+
declare function formatText(result: LintResult, cwd: string): string;
|
|
84
|
+
declare function formatJson(result: LintResult, cwd: string): string;
|
|
85
|
+
|
|
86
|
+
declare function lintFile(filePath: string, cwd: string): FileResult;
|
|
87
|
+
declare function lint(cwd: string, files?: string[]): LintResult;
|
|
88
|
+
|
|
89
|
+
export { type CLIOptions, type Config, type FileResult, type LintFinding, type LintResult, computeScore, discoverContextFiles, formatJson, formatText, lint, lintFile, loadConfig, parseFile };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { resolve as resolve4 } from "path";
|
|
3
|
+
|
|
4
|
+
// src/checkers.ts
|
|
5
|
+
import { existsSync, readFileSync } from "fs";
|
|
6
|
+
import { dirname, resolve } from "path";
|
|
7
|
+
function checkPaths(parsed, filePath) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
const baseDir = dirname(filePath);
|
|
10
|
+
for (const ref of parsed.paths) {
|
|
11
|
+
const resolved = resolve(baseDir, ref.value);
|
|
12
|
+
if (!existsSync(resolved)) {
|
|
13
|
+
findings.push({
|
|
14
|
+
file: filePath,
|
|
15
|
+
rule: "check:paths",
|
|
16
|
+
line: ref.line,
|
|
17
|
+
column: ref.column,
|
|
18
|
+
severity: "error",
|
|
19
|
+
message: `Path does not exist: ${ref.value}`
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return findings;
|
|
24
|
+
}
|
|
25
|
+
function checkScripts(parsed, filePath) {
|
|
26
|
+
const findings = [];
|
|
27
|
+
const baseDir = dirname(filePath);
|
|
28
|
+
const pkgPath = resolve(baseDir, "package.json");
|
|
29
|
+
let scripts = {};
|
|
30
|
+
if (existsSync(pkgPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
33
|
+
scripts = pkg.scripts || {};
|
|
34
|
+
} catch {
|
|
35
|
+
return findings;
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
return findings;
|
|
39
|
+
}
|
|
40
|
+
for (const cmd of parsed.commands) {
|
|
41
|
+
const match = /(?:npm|pnpm|yarn|bun)\s+run\s+([\w:@./-]+)/.exec(cmd.value);
|
|
42
|
+
const directMatch = /(?:npm|pnpm|yarn|bun)\s+(test|start|build|lint)\b/.exec(
|
|
43
|
+
cmd.value
|
|
44
|
+
);
|
|
45
|
+
const scriptName = match?.[1] || directMatch?.[1];
|
|
46
|
+
if (scriptName && !(scriptName in scripts)) {
|
|
47
|
+
findings.push({
|
|
48
|
+
file: filePath,
|
|
49
|
+
rule: "check:scripts",
|
|
50
|
+
line: cmd.line,
|
|
51
|
+
column: cmd.column,
|
|
52
|
+
severity: "error",
|
|
53
|
+
message: `Script not found in package.json: "${scriptName}"`
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
}
|
|
59
|
+
function checkTokenBudget(parsed, filePath, config) {
|
|
60
|
+
const findings = [];
|
|
61
|
+
const estimatedTokens = Math.ceil(parsed.content.length / 4);
|
|
62
|
+
if (estimatedTokens > config.tokenBudget.error) {
|
|
63
|
+
findings.push({
|
|
64
|
+
file: filePath,
|
|
65
|
+
rule: "check:token-budget",
|
|
66
|
+
line: 1,
|
|
67
|
+
column: 1,
|
|
68
|
+
severity: "error",
|
|
69
|
+
message: `File is ~${estimatedTokens} tokens (limit: ${config.tokenBudget.error}). Consider splitting or condensing.`
|
|
70
|
+
});
|
|
71
|
+
} else if (estimatedTokens > config.tokenBudget.warn) {
|
|
72
|
+
findings.push({
|
|
73
|
+
file: filePath,
|
|
74
|
+
rule: "check:token-budget",
|
|
75
|
+
line: 1,
|
|
76
|
+
column: 1,
|
|
77
|
+
severity: "warning",
|
|
78
|
+
message: `File is ~${estimatedTokens} tokens (warn threshold: ${config.tokenBudget.warn}). Consider condensing.`
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return findings;
|
|
82
|
+
}
|
|
83
|
+
function checkVague(parsed, filePath, config) {
|
|
84
|
+
const findings = [];
|
|
85
|
+
for (let i = 0; i < parsed.lines.length; i++) {
|
|
86
|
+
const line = parsed.lines[i].toLowerCase();
|
|
87
|
+
for (const pattern of config.vaguePatterns) {
|
|
88
|
+
if (line.includes(pattern.toLowerCase())) {
|
|
89
|
+
findings.push({
|
|
90
|
+
file: filePath,
|
|
91
|
+
rule: "check:vague",
|
|
92
|
+
line: i + 1,
|
|
93
|
+
column: line.indexOf(pattern.toLowerCase()) + 1,
|
|
94
|
+
severity: "warning",
|
|
95
|
+
message: `Vague instruction: "${pattern}". Replace with specific, actionable guidance.`
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return findings;
|
|
101
|
+
}
|
|
102
|
+
function checkRequiredSections(parsed, filePath, config) {
|
|
103
|
+
const findings = [];
|
|
104
|
+
const normalizedSections = parsed.sections.map((s) => s.toLowerCase());
|
|
105
|
+
for (const required of config.requiredSections) {
|
|
106
|
+
const found = normalizedSections.some(
|
|
107
|
+
(s) => s.includes(required.toLowerCase())
|
|
108
|
+
);
|
|
109
|
+
if (!found) {
|
|
110
|
+
findings.push({
|
|
111
|
+
file: filePath,
|
|
112
|
+
rule: "check:required-sections",
|
|
113
|
+
line: 1,
|
|
114
|
+
column: 1,
|
|
115
|
+
severity: "warning",
|
|
116
|
+
message: `Missing recommended section: "${required}"`
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return findings;
|
|
121
|
+
}
|
|
122
|
+
function checkStaleDates(parsed, filePath, config) {
|
|
123
|
+
const findings = [];
|
|
124
|
+
const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
|
|
125
|
+
const threshold = currentYear - config.staleDateYears;
|
|
126
|
+
const yearPattern = /\b(20[0-9]{2})\b/g;
|
|
127
|
+
for (let i = 0; i < parsed.lines.length; i++) {
|
|
128
|
+
let match;
|
|
129
|
+
yearPattern.lastIndex = 0;
|
|
130
|
+
while ((match = yearPattern.exec(parsed.lines[i])) !== null) {
|
|
131
|
+
const year = parseInt(match[1], 10);
|
|
132
|
+
if (year < threshold) {
|
|
133
|
+
findings.push({
|
|
134
|
+
file: filePath,
|
|
135
|
+
rule: "check:stale-dates",
|
|
136
|
+
line: i + 1,
|
|
137
|
+
column: match.index + 1,
|
|
138
|
+
severity: "warning",
|
|
139
|
+
message: `Possibly stale year reference: ${year} (older than ${config.staleDateYears} years)`
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
146
|
+
function checkContradictions(parsed, filePath) {
|
|
147
|
+
const findings = [];
|
|
148
|
+
const contradictionPairs = [
|
|
149
|
+
[
|
|
150
|
+
/\balways use (\w+)/i,
|
|
151
|
+
/\bnever use (\w+)/i,
|
|
152
|
+
'Contradictory "always use" and "never use" directives'
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
/\bdo not (?:use|add|include) (comments|docstrings|type annotations)/i,
|
|
156
|
+
/\b(?:always|must) (?:add|include|write) \1/i,
|
|
157
|
+
"Contradictory directives about adding/not adding"
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
/\bprefer (\w+) over (\w+)/i,
|
|
161
|
+
/\bprefer \2 over \1/i,
|
|
162
|
+
"Contradictory preference directives"
|
|
163
|
+
]
|
|
164
|
+
];
|
|
165
|
+
const lineTexts = parsed.lines;
|
|
166
|
+
for (const [patternA, patternB, message] of contradictionPairs) {
|
|
167
|
+
const matchesA = [];
|
|
168
|
+
const matchesB = [];
|
|
169
|
+
for (let i = 0; i < lineTexts.length; i++) {
|
|
170
|
+
const lineText = lineTexts[i];
|
|
171
|
+
const a = patternA.exec(lineText);
|
|
172
|
+
if (a) matchesA.push({ line: i + 1, match: a });
|
|
173
|
+
const b = patternB.exec(lineText);
|
|
174
|
+
if (b) matchesB.push({ line: i + 1, match: b });
|
|
175
|
+
}
|
|
176
|
+
if (matchesA.length > 0 && matchesB.length > 0) {
|
|
177
|
+
for (const a of matchesA) {
|
|
178
|
+
for (const b of matchesB) {
|
|
179
|
+
if (a.match[1] && b.match[1] && a.match[1].toLowerCase() === b.match[1].toLowerCase()) {
|
|
180
|
+
findings.push({
|
|
181
|
+
file: filePath,
|
|
182
|
+
rule: "check:contradictions",
|
|
183
|
+
line: b.line,
|
|
184
|
+
column: 1,
|
|
185
|
+
severity: "warning",
|
|
186
|
+
message: `${message} (conflicts with line ${a.line})`
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return findings;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/config.ts
|
|
197
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
198
|
+
import { resolve as resolve2 } from "path";
|
|
199
|
+
|
|
200
|
+
// src/types.ts
|
|
201
|
+
var DEFAULT_CONFIG = {
|
|
202
|
+
tokenBudget: { warn: 2e3, error: 5e3 },
|
|
203
|
+
requiredSections: ["Setup", "Testing", "Build"],
|
|
204
|
+
staleDateYears: 2,
|
|
205
|
+
vaguePatterns: [
|
|
206
|
+
"follow best practices",
|
|
207
|
+
"be careful",
|
|
208
|
+
"use good judgment",
|
|
209
|
+
"use common sense",
|
|
210
|
+
"as appropriate",
|
|
211
|
+
"when necessary",
|
|
212
|
+
"if needed",
|
|
213
|
+
"as needed",
|
|
214
|
+
"handle edge cases",
|
|
215
|
+
"write clean code",
|
|
216
|
+
"keep it simple",
|
|
217
|
+
"use proper",
|
|
218
|
+
"ensure quality"
|
|
219
|
+
],
|
|
220
|
+
ignore: []
|
|
221
|
+
};
|
|
222
|
+
var CONTEXT_FILE_NAMES = [
|
|
223
|
+
"CLAUDE.md",
|
|
224
|
+
"AGENTS.md",
|
|
225
|
+
".cursorrules",
|
|
226
|
+
"copilot-instructions.md",
|
|
227
|
+
".github/copilot-instructions.md"
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
// src/config.ts
|
|
231
|
+
function loadConfig(cwd) {
|
|
232
|
+
const configPath = resolve2(cwd, ".agent-context-lint.json");
|
|
233
|
+
if (existsSync2(configPath)) {
|
|
234
|
+
try {
|
|
235
|
+
const raw = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
236
|
+
return mergeConfig(raw);
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const pkgPath = resolve2(cwd, "package.json");
|
|
241
|
+
if (existsSync2(pkgPath)) {
|
|
242
|
+
try {
|
|
243
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
244
|
+
if (pkg.agentContextLint) {
|
|
245
|
+
return mergeConfig(pkg.agentContextLint);
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { ...DEFAULT_CONFIG };
|
|
251
|
+
}
|
|
252
|
+
function mergeConfig(overrides) {
|
|
253
|
+
return {
|
|
254
|
+
tokenBudget: {
|
|
255
|
+
...DEFAULT_CONFIG.tokenBudget,
|
|
256
|
+
...overrides.tokenBudget
|
|
257
|
+
},
|
|
258
|
+
requiredSections: overrides.requiredSections ?? DEFAULT_CONFIG.requiredSections,
|
|
259
|
+
staleDateYears: overrides.staleDateYears ?? DEFAULT_CONFIG.staleDateYears,
|
|
260
|
+
vaguePatterns: overrides.vaguePatterns ?? DEFAULT_CONFIG.vaguePatterns,
|
|
261
|
+
ignore: overrides.ignore ?? DEFAULT_CONFIG.ignore
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/discovery.ts
|
|
266
|
+
import { existsSync as existsSync3 } from "fs";
|
|
267
|
+
import { resolve as resolve3 } from "path";
|
|
268
|
+
function discoverContextFiles(cwd) {
|
|
269
|
+
const found = [];
|
|
270
|
+
for (const name of CONTEXT_FILE_NAMES) {
|
|
271
|
+
const fullPath = resolve3(cwd, name);
|
|
272
|
+
if (existsSync3(fullPath)) {
|
|
273
|
+
found.push(fullPath);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return found;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/parser.ts
|
|
280
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
281
|
+
var PATH_PATTERN = /(?:^|\s|`)(\.?\.?\/[\w./@-]+[\w/@-])/g;
|
|
282
|
+
var COMMAND_PATTERN = /(?:npm|npx|pnpm|yarn|bun|bunx)\s+(?:run\s+)?[\w:@./-]+/g;
|
|
283
|
+
var HEADING_PATTERN = /^#{1,6}\s+(.+)$/;
|
|
284
|
+
var FENCED_BLOCK_START = /^```(\w*)/;
|
|
285
|
+
var FENCED_BLOCK_END = /^```\s*$/;
|
|
286
|
+
var INLINE_CODE_PATTERN = /`([^`]+)`/g;
|
|
287
|
+
function parseFile(filePath) {
|
|
288
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
289
|
+
const lines = content.split("\n");
|
|
290
|
+
const paths = [];
|
|
291
|
+
const commands = [];
|
|
292
|
+
const sections = [];
|
|
293
|
+
const codeBlocks = [];
|
|
294
|
+
const inlineCode = [];
|
|
295
|
+
let inCodeBlock = false;
|
|
296
|
+
let codeBlockLang = "";
|
|
297
|
+
let codeBlockContent = "";
|
|
298
|
+
let codeBlockStart = 0;
|
|
299
|
+
for (let i = 0; i < lines.length; i++) {
|
|
300
|
+
const line = lines[i];
|
|
301
|
+
const lineNum = i + 1;
|
|
302
|
+
if (!inCodeBlock) {
|
|
303
|
+
const blockStart = FENCED_BLOCK_START.exec(line);
|
|
304
|
+
if (blockStart && line.trimStart().startsWith("```")) {
|
|
305
|
+
inCodeBlock = true;
|
|
306
|
+
codeBlockLang = blockStart[1] || "";
|
|
307
|
+
codeBlockContent = "";
|
|
308
|
+
codeBlockStart = lineNum;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
if (FENCED_BLOCK_END.test(line) && line.trimStart() === "```") {
|
|
313
|
+
codeBlocks.push({
|
|
314
|
+
content: codeBlockContent,
|
|
315
|
+
lang: codeBlockLang,
|
|
316
|
+
line: codeBlockStart
|
|
317
|
+
});
|
|
318
|
+
inCodeBlock = false;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
codeBlockContent += (codeBlockContent ? "\n" : "") + line;
|
|
322
|
+
}
|
|
323
|
+
const headingMatch = HEADING_PATTERN.exec(line);
|
|
324
|
+
if (headingMatch) {
|
|
325
|
+
sections.push(headingMatch[1].trim());
|
|
326
|
+
}
|
|
327
|
+
let pathMatch;
|
|
328
|
+
PATH_PATTERN.lastIndex = 0;
|
|
329
|
+
while ((pathMatch = PATH_PATTERN.exec(line)) !== null) {
|
|
330
|
+
const value = pathMatch[1];
|
|
331
|
+
if (value.includes("://")) continue;
|
|
332
|
+
paths.push({
|
|
333
|
+
value,
|
|
334
|
+
line: lineNum,
|
|
335
|
+
column: pathMatch.index + (pathMatch[0].length - value.length) + 1
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
let cmdMatch;
|
|
339
|
+
COMMAND_PATTERN.lastIndex = 0;
|
|
340
|
+
while ((cmdMatch = COMMAND_PATTERN.exec(line)) !== null) {
|
|
341
|
+
commands.push({
|
|
342
|
+
value: cmdMatch[0],
|
|
343
|
+
line: lineNum,
|
|
344
|
+
column: cmdMatch.index + 1
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (!inCodeBlock) {
|
|
348
|
+
let inlineMatch;
|
|
349
|
+
INLINE_CODE_PATTERN.lastIndex = 0;
|
|
350
|
+
while ((inlineMatch = INLINE_CODE_PATTERN.exec(line)) !== null) {
|
|
351
|
+
inlineCode.push({
|
|
352
|
+
content: inlineMatch[1],
|
|
353
|
+
line: lineNum,
|
|
354
|
+
column: inlineMatch.index + 2
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { content, lines, paths, commands, sections, codeBlocks, inlineCode };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/scorer.ts
|
|
363
|
+
function computeScore(findings) {
|
|
364
|
+
let score = 100;
|
|
365
|
+
for (const finding of findings) {
|
|
366
|
+
if (finding.severity === "error") {
|
|
367
|
+
score -= 15;
|
|
368
|
+
} else {
|
|
369
|
+
score -= 5;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return Math.max(0, score);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/reporter.ts
|
|
376
|
+
import { relative } from "path";
|
|
377
|
+
function formatText(result, cwd) {
|
|
378
|
+
const lines = [];
|
|
379
|
+
for (const file of result.files) {
|
|
380
|
+
const relPath = relative(cwd, file.file);
|
|
381
|
+
lines.push(`
|
|
382
|
+
${relPath} (score: ${file.score}/100)`);
|
|
383
|
+
if (file.findings.length === 0) {
|
|
384
|
+
lines.push(" No issues found.");
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
for (const f of file.findings) {
|
|
388
|
+
const icon = f.severity === "error" ? "x" : "!";
|
|
389
|
+
lines.push(
|
|
390
|
+
` ${f.line}:${f.column} ${icon} ${f.message} [${f.rule}]`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
lines.push("");
|
|
395
|
+
lines.push(
|
|
396
|
+
` ${result.totalFindings} problems (${result.errors} errors, ${result.warnings} warnings)`
|
|
397
|
+
);
|
|
398
|
+
lines.push("");
|
|
399
|
+
return lines.join("\n");
|
|
400
|
+
}
|
|
401
|
+
function formatJson(result, cwd) {
|
|
402
|
+
const output = {
|
|
403
|
+
...result,
|
|
404
|
+
files: result.files.map((f) => ({
|
|
405
|
+
...f,
|
|
406
|
+
file: relative(cwd, f.file)
|
|
407
|
+
}))
|
|
408
|
+
};
|
|
409
|
+
return JSON.stringify(output, null, 2);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/index.ts
|
|
413
|
+
function lintFile(filePath, cwd) {
|
|
414
|
+
const config = loadConfig(cwd);
|
|
415
|
+
const parsed = parseFile(filePath);
|
|
416
|
+
const findings = [
|
|
417
|
+
...checkPaths(parsed, filePath),
|
|
418
|
+
...checkScripts(parsed, filePath),
|
|
419
|
+
...checkTokenBudget(parsed, filePath, config),
|
|
420
|
+
...checkVague(parsed, filePath, config),
|
|
421
|
+
...checkRequiredSections(parsed, filePath, config),
|
|
422
|
+
...checkStaleDates(parsed, filePath, config),
|
|
423
|
+
...checkContradictions(parsed, filePath)
|
|
424
|
+
];
|
|
425
|
+
return {
|
|
426
|
+
file: filePath,
|
|
427
|
+
findings,
|
|
428
|
+
score: computeScore(findings)
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function lint(cwd, files) {
|
|
432
|
+
const targetFiles = files && files.length > 0 ? files.map((f) => resolve4(cwd, f)) : discoverContextFiles(cwd);
|
|
433
|
+
if (targetFiles.length === 0) {
|
|
434
|
+
return { files: [], totalFindings: 0, errors: 0, warnings: 0 };
|
|
435
|
+
}
|
|
436
|
+
const results = targetFiles.map((f) => lintFile(f, cwd));
|
|
437
|
+
const totalFindings = results.reduce(
|
|
438
|
+
(sum, r) => sum + r.findings.length,
|
|
439
|
+
0
|
|
440
|
+
);
|
|
441
|
+
const errors = results.reduce(
|
|
442
|
+
(sum, r) => sum + r.findings.filter((f) => f.severity === "error").length,
|
|
443
|
+
0
|
|
444
|
+
);
|
|
445
|
+
const warnings = results.reduce(
|
|
446
|
+
(sum, r) => sum + r.findings.filter((f) => f.severity === "warning").length,
|
|
447
|
+
0
|
|
448
|
+
);
|
|
449
|
+
return { files: results, totalFindings, errors, warnings };
|
|
450
|
+
}
|
|
451
|
+
export {
|
|
452
|
+
computeScore,
|
|
453
|
+
discoverContextFiles,
|
|
454
|
+
formatJson,
|
|
455
|
+
formatText,
|
|
456
|
+
lint,
|
|
457
|
+
lintFile,
|
|
458
|
+
loadConfig,
|
|
459
|
+
parseFile
|
|
460
|
+
};
|
|
461
|
+
//# sourceMappingURL=index.js.map
|