agent-context-lint 0.1.0 → 0.1.1
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 +26 -5
- package/action.yml +14 -0
- package/dist/cli.js +211 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +169 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +168 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
computeScore: () => computeScore,
|
|
24
24
|
discoverContextFiles: () => discoverContextFiles,
|
|
25
|
+
fixFile: () => fixFile,
|
|
25
26
|
formatJson: () => formatJson,
|
|
26
27
|
formatText: () => formatText,
|
|
27
28
|
lint: () => lint,
|
|
@@ -33,6 +34,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
33
34
|
var import_node_path5 = require("path");
|
|
34
35
|
|
|
35
36
|
// src/checkers.ts
|
|
37
|
+
var import_node_child_process = require("child_process");
|
|
36
38
|
var import_node_fs = require("fs");
|
|
37
39
|
var import_node_path = require("path");
|
|
38
40
|
function checkPaths(parsed, filePath) {
|
|
@@ -223,6 +225,134 @@ function checkContradictions(parsed, filePath) {
|
|
|
223
225
|
}
|
|
224
226
|
return findings;
|
|
225
227
|
}
|
|
228
|
+
var SHELL_BUILTINS = /* @__PURE__ */ new Set([
|
|
229
|
+
"cd",
|
|
230
|
+
"export",
|
|
231
|
+
"echo",
|
|
232
|
+
"source",
|
|
233
|
+
"set",
|
|
234
|
+
"unset",
|
|
235
|
+
"alias",
|
|
236
|
+
"unalias",
|
|
237
|
+
"type",
|
|
238
|
+
"readonly",
|
|
239
|
+
"declare",
|
|
240
|
+
"local",
|
|
241
|
+
"eval",
|
|
242
|
+
"exec",
|
|
243
|
+
"trap",
|
|
244
|
+
"return",
|
|
245
|
+
"exit",
|
|
246
|
+
"shift",
|
|
247
|
+
"wait",
|
|
248
|
+
"read",
|
|
249
|
+
"pushd",
|
|
250
|
+
"popd",
|
|
251
|
+
"dirs",
|
|
252
|
+
"ulimit",
|
|
253
|
+
"umask",
|
|
254
|
+
"getopts",
|
|
255
|
+
"hash",
|
|
256
|
+
"pwd",
|
|
257
|
+
"test",
|
|
258
|
+
"true",
|
|
259
|
+
"false",
|
|
260
|
+
"printf",
|
|
261
|
+
"let",
|
|
262
|
+
"if",
|
|
263
|
+
"then",
|
|
264
|
+
"else",
|
|
265
|
+
"fi",
|
|
266
|
+
"for",
|
|
267
|
+
"do",
|
|
268
|
+
"done",
|
|
269
|
+
"while",
|
|
270
|
+
"until",
|
|
271
|
+
"case",
|
|
272
|
+
"esac",
|
|
273
|
+
"in",
|
|
274
|
+
"function"
|
|
275
|
+
]);
|
|
276
|
+
var SHELL_LANGS = /* @__PURE__ */ new Set(["bash", "sh", "shell"]);
|
|
277
|
+
function checkCommands(parsed, filePath) {
|
|
278
|
+
const findings = [];
|
|
279
|
+
const cache = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const block of parsed.codeBlocks) {
|
|
281
|
+
if (!SHELL_LANGS.has(block.lang)) continue;
|
|
282
|
+
const lines = block.content.split("\n");
|
|
283
|
+
for (let i = 0; i < lines.length; i++) {
|
|
284
|
+
let line = lines[i].trim();
|
|
285
|
+
if (!line || line.startsWith("#")) continue;
|
|
286
|
+
if (line.startsWith("$ ") || line.startsWith("> ")) {
|
|
287
|
+
line = line.slice(2).trim();
|
|
288
|
+
}
|
|
289
|
+
if (!line) continue;
|
|
290
|
+
const cmd = line.split(/\s/)[0];
|
|
291
|
+
if (!cmd || SHELL_BUILTINS.has(cmd)) continue;
|
|
292
|
+
if (/^[A-Z_]+=/.test(cmd)) continue;
|
|
293
|
+
if (!cache.has(cmd)) {
|
|
294
|
+
try {
|
|
295
|
+
(0, import_node_child_process.execFileSync)("which", [cmd], { stdio: "pipe" });
|
|
296
|
+
cache.set(cmd, true);
|
|
297
|
+
} catch {
|
|
298
|
+
cache.set(cmd, false);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!cache.get(cmd)) {
|
|
302
|
+
findings.push({
|
|
303
|
+
file: filePath,
|
|
304
|
+
rule: "check:commands",
|
|
305
|
+
line: block.line + 1 + i,
|
|
306
|
+
column: 1,
|
|
307
|
+
severity: "warning",
|
|
308
|
+
message: `Command not found on system: "${cmd}"`
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return findings;
|
|
314
|
+
}
|
|
315
|
+
var JS_LANGS = /* @__PURE__ */ new Set(["ts", "js", "typescript", "javascript", "tsx", "jsx"]);
|
|
316
|
+
var IMPORT_FROM_RE = /(?:import\s+.*?\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
|
|
317
|
+
var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cjs"];
|
|
318
|
+
function resolveModule(base, importPath) {
|
|
319
|
+
const resolved = (0, import_node_path.resolve)(base, importPath);
|
|
320
|
+
if ((0, import_node_fs.existsSync)(resolved) && !resolved.endsWith("/")) return true;
|
|
321
|
+
for (const ext of EXTENSIONS) {
|
|
322
|
+
if ((0, import_node_fs.existsSync)(resolved + ext)) return true;
|
|
323
|
+
}
|
|
324
|
+
for (const ext of EXTENSIONS) {
|
|
325
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.resolve)(resolved, "index" + ext))) return true;
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
function checkImports(parsed, filePath) {
|
|
330
|
+
const findings = [];
|
|
331
|
+
const baseDir = (0, import_node_path.dirname)(filePath);
|
|
332
|
+
for (const block of parsed.codeBlocks) {
|
|
333
|
+
if (!JS_LANGS.has(block.lang)) continue;
|
|
334
|
+
const lines = block.content.split("\n");
|
|
335
|
+
for (let i = 0; i < lines.length; i++) {
|
|
336
|
+
let match;
|
|
337
|
+
IMPORT_FROM_RE.lastIndex = 0;
|
|
338
|
+
while ((match = IMPORT_FROM_RE.exec(lines[i])) !== null) {
|
|
339
|
+
const importPath = match[1] || match[2];
|
|
340
|
+
if (!importPath.startsWith("./") && !importPath.startsWith("../")) continue;
|
|
341
|
+
if (!resolveModule(baseDir, importPath)) {
|
|
342
|
+
findings.push({
|
|
343
|
+
file: filePath,
|
|
344
|
+
rule: "check:imports",
|
|
345
|
+
line: block.line + 1 + i,
|
|
346
|
+
column: match.index + 1,
|
|
347
|
+
severity: "error",
|
|
348
|
+
message: `Import path does not resolve: "${importPath}"`
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return findings;
|
|
355
|
+
}
|
|
226
356
|
|
|
227
357
|
// src/config.ts
|
|
228
358
|
var import_node_fs2 = require("fs");
|
|
@@ -440,6 +570,41 @@ function formatJson(result, cwd) {
|
|
|
440
570
|
return JSON.stringify(output, null, 2);
|
|
441
571
|
}
|
|
442
572
|
|
|
573
|
+
// src/fixer.ts
|
|
574
|
+
var import_node_fs5 = require("fs");
|
|
575
|
+
function fixFile(filePath) {
|
|
576
|
+
const original = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
|
|
577
|
+
const changes = [];
|
|
578
|
+
let content = original;
|
|
579
|
+
const lines = content.split("\n");
|
|
580
|
+
for (let i = 0; i < lines.length; i++) {
|
|
581
|
+
if (lines[i] !== lines[i].trimEnd()) {
|
|
582
|
+
changes.push({ line: i + 1, description: "Removed trailing whitespace" });
|
|
583
|
+
lines[i] = lines[i].trimEnd();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
content = lines.join("\n");
|
|
587
|
+
const collapsed = content.replace(/\n{3,}/g, (match, offset) => {
|
|
588
|
+
const before = content.slice(0, offset);
|
|
589
|
+
const lineNum = before.split("\n").length + 1;
|
|
590
|
+
changes.push({ line: lineNum, description: "Collapsed multiple blank lines" });
|
|
591
|
+
return "\n\n";
|
|
592
|
+
});
|
|
593
|
+
content = collapsed;
|
|
594
|
+
if (content.length > 0 && !content.endsWith("\n")) {
|
|
595
|
+
changes.push({
|
|
596
|
+
line: content.split("\n").length,
|
|
597
|
+
description: "Added trailing newline"
|
|
598
|
+
});
|
|
599
|
+
content += "\n";
|
|
600
|
+
}
|
|
601
|
+
const fixed = content !== original;
|
|
602
|
+
if (fixed) {
|
|
603
|
+
(0, import_node_fs5.writeFileSync)(filePath, content);
|
|
604
|
+
}
|
|
605
|
+
return { file: filePath, fixed, changes };
|
|
606
|
+
}
|
|
607
|
+
|
|
443
608
|
// src/index.ts
|
|
444
609
|
function lintFile(filePath, cwd) {
|
|
445
610
|
const config = loadConfig(cwd);
|
|
@@ -451,7 +616,9 @@ function lintFile(filePath, cwd) {
|
|
|
451
616
|
...checkVague(parsed, filePath, config),
|
|
452
617
|
...checkRequiredSections(parsed, filePath, config),
|
|
453
618
|
...checkStaleDates(parsed, filePath, config),
|
|
454
|
-
...checkContradictions(parsed, filePath)
|
|
619
|
+
...checkContradictions(parsed, filePath),
|
|
620
|
+
...checkCommands(parsed, filePath),
|
|
621
|
+
...checkImports(parsed, filePath)
|
|
455
622
|
];
|
|
456
623
|
return {
|
|
457
624
|
file: filePath,
|
|
@@ -483,6 +650,7 @@ function lint(cwd, files) {
|
|
|
483
650
|
0 && (module.exports = {
|
|
484
651
|
computeScore,
|
|
485
652
|
discoverContextFiles,
|
|
653
|
+
fixFile,
|
|
486
654
|
formatJson,
|
|
487
655
|
formatText,
|
|
488
656
|
lint,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/checkers.ts","../src/config.ts","../src/types.ts","../src/discovery.ts","../src/parser.ts","../src/scorer.ts","../src/reporter.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport {\n checkContradictions,\n checkPaths,\n checkRequiredSections,\n checkScripts,\n checkStaleDates,\n checkTokenBudget,\n checkVague,\n} from './checkers.js';\nimport { loadConfig } from './config.js';\nimport { discoverContextFiles } from './discovery.js';\nimport { parseFile } from './parser.js';\nimport { computeScore } from './scorer.js';\nimport type { FileResult, LintFinding, LintResult } from './types.js';\n\nexport type { CLIOptions, Config, FileResult, LintFinding, LintResult } from './types.js';\nexport { discoverContextFiles } from './discovery.js';\nexport { parseFile } from './parser.js';\nexport { computeScore } from './scorer.js';\nexport { loadConfig } from './config.js';\nexport { formatJson, formatText } from './reporter.js';\n\nexport function lintFile(filePath: string, cwd: string): FileResult {\n const config = loadConfig(cwd);\n const parsed = parseFile(filePath);\n\n const findings: LintFinding[] = [\n ...checkPaths(parsed, filePath),\n ...checkScripts(parsed, filePath),\n ...checkTokenBudget(parsed, filePath, config),\n ...checkVague(parsed, filePath, config),\n ...checkRequiredSections(parsed, filePath, config),\n ...checkStaleDates(parsed, filePath, config),\n ...checkContradictions(parsed, filePath),\n ];\n\n return {\n file: filePath,\n findings,\n score: computeScore(findings),\n };\n}\n\nexport function lint(cwd: string, files?: string[]): LintResult {\n const targetFiles =\n files && files.length > 0\n ? files.map((f) => resolve(cwd, f))\n : discoverContextFiles(cwd);\n\n if (targetFiles.length === 0) {\n return { files: [], totalFindings: 0, errors: 0, warnings: 0 };\n }\n\n const results = targetFiles.map((f) => lintFile(f, cwd));\n\n const totalFindings = results.reduce(\n (sum, r) => sum + r.findings.length,\n 0,\n );\n const errors = results.reduce(\n (sum, r) => sum + r.findings.filter((f) => f.severity === 'error').length,\n 0,\n );\n const warnings = results.reduce(\n (sum, r) =>\n sum + r.findings.filter((f) => f.severity === 'warning').length,\n 0,\n );\n\n return { files: results, totalFindings, errors, warnings };\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport type { ParsedFile } from './parser.js';\nimport type { Config, LintFinding } from './types.js';\n\nexport function checkPaths(\n parsed: ParsedFile,\n filePath: string,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const baseDir = dirname(filePath);\n\n for (const ref of parsed.paths) {\n const resolved = resolve(baseDir, ref.value);\n if (!existsSync(resolved)) {\n findings.push({\n file: filePath,\n rule: 'check:paths',\n line: ref.line,\n column: ref.column,\n severity: 'error',\n message: `Path does not exist: ${ref.value}`,\n });\n }\n }\n\n return findings;\n}\n\nexport function checkScripts(\n parsed: ParsedFile,\n filePath: string,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const baseDir = dirname(filePath);\n const pkgPath = resolve(baseDir, 'package.json');\n\n let scripts: Record<string, string> = {};\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n scripts = pkg.scripts || {};\n } catch {\n // Invalid package.json — skip script checks\n return findings;\n }\n } else {\n // No package.json — skip script checks\n return findings;\n }\n\n for (const cmd of parsed.commands) {\n // Extract the script name from commands like \"npm run test\", \"pnpm build\"\n const match = /(?:npm|pnpm|yarn|bun)\\s+run\\s+([\\w:@./-]+)/.exec(cmd.value);\n const directMatch = /(?:npm|pnpm|yarn|bun)\\s+(test|start|build|lint)\\b/.exec(\n cmd.value,\n );\n\n const scriptName = match?.[1] || directMatch?.[1];\n if (scriptName && !(scriptName in scripts)) {\n findings.push({\n file: filePath,\n rule: 'check:scripts',\n line: cmd.line,\n column: cmd.column,\n severity: 'error',\n message: `Script not found in package.json: \"${scriptName}\"`,\n });\n }\n }\n\n return findings;\n}\n\nexport function checkTokenBudget(\n parsed: ParsedFile,\n filePath: string,\n config: Config,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n\n // Rough token estimate: ~4 chars per token for English text\n const estimatedTokens = Math.ceil(parsed.content.length / 4);\n\n if (estimatedTokens > config.tokenBudget.error) {\n findings.push({\n file: filePath,\n rule: 'check:token-budget',\n line: 1,\n column: 1,\n severity: 'error',\n message: `File is ~${estimatedTokens} tokens (limit: ${config.tokenBudget.error}). Consider splitting or condensing.`,\n });\n } else if (estimatedTokens > config.tokenBudget.warn) {\n findings.push({\n file: filePath,\n rule: 'check:token-budget',\n line: 1,\n column: 1,\n severity: 'warning',\n message: `File is ~${estimatedTokens} tokens (warn threshold: ${config.tokenBudget.warn}). Consider condensing.`,\n });\n }\n\n return findings;\n}\n\nexport function checkVague(\n parsed: ParsedFile,\n filePath: string,\n config: Config,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n\n for (let i = 0; i < parsed.lines.length; i++) {\n const line = parsed.lines[i].toLowerCase();\n for (const pattern of config.vaguePatterns) {\n if (line.includes(pattern.toLowerCase())) {\n findings.push({\n file: filePath,\n rule: 'check:vague',\n line: i + 1,\n column: line.indexOf(pattern.toLowerCase()) + 1,\n severity: 'warning',\n message: `Vague instruction: \"${pattern}\". Replace with specific, actionable guidance.`,\n });\n }\n }\n }\n\n return findings;\n}\n\nexport function checkRequiredSections(\n parsed: ParsedFile,\n filePath: string,\n config: Config,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const normalizedSections = parsed.sections.map((s) => s.toLowerCase());\n\n for (const required of config.requiredSections) {\n const found = normalizedSections.some(\n (s) => s.includes(required.toLowerCase()),\n );\n if (!found) {\n findings.push({\n file: filePath,\n rule: 'check:required-sections',\n line: 1,\n column: 1,\n severity: 'warning',\n message: `Missing recommended section: \"${required}\"`,\n });\n }\n }\n\n return findings;\n}\n\nexport function checkStaleDates(\n parsed: ParsedFile,\n filePath: string,\n config: Config,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const currentYear = new Date().getFullYear();\n const threshold = currentYear - config.staleDateYears;\n\n const yearPattern = /\\b(20[0-9]{2})\\b/g;\n\n for (let i = 0; i < parsed.lines.length; i++) {\n let match: RegExpExecArray | null;\n yearPattern.lastIndex = 0;\n while ((match = yearPattern.exec(parsed.lines[i])) !== null) {\n const year = parseInt(match[1], 10);\n if (year < threshold) {\n findings.push({\n file: filePath,\n rule: 'check:stale-dates',\n line: i + 1,\n column: match.index + 1,\n severity: 'warning',\n message: `Possibly stale year reference: ${year} (older than ${config.staleDateYears} years)`,\n });\n }\n }\n }\n\n return findings;\n}\n\nexport function checkContradictions(\n parsed: ParsedFile,\n filePath: string,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n\n // Contradiction pairs: if both patterns appear, flag them\n const contradictionPairs: [RegExp, RegExp, string][] = [\n [\n /\\balways use (\\w+)/i,\n /\\bnever use (\\w+)/i,\n 'Contradictory \"always use\" and \"never use\" directives',\n ],\n [\n /\\bdo not (?:use|add|include) (comments|docstrings|type annotations)/i,\n /\\b(?:always|must) (?:add|include|write) \\1/i,\n 'Contradictory directives about adding/not adding',\n ],\n [\n /\\bprefer (\\w+) over (\\w+)/i,\n /\\bprefer \\2 over \\1/i,\n 'Contradictory preference directives',\n ],\n ];\n\n const lineTexts = parsed.lines;\n\n for (const [patternA, patternB, message] of contradictionPairs) {\n const matchesA: { line: number; match: RegExpExecArray }[] = [];\n const matchesB: { line: number; match: RegExpExecArray }[] = [];\n\n for (let i = 0; i < lineTexts.length; i++) {\n const lineText = lineTexts[i];\n const a = patternA.exec(lineText);\n if (a) matchesA.push({ line: i + 1, match: a });\n const b = patternB.exec(lineText);\n if (b) matchesB.push({ line: i + 1, match: b });\n }\n\n if (matchesA.length > 0 && matchesB.length > 0) {\n // Check if contradictions reference the same term\n for (const a of matchesA) {\n for (const b of matchesB) {\n if (\n a.match[1] &&\n b.match[1] &&\n a.match[1].toLowerCase() === b.match[1].toLowerCase()\n ) {\n findings.push({\n file: filePath,\n rule: 'check:contradictions',\n line: b.line,\n column: 1,\n severity: 'warning',\n message: `${message} (conflicts with line ${a.line})`,\n });\n }\n }\n }\n }\n }\n\n return findings;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { DEFAULT_CONFIG, type Config } from './types.js';\n\nexport function loadConfig(cwd: string): Config {\n // Try .agent-context-lint.json\n const configPath = resolve(cwd, '.agent-context-lint.json');\n if (existsSync(configPath)) {\n try {\n const raw = JSON.parse(readFileSync(configPath, 'utf-8'));\n return mergeConfig(raw);\n } catch {\n // Invalid config file — use defaults\n }\n }\n\n // Try package.json agentContextLint key\n const pkgPath = resolve(cwd, 'package.json');\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n if (pkg.agentContextLint) {\n return mergeConfig(pkg.agentContextLint);\n }\n } catch {\n // Invalid package.json — use defaults\n }\n }\n\n return { ...DEFAULT_CONFIG };\n}\n\nfunction mergeConfig(overrides: Partial<Config>): Config {\n return {\n tokenBudget: {\n ...DEFAULT_CONFIG.tokenBudget,\n ...overrides.tokenBudget,\n },\n requiredSections:\n overrides.requiredSections ?? DEFAULT_CONFIG.requiredSections,\n staleDateYears: overrides.staleDateYears ?? DEFAULT_CONFIG.staleDateYears,\n vaguePatterns: overrides.vaguePatterns ?? DEFAULT_CONFIG.vaguePatterns,\n ignore: overrides.ignore ?? DEFAULT_CONFIG.ignore,\n };\n}\n","export interface LintFinding {\n file: string;\n rule: string;\n line: number;\n column: number;\n severity: 'error' | 'warning';\n message: string;\n}\n\nexport interface FileResult {\n file: string;\n findings: LintFinding[];\n score: number;\n}\n\nexport interface LintResult {\n files: FileResult[];\n totalFindings: number;\n errors: number;\n warnings: number;\n}\n\nexport interface CLIOptions {\n files: string[];\n format: 'text' | 'json';\n fix: boolean;\n cwd: string;\n}\n\nexport interface Config {\n tokenBudget: { warn: number; error: number };\n requiredSections: string[];\n staleDateYears: number;\n vaguePatterns: string[];\n ignore: string[];\n}\n\nexport const DEFAULT_CONFIG: Config = {\n tokenBudget: { warn: 2000, error: 5000 },\n requiredSections: ['Setup', 'Testing', 'Build'],\n staleDateYears: 2,\n vaguePatterns: [\n 'follow best practices',\n 'be careful',\n 'use good judgment',\n 'use common sense',\n 'as appropriate',\n 'when necessary',\n 'if needed',\n 'as needed',\n 'handle edge cases',\n 'write clean code',\n 'keep it simple',\n 'use proper',\n 'ensure quality',\n ],\n ignore: [],\n};\n\nexport const CONTEXT_FILE_NAMES = [\n 'CLAUDE.md',\n 'AGENTS.md',\n '.cursorrules',\n 'copilot-instructions.md',\n '.github/copilot-instructions.md',\n];\n","import { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { CONTEXT_FILE_NAMES } from './types.js';\n\nexport function discoverContextFiles(cwd: string): string[] {\n const found: string[] = [];\n for (const name of CONTEXT_FILE_NAMES) {\n const fullPath = resolve(cwd, name);\n if (existsSync(fullPath)) {\n found.push(fullPath);\n }\n }\n return found;\n}\n","import { readFileSync } from 'node:fs';\n\nexport interface ParsedFile {\n content: string;\n lines: string[];\n paths: PathReference[];\n commands: CommandReference[];\n sections: string[];\n codeBlocks: CodeBlock[];\n inlineCode: InlineCode[];\n}\n\nexport interface PathReference {\n value: string;\n line: number;\n column: number;\n}\n\nexport interface CommandReference {\n value: string;\n line: number;\n column: number;\n}\n\nexport interface CodeBlock {\n content: string;\n lang: string;\n line: number;\n}\n\nexport interface InlineCode {\n content: string;\n line: number;\n column: number;\n}\n\nconst PATH_PATTERN = /(?:^|\\s|`)(\\.?\\.?\\/[\\w./@-]+[\\w/@-])/g;\nconst COMMAND_PATTERN = /(?:npm|npx|pnpm|yarn|bun|bunx)\\s+(?:run\\s+)?[\\w:@./-]+/g;\nconst HEADING_PATTERN = /^#{1,6}\\s+(.+)$/;\nconst FENCED_BLOCK_START = /^```(\\w*)/;\nconst FENCED_BLOCK_END = /^```\\s*$/;\nconst INLINE_CODE_PATTERN = /`([^`]+)`/g;\n\nexport function parseFile(filePath: string): ParsedFile {\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n\n const paths: PathReference[] = [];\n const commands: CommandReference[] = [];\n const sections: string[] = [];\n const codeBlocks: CodeBlock[] = [];\n const inlineCode: InlineCode[] = [];\n\n let inCodeBlock = false;\n let codeBlockLang = '';\n let codeBlockContent = '';\n let codeBlockStart = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineNum = i + 1;\n\n // Track fenced code blocks\n if (!inCodeBlock) {\n const blockStart = FENCED_BLOCK_START.exec(line);\n if (blockStart && line.trimStart().startsWith('```')) {\n inCodeBlock = true;\n codeBlockLang = blockStart[1] || '';\n codeBlockContent = '';\n codeBlockStart = lineNum;\n continue;\n }\n } else {\n if (FENCED_BLOCK_END.test(line) && line.trimStart() === '```') {\n codeBlocks.push({\n content: codeBlockContent,\n lang: codeBlockLang,\n line: codeBlockStart,\n });\n inCodeBlock = false;\n continue;\n }\n codeBlockContent += (codeBlockContent ? '\\n' : '') + line;\n }\n\n // Extract headings as sections\n const headingMatch = HEADING_PATTERN.exec(line);\n if (headingMatch) {\n sections.push(headingMatch[1].trim());\n }\n\n // Extract paths from both inline code and plain text\n let pathMatch: RegExpExecArray | null;\n PATH_PATTERN.lastIndex = 0;\n while ((pathMatch = PATH_PATTERN.exec(line)) !== null) {\n const value = pathMatch[1];\n // Skip URLs\n if (value.includes('://')) continue;\n paths.push({\n value,\n line: lineNum,\n column: pathMatch.index + (pathMatch[0].length - value.length) + 1,\n });\n }\n\n // Extract npm/pnpm/yarn/bun commands\n let cmdMatch: RegExpExecArray | null;\n COMMAND_PATTERN.lastIndex = 0;\n while ((cmdMatch = COMMAND_PATTERN.exec(line)) !== null) {\n commands.push({\n value: cmdMatch[0],\n line: lineNum,\n column: cmdMatch.index + 1,\n });\n }\n\n // Extract inline code spans\n if (!inCodeBlock) {\n let inlineMatch: RegExpExecArray | null;\n INLINE_CODE_PATTERN.lastIndex = 0;\n while ((inlineMatch = INLINE_CODE_PATTERN.exec(line)) !== null) {\n inlineCode.push({\n content: inlineMatch[1],\n line: lineNum,\n column: inlineMatch.index + 2,\n });\n }\n }\n }\n\n return { content, lines, paths, commands, sections, codeBlocks, inlineCode };\n}\n","import type { LintFinding } from './types.js';\n\n/**\n * Computes a 0–100 quality score for a file based on its findings.\n *\n * Starts at 100 and deducts:\n * - 15 points per error\n * - 5 points per warning\n *\n * Minimum score is 0.\n */\nexport function computeScore(findings: LintFinding[]): number {\n let score = 100;\n for (const finding of findings) {\n if (finding.severity === 'error') {\n score -= 15;\n } else {\n score -= 5;\n }\n }\n return Math.max(0, score);\n}\n","import { relative } from 'node:path';\nimport type { LintResult } from './types.js';\n\nexport function formatText(result: LintResult, cwd: string): string {\n const lines: string[] = [];\n\n for (const file of result.files) {\n const relPath = relative(cwd, file.file);\n lines.push(`\\n ${relPath} (score: ${file.score}/100)`);\n\n if (file.findings.length === 0) {\n lines.push(' No issues found.');\n continue;\n }\n\n for (const f of file.findings) {\n const icon = f.severity === 'error' ? 'x' : '!';\n lines.push(\n ` ${f.line}:${f.column} ${icon} ${f.message} [${f.rule}]`,\n );\n }\n }\n\n lines.push('');\n lines.push(\n ` ${result.totalFindings} problems (${result.errors} errors, ${result.warnings} warnings)`,\n );\n lines.push('');\n\n return lines.join('\\n');\n}\n\nexport function formatJson(result: LintResult, cwd: string): string {\n const output = {\n ...result,\n files: result.files.map((f) => ({\n ...f,\n file: relative(cwd, f.file),\n })),\n };\n return JSON.stringify(output, null, 2);\n}\n"],"mappings":"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","names":["import_node_path","import_node_fs","import_node_path","import_node_fs","import_node_path","import_node_fs","import_node_path"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/checkers.ts","../src/config.ts","../src/types.ts","../src/discovery.ts","../src/parser.ts","../src/scorer.ts","../src/reporter.ts","../src/fixer.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport {\n checkCommands,\n checkContradictions,\n checkImports,\n checkPaths,\n checkRequiredSections,\n checkScripts,\n checkStaleDates,\n checkTokenBudget,\n checkVague,\n} from './checkers.js';\nimport { loadConfig } from './config.js';\nimport { discoverContextFiles } from './discovery.js';\nimport { parseFile } from './parser.js';\nimport { computeScore } from './scorer.js';\nimport type { FileResult, LintFinding, LintResult } from './types.js';\n\nexport type { CLIOptions, Config, FileResult, LintFinding, LintResult } from './types.js';\nexport { discoverContextFiles } from './discovery.js';\nexport { parseFile } from './parser.js';\nexport { computeScore } from './scorer.js';\nexport { loadConfig } from './config.js';\nexport { formatJson, formatText } from './reporter.js';\nexport { fixFile } from './fixer.js';\nexport type { FixResult, FixChange } from './fixer.js';\n\nexport function lintFile(filePath: string, cwd: string): FileResult {\n const config = loadConfig(cwd);\n const parsed = parseFile(filePath);\n\n const findings: LintFinding[] = [\n ...checkPaths(parsed, filePath),\n ...checkScripts(parsed, filePath),\n ...checkTokenBudget(parsed, filePath, config),\n ...checkVague(parsed, filePath, config),\n ...checkRequiredSections(parsed, filePath, config),\n ...checkStaleDates(parsed, filePath, config),\n ...checkContradictions(parsed, filePath),\n ...checkCommands(parsed, filePath),\n ...checkImports(parsed, filePath),\n ];\n\n return {\n file: filePath,\n findings,\n score: computeScore(findings),\n };\n}\n\nexport function lint(cwd: string, files?: string[]): LintResult {\n const targetFiles =\n files && files.length > 0\n ? files.map((f) => resolve(cwd, f))\n : discoverContextFiles(cwd);\n\n if (targetFiles.length === 0) {\n return { files: [], totalFindings: 0, errors: 0, warnings: 0 };\n }\n\n const results = targetFiles.map((f) => lintFile(f, cwd));\n\n const totalFindings = results.reduce(\n (sum, r) => sum + r.findings.length,\n 0,\n );\n const errors = results.reduce(\n (sum, r) => sum + r.findings.filter((f) => f.severity === 'error').length,\n 0,\n );\n const warnings = results.reduce(\n (sum, r) =>\n sum + r.findings.filter((f) => f.severity === 'warning').length,\n 0,\n );\n\n return { files: results, totalFindings, errors, warnings };\n}\n","import { execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport type { ParsedFile } from './parser.js';\nimport type { Config, LintFinding } from './types.js';\n\nexport function checkPaths(\n parsed: ParsedFile,\n filePath: string,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const baseDir = dirname(filePath);\n\n for (const ref of parsed.paths) {\n const resolved = resolve(baseDir, ref.value);\n if (!existsSync(resolved)) {\n findings.push({\n file: filePath,\n rule: 'check:paths',\n line: ref.line,\n column: ref.column,\n severity: 'error',\n message: `Path does not exist: ${ref.value}`,\n });\n }\n }\n\n return findings;\n}\n\nexport function checkScripts(\n parsed: ParsedFile,\n filePath: string,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const baseDir = dirname(filePath);\n const pkgPath = resolve(baseDir, 'package.json');\n\n let scripts: Record<string, string> = {};\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n scripts = pkg.scripts || {};\n } catch {\n // Invalid package.json — skip script checks\n return findings;\n }\n } else {\n // No package.json — skip script checks\n return findings;\n }\n\n for (const cmd of parsed.commands) {\n // Extract the script name from commands like \"npm run test\", \"pnpm build\"\n const match = /(?:npm|pnpm|yarn|bun)\\s+run\\s+([\\w:@./-]+)/.exec(cmd.value);\n const directMatch = /(?:npm|pnpm|yarn|bun)\\s+(test|start|build|lint)\\b/.exec(\n cmd.value,\n );\n\n const scriptName = match?.[1] || directMatch?.[1];\n if (scriptName && !(scriptName in scripts)) {\n findings.push({\n file: filePath,\n rule: 'check:scripts',\n line: cmd.line,\n column: cmd.column,\n severity: 'error',\n message: `Script not found in package.json: \"${scriptName}\"`,\n });\n }\n }\n\n return findings;\n}\n\nexport function checkTokenBudget(\n parsed: ParsedFile,\n filePath: string,\n config: Config,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n\n // Rough token estimate: ~4 chars per token for English text\n const estimatedTokens = Math.ceil(parsed.content.length / 4);\n\n if (estimatedTokens > config.tokenBudget.error) {\n findings.push({\n file: filePath,\n rule: 'check:token-budget',\n line: 1,\n column: 1,\n severity: 'error',\n message: `File is ~${estimatedTokens} tokens (limit: ${config.tokenBudget.error}). Consider splitting or condensing.`,\n });\n } else if (estimatedTokens > config.tokenBudget.warn) {\n findings.push({\n file: filePath,\n rule: 'check:token-budget',\n line: 1,\n column: 1,\n severity: 'warning',\n message: `File is ~${estimatedTokens} tokens (warn threshold: ${config.tokenBudget.warn}). Consider condensing.`,\n });\n }\n\n return findings;\n}\n\nexport function checkVague(\n parsed: ParsedFile,\n filePath: string,\n config: Config,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n\n for (let i = 0; i < parsed.lines.length; i++) {\n const line = parsed.lines[i].toLowerCase();\n for (const pattern of config.vaguePatterns) {\n if (line.includes(pattern.toLowerCase())) {\n findings.push({\n file: filePath,\n rule: 'check:vague',\n line: i + 1,\n column: line.indexOf(pattern.toLowerCase()) + 1,\n severity: 'warning',\n message: `Vague instruction: \"${pattern}\". Replace with specific, actionable guidance.`,\n });\n }\n }\n }\n\n return findings;\n}\n\nexport function checkRequiredSections(\n parsed: ParsedFile,\n filePath: string,\n config: Config,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const normalizedSections = parsed.sections.map((s) => s.toLowerCase());\n\n for (const required of config.requiredSections) {\n const found = normalizedSections.some(\n (s) => s.includes(required.toLowerCase()),\n );\n if (!found) {\n findings.push({\n file: filePath,\n rule: 'check:required-sections',\n line: 1,\n column: 1,\n severity: 'warning',\n message: `Missing recommended section: \"${required}\"`,\n });\n }\n }\n\n return findings;\n}\n\nexport function checkStaleDates(\n parsed: ParsedFile,\n filePath: string,\n config: Config,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const currentYear = new Date().getFullYear();\n const threshold = currentYear - config.staleDateYears;\n\n const yearPattern = /\\b(20[0-9]{2})\\b/g;\n\n for (let i = 0; i < parsed.lines.length; i++) {\n let match: RegExpExecArray | null;\n yearPattern.lastIndex = 0;\n while ((match = yearPattern.exec(parsed.lines[i])) !== null) {\n const year = parseInt(match[1], 10);\n if (year < threshold) {\n findings.push({\n file: filePath,\n rule: 'check:stale-dates',\n line: i + 1,\n column: match.index + 1,\n severity: 'warning',\n message: `Possibly stale year reference: ${year} (older than ${config.staleDateYears} years)`,\n });\n }\n }\n }\n\n return findings;\n}\n\nexport function checkContradictions(\n parsed: ParsedFile,\n filePath: string,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n\n // Contradiction pairs: if both patterns appear, flag them\n const contradictionPairs: [RegExp, RegExp, string][] = [\n [\n /\\balways use (\\w+)/i,\n /\\bnever use (\\w+)/i,\n 'Contradictory \"always use\" and \"never use\" directives',\n ],\n [\n /\\bdo not (?:use|add|include) (comments|docstrings|type annotations)/i,\n /\\b(?:always|must) (?:add|include|write) \\1/i,\n 'Contradictory directives about adding/not adding',\n ],\n [\n /\\bprefer (\\w+) over (\\w+)/i,\n /\\bprefer \\2 over \\1/i,\n 'Contradictory preference directives',\n ],\n ];\n\n const lineTexts = parsed.lines;\n\n for (const [patternA, patternB, message] of contradictionPairs) {\n const matchesA: { line: number; match: RegExpExecArray }[] = [];\n const matchesB: { line: number; match: RegExpExecArray }[] = [];\n\n for (let i = 0; i < lineTexts.length; i++) {\n const lineText = lineTexts[i];\n const a = patternA.exec(lineText);\n if (a) matchesA.push({ line: i + 1, match: a });\n const b = patternB.exec(lineText);\n if (b) matchesB.push({ line: i + 1, match: b });\n }\n\n if (matchesA.length > 0 && matchesB.length > 0) {\n // Check if contradictions reference the same term\n for (const a of matchesA) {\n for (const b of matchesB) {\n if (\n a.match[1] &&\n b.match[1] &&\n a.match[1].toLowerCase() === b.match[1].toLowerCase()\n ) {\n findings.push({\n file: filePath,\n rule: 'check:contradictions',\n line: b.line,\n column: 1,\n severity: 'warning',\n message: `${message} (conflicts with line ${a.line})`,\n });\n }\n }\n }\n }\n }\n\n return findings;\n}\n\nconst SHELL_BUILTINS = new Set([\n 'cd', 'export', 'echo', 'source', 'set', 'unset', 'alias', 'unalias',\n 'type', 'readonly', 'declare', 'local', 'eval', 'exec', 'trap',\n 'return', 'exit', 'shift', 'wait', 'read', 'pushd', 'popd', 'dirs',\n 'ulimit', 'umask', 'getopts', 'hash', 'pwd', 'test', 'true', 'false',\n 'printf', 'let', 'if', 'then', 'else', 'fi', 'for', 'do', 'done',\n 'while', 'until', 'case', 'esac', 'in', 'function',\n]);\n\nconst SHELL_LANGS = new Set(['bash', 'sh', 'shell']);\n\nexport function checkCommands(\n parsed: ParsedFile,\n filePath: string,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const cache = new Map<string, boolean>();\n\n for (const block of parsed.codeBlocks) {\n if (!SHELL_LANGS.has(block.lang)) continue;\n\n const lines = block.content.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n let line = lines[i].trim();\n if (!line || line.startsWith('#')) continue;\n // Strip prompt prefixes\n if (line.startsWith('$ ') || line.startsWith('> ')) {\n line = line.slice(2).trim();\n }\n if (!line) continue;\n\n const cmd = line.split(/\\s/)[0];\n if (!cmd || SHELL_BUILTINS.has(cmd)) continue;\n // Skip environment variable assignments\n if (/^[A-Z_]+=/.test(cmd)) continue;\n\n if (!cache.has(cmd)) {\n try {\n execFileSync('which', [cmd], { stdio: 'pipe' });\n cache.set(cmd, true);\n } catch {\n cache.set(cmd, false);\n }\n }\n\n if (!cache.get(cmd)) {\n findings.push({\n file: filePath,\n rule: 'check:commands',\n line: block.line + 1 + i,\n column: 1,\n severity: 'warning',\n message: `Command not found on system: \"${cmd}\"`,\n });\n }\n }\n }\n\n return findings;\n}\n\nconst JS_LANGS = new Set(['ts', 'js', 'typescript', 'javascript', 'tsx', 'jsx']);\nconst IMPORT_FROM_RE = /(?:import\\s+.*?\\s+from\\s+['\"]([^'\"]+)['\"]|require\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\))/g;\nconst EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.cjs'];\n\nfunction resolveModule(base: string, importPath: string): boolean {\n const resolved = resolve(base, importPath);\n\n // Exact match\n if (existsSync(resolved) && !resolved.endsWith('/')) return true;\n\n // Try extension fallbacks\n for (const ext of EXTENSIONS) {\n if (existsSync(resolved + ext)) return true;\n }\n\n // Try index files\n for (const ext of EXTENSIONS) {\n if (existsSync(resolve(resolved, 'index' + ext))) return true;\n }\n\n return false;\n}\n\nexport function checkImports(\n parsed: ParsedFile,\n filePath: string,\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const baseDir = dirname(filePath);\n\n for (const block of parsed.codeBlocks) {\n if (!JS_LANGS.has(block.lang)) continue;\n\n const lines = block.content.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n let match: RegExpExecArray | null;\n IMPORT_FROM_RE.lastIndex = 0;\n while ((match = IMPORT_FROM_RE.exec(lines[i])) !== null) {\n const importPath = match[1] || match[2];\n // Only check relative imports\n if (!importPath.startsWith('./') && !importPath.startsWith('../')) continue;\n\n if (!resolveModule(baseDir, importPath)) {\n findings.push({\n file: filePath,\n rule: 'check:imports',\n line: block.line + 1 + i,\n column: match.index + 1,\n severity: 'error',\n message: `Import path does not resolve: \"${importPath}\"`,\n });\n }\n }\n }\n }\n\n return findings;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { DEFAULT_CONFIG, type Config } from './types.js';\n\nexport function loadConfig(cwd: string): Config {\n // Try .agent-context-lint.json\n const configPath = resolve(cwd, '.agent-context-lint.json');\n if (existsSync(configPath)) {\n try {\n const raw = JSON.parse(readFileSync(configPath, 'utf-8'));\n return mergeConfig(raw);\n } catch {\n // Invalid config file — use defaults\n }\n }\n\n // Try package.json agentContextLint key\n const pkgPath = resolve(cwd, 'package.json');\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));\n if (pkg.agentContextLint) {\n return mergeConfig(pkg.agentContextLint);\n }\n } catch {\n // Invalid package.json — use defaults\n }\n }\n\n return { ...DEFAULT_CONFIG };\n}\n\nfunction mergeConfig(overrides: Partial<Config>): Config {\n return {\n tokenBudget: {\n ...DEFAULT_CONFIG.tokenBudget,\n ...overrides.tokenBudget,\n },\n requiredSections:\n overrides.requiredSections ?? DEFAULT_CONFIG.requiredSections,\n staleDateYears: overrides.staleDateYears ?? DEFAULT_CONFIG.staleDateYears,\n vaguePatterns: overrides.vaguePatterns ?? DEFAULT_CONFIG.vaguePatterns,\n ignore: overrides.ignore ?? DEFAULT_CONFIG.ignore,\n };\n}\n","export interface LintFinding {\n file: string;\n rule: string;\n line: number;\n column: number;\n severity: 'error' | 'warning';\n message: string;\n}\n\nexport interface FileResult {\n file: string;\n findings: LintFinding[];\n score: number;\n}\n\nexport interface LintResult {\n files: FileResult[];\n totalFindings: number;\n errors: number;\n warnings: number;\n}\n\nexport interface CLIOptions {\n files: string[];\n format: 'text' | 'json';\n fix: boolean;\n cwd: string;\n}\n\nexport interface Config {\n tokenBudget: { warn: number; error: number };\n requiredSections: string[];\n staleDateYears: number;\n vaguePatterns: string[];\n ignore: string[];\n}\n\nexport const DEFAULT_CONFIG: Config = {\n tokenBudget: { warn: 2000, error: 5000 },\n requiredSections: ['Setup', 'Testing', 'Build'],\n staleDateYears: 2,\n vaguePatterns: [\n 'follow best practices',\n 'be careful',\n 'use good judgment',\n 'use common sense',\n 'as appropriate',\n 'when necessary',\n 'if needed',\n 'as needed',\n 'handle edge cases',\n 'write clean code',\n 'keep it simple',\n 'use proper',\n 'ensure quality',\n ],\n ignore: [],\n};\n\nexport const CONTEXT_FILE_NAMES = [\n 'CLAUDE.md',\n 'AGENTS.md',\n '.cursorrules',\n 'copilot-instructions.md',\n '.github/copilot-instructions.md',\n];\n","import { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { CONTEXT_FILE_NAMES } from './types.js';\n\nexport function discoverContextFiles(cwd: string): string[] {\n const found: string[] = [];\n for (const name of CONTEXT_FILE_NAMES) {\n const fullPath = resolve(cwd, name);\n if (existsSync(fullPath)) {\n found.push(fullPath);\n }\n }\n return found;\n}\n","import { readFileSync } from 'node:fs';\n\nexport interface ParsedFile {\n content: string;\n lines: string[];\n paths: PathReference[];\n commands: CommandReference[];\n sections: string[];\n codeBlocks: CodeBlock[];\n inlineCode: InlineCode[];\n}\n\nexport interface PathReference {\n value: string;\n line: number;\n column: number;\n}\n\nexport interface CommandReference {\n value: string;\n line: number;\n column: number;\n}\n\nexport interface CodeBlock {\n content: string;\n lang: string;\n line: number;\n}\n\nexport interface InlineCode {\n content: string;\n line: number;\n column: number;\n}\n\nconst PATH_PATTERN = /(?:^|\\s|`)(\\.?\\.?\\/[\\w./@-]+[\\w/@-])/g;\nconst COMMAND_PATTERN = /(?:npm|npx|pnpm|yarn|bun|bunx)\\s+(?:run\\s+)?[\\w:@./-]+/g;\nconst HEADING_PATTERN = /^#{1,6}\\s+(.+)$/;\nconst FENCED_BLOCK_START = /^```(\\w*)/;\nconst FENCED_BLOCK_END = /^```\\s*$/;\nconst INLINE_CODE_PATTERN = /`([^`]+)`/g;\n\nexport function parseFile(filePath: string): ParsedFile {\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n\n const paths: PathReference[] = [];\n const commands: CommandReference[] = [];\n const sections: string[] = [];\n const codeBlocks: CodeBlock[] = [];\n const inlineCode: InlineCode[] = [];\n\n let inCodeBlock = false;\n let codeBlockLang = '';\n let codeBlockContent = '';\n let codeBlockStart = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineNum = i + 1;\n\n // Track fenced code blocks\n if (!inCodeBlock) {\n const blockStart = FENCED_BLOCK_START.exec(line);\n if (blockStart && line.trimStart().startsWith('```')) {\n inCodeBlock = true;\n codeBlockLang = blockStart[1] || '';\n codeBlockContent = '';\n codeBlockStart = lineNum;\n continue;\n }\n } else {\n if (FENCED_BLOCK_END.test(line) && line.trimStart() === '```') {\n codeBlocks.push({\n content: codeBlockContent,\n lang: codeBlockLang,\n line: codeBlockStart,\n });\n inCodeBlock = false;\n continue;\n }\n codeBlockContent += (codeBlockContent ? '\\n' : '') + line;\n }\n\n // Extract headings as sections\n const headingMatch = HEADING_PATTERN.exec(line);\n if (headingMatch) {\n sections.push(headingMatch[1].trim());\n }\n\n // Extract paths from both inline code and plain text\n let pathMatch: RegExpExecArray | null;\n PATH_PATTERN.lastIndex = 0;\n while ((pathMatch = PATH_PATTERN.exec(line)) !== null) {\n const value = pathMatch[1];\n // Skip URLs\n if (value.includes('://')) continue;\n paths.push({\n value,\n line: lineNum,\n column: pathMatch.index + (pathMatch[0].length - value.length) + 1,\n });\n }\n\n // Extract npm/pnpm/yarn/bun commands\n let cmdMatch: RegExpExecArray | null;\n COMMAND_PATTERN.lastIndex = 0;\n while ((cmdMatch = COMMAND_PATTERN.exec(line)) !== null) {\n commands.push({\n value: cmdMatch[0],\n line: lineNum,\n column: cmdMatch.index + 1,\n });\n }\n\n // Extract inline code spans\n if (!inCodeBlock) {\n let inlineMatch: RegExpExecArray | null;\n INLINE_CODE_PATTERN.lastIndex = 0;\n while ((inlineMatch = INLINE_CODE_PATTERN.exec(line)) !== null) {\n inlineCode.push({\n content: inlineMatch[1],\n line: lineNum,\n column: inlineMatch.index + 2,\n });\n }\n }\n }\n\n return { content, lines, paths, commands, sections, codeBlocks, inlineCode };\n}\n","import type { LintFinding } from './types.js';\n\n/**\n * Computes a 0–100 quality score for a file based on its findings.\n *\n * Starts at 100 and deducts:\n * - 15 points per error\n * - 5 points per warning\n *\n * Minimum score is 0.\n */\nexport function computeScore(findings: LintFinding[]): number {\n let score = 100;\n for (const finding of findings) {\n if (finding.severity === 'error') {\n score -= 15;\n } else {\n score -= 5;\n }\n }\n return Math.max(0, score);\n}\n","import { relative } from 'node:path';\nimport type { LintResult } from './types.js';\n\nexport function formatText(result: LintResult, cwd: string): string {\n const lines: string[] = [];\n\n for (const file of result.files) {\n const relPath = relative(cwd, file.file);\n lines.push(`\\n ${relPath} (score: ${file.score}/100)`);\n\n if (file.findings.length === 0) {\n lines.push(' No issues found.');\n continue;\n }\n\n for (const f of file.findings) {\n const icon = f.severity === 'error' ? 'x' : '!';\n lines.push(\n ` ${f.line}:${f.column} ${icon} ${f.message} [${f.rule}]`,\n );\n }\n }\n\n lines.push('');\n lines.push(\n ` ${result.totalFindings} problems (${result.errors} errors, ${result.warnings} warnings)`,\n );\n lines.push('');\n\n return lines.join('\\n');\n}\n\nexport function formatJson(result: LintResult, cwd: string): string {\n const output = {\n ...result,\n files: result.files.map((f) => ({\n ...f,\n file: relative(cwd, f.file),\n })),\n };\n return JSON.stringify(output, null, 2);\n}\n","import { readFileSync, writeFileSync } from 'node:fs';\n\nexport interface FixChange {\n line: number;\n description: string;\n}\n\nexport interface FixResult {\n file: string;\n fixed: boolean;\n changes: FixChange[];\n}\n\nexport function fixFile(filePath: string): FixResult {\n const original = readFileSync(filePath, 'utf-8');\n const changes: FixChange[] = [];\n let content = original;\n\n // Fix trailing whitespace on each line\n const lines = content.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n if (lines[i] !== lines[i].trimEnd()) {\n changes.push({ line: i + 1, description: 'Removed trailing whitespace' });\n lines[i] = lines[i].trimEnd();\n }\n }\n content = lines.join('\\n');\n\n // Fix multiple consecutive blank lines → single blank line\n const collapsed = content.replace(/\\n{3,}/g, (match, offset) => {\n const before = content.slice(0, offset);\n const lineNum = before.split('\\n').length + 1;\n changes.push({ line: lineNum, description: 'Collapsed multiple blank lines' });\n return '\\n\\n';\n });\n content = collapsed;\n\n // Fix missing trailing newline\n if (content.length > 0 && !content.endsWith('\\n')) {\n changes.push({\n line: content.split('\\n').length,\n description: 'Added trailing newline',\n });\n content += '\\n';\n }\n\n const fixed = content !== original;\n if (fixed) {\n writeFileSync(filePath, content);\n }\n\n return { file: filePath, fixed, changes };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,oBAAwB;;;ACAxB,gCAA6B;AAC7B,qBAAyC;AACzC,uBAAiC;AAI1B,SAAS,WACd,QACA,UACe;AACf,QAAM,WAA0B,CAAC;AACjC,QAAM,cAAU,0BAAQ,QAAQ;AAEhC,aAAW,OAAO,OAAO,OAAO;AAC9B,UAAM,eAAW,0BAAQ,SAAS,IAAI,KAAK;AAC3C,QAAI,KAAC,2BAAW,QAAQ,GAAG;AACzB,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM,IAAI;AAAA,QACV,QAAQ,IAAI;AAAA,QACZ,UAAU;AAAA,QACV,SAAS,wBAAwB,IAAI,KAAK;AAAA,MAC5C,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,aACd,QACA,UACe;AACf,QAAM,WAA0B,CAAC;AACjC,QAAM,cAAU,0BAAQ,QAAQ;AAChC,QAAM,cAAU,0BAAQ,SAAS,cAAc;AAE/C,MAAI,UAAkC,CAAC;AACvC,UAAI,2BAAW,OAAO,GAAG;AACvB,QAAI;AACF,YAAM,MAAM,KAAK,UAAM,6BAAa,SAAS,OAAO,CAAC;AACrD,gBAAU,IAAI,WAAW,CAAC;AAAA,IAC5B,QAAQ;AAEN,aAAO;AAAA,IACT;AAAA,EACF,OAAO;AAEL,WAAO;AAAA,EACT;AAEA,aAAW,OAAO,OAAO,UAAU;AAEjC,UAAM,QAAQ,6CAA6C,KAAK,IAAI,KAAK;AACzE,UAAM,cAAc,oDAAoD;AAAA,MACtE,IAAI;AAAA,IACN;AAEA,UAAM,aAAa,QAAQ,CAAC,KAAK,cAAc,CAAC;AAChD,QAAI,cAAc,EAAE,cAAc,UAAU;AAC1C,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM,IAAI;AAAA,QACV,QAAQ,IAAI;AAAA,QACZ,UAAU;AAAA,QACV,SAAS,sCAAsC,UAAU;AAAA,MAC3D,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,iBACd,QACA,UACA,QACe;AACf,QAAM,WAA0B,CAAC;AAGjC,QAAM,kBAAkB,KAAK,KAAK,OAAO,QAAQ,SAAS,CAAC;AAE3D,MAAI,kBAAkB,OAAO,YAAY,OAAO;AAC9C,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,YAAY,eAAe,mBAAmB,OAAO,YAAY,KAAK;AAAA,IACjF,CAAC;AAAA,EACH,WAAW,kBAAkB,OAAO,YAAY,MAAM;AACpD,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,YAAY,eAAe,4BAA4B,OAAO,YAAY,IAAI;AAAA,IACzF,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAEO,SAAS,WACd,QACA,UACA,QACe;AACf,QAAM,WAA0B,CAAC;AAEjC,WAAS,IAAI,GAAG,IAAI,OAAO,MAAM,QAAQ,KAAK;AAC5C,UAAM,OAAO,OAAO,MAAM,CAAC,EAAE,YAAY;AACzC,eAAW,WAAW,OAAO,eAAe;AAC1C,UAAI,KAAK,SAAS,QAAQ,YAAY,CAAC,GAAG;AACxC,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM,IAAI;AAAA,UACV,QAAQ,KAAK,QAAQ,QAAQ,YAAY,CAAC,IAAI;AAAA,UAC9C,UAAU;AAAA,UACV,SAAS,uBAAuB,OAAO;AAAA,QACzC,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,sBACd,QACA,UACA,QACe;AACf,QAAM,WAA0B,CAAC;AACjC,QAAM,qBAAqB,OAAO,SAAS,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;AAErE,aAAW,YAAY,OAAO,kBAAkB;AAC9C,UAAM,QAAQ,mBAAmB;AAAA,MAC/B,CAAC,MAAM,EAAE,SAAS,SAAS,YAAY,CAAC;AAAA,IAC1C;AACA,QAAI,CAAC,OAAO;AACV,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS,iCAAiC,QAAQ;AAAA,MACpD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,gBACd,QACA,UACA,QACe;AACf,QAAM,WAA0B,CAAC;AACjC,QAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC3C,QAAM,YAAY,cAAc,OAAO;AAEvC,QAAM,cAAc;AAEpB,WAAS,IAAI,GAAG,IAAI,OAAO,MAAM,QAAQ,KAAK;AAC5C,QAAI;AACJ,gBAAY,YAAY;AACxB,YAAQ,QAAQ,YAAY,KAAK,OAAO,MAAM,CAAC,CAAC,OAAO,MAAM;AAC3D,YAAM,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE;AAClC,UAAI,OAAO,WAAW;AACpB,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM,IAAI;AAAA,UACV,QAAQ,MAAM,QAAQ;AAAA,UACtB,UAAU;AAAA,UACV,SAAS,kCAAkC,IAAI,gBAAgB,OAAO,cAAc;AAAA,QACtF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,oBACd,QACA,UACe;AACf,QAAM,WAA0B,CAAC;AAGjC,QAAM,qBAAiD;AAAA,IACrD;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,OAAO;AAEzB,aAAW,CAAC,UAAU,UAAU,OAAO,KAAK,oBAAoB;AAC9D,UAAM,WAAuD,CAAC;AAC9D,UAAM,WAAuD,CAAC;AAE9D,aAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;AACzC,YAAM,WAAW,UAAU,CAAC;AAC5B,YAAM,IAAI,SAAS,KAAK,QAAQ;AAChC,UAAI,EAAG,UAAS,KAAK,EAAE,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;AAC9C,YAAM,IAAI,SAAS,KAAK,QAAQ;AAChC,UAAI,EAAG,UAAS,KAAK,EAAE,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;AAAA,IAChD;AAEA,QAAI,SAAS,SAAS,KAAK,SAAS,SAAS,GAAG;AAE9C,iBAAW,KAAK,UAAU;AACxB,mBAAW,KAAK,UAAU;AACxB,cACE,EAAE,MAAM,CAAC,KACT,EAAE,MAAM,CAAC,KACT,EAAE,MAAM,CAAC,EAAE,YAAY,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY,GACpD;AACA,qBAAS,KAAK;AAAA,cACZ,MAAM;AAAA,cACN,MAAM;AAAA,cACN,MAAM,EAAE;AAAA,cACR,QAAQ;AAAA,cACR,UAAU;AAAA,cACV,SAAS,GAAG,OAAO,yBAAyB,EAAE,IAAI;AAAA,YACpD,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,IAAM,iBAAiB,oBAAI,IAAI;AAAA,EAC7B;AAAA,EAAM;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAO;AAAA,EAAS;AAAA,EAAS;AAAA,EAC3D;AAAA,EAAQ;AAAA,EAAY;AAAA,EAAW;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EACxD;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAQ;AAAA,EAC5D;AAAA,EAAU;AAAA,EAAS;AAAA,EAAW;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAC7D;AAAA,EAAU;AAAA,EAAO;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAO;AAAA,EAAM;AAAA,EAC1D;AAAA,EAAS;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAM;AAC1C,CAAC;AAED,IAAM,cAAc,oBAAI,IAAI,CAAC,QAAQ,MAAM,OAAO,CAAC;AAE5C,SAAS,cACd,QACA,UACe;AACf,QAAM,WAA0B,CAAC;AACjC,QAAM,QAAQ,oBAAI,IAAqB;AAEvC,aAAW,SAAS,OAAO,YAAY;AACrC,QAAI,CAAC,YAAY,IAAI,MAAM,IAAI,EAAG;AAElC,UAAM,QAAQ,MAAM,QAAQ,MAAM,IAAI;AACtC,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAI,OAAO,MAAM,CAAC,EAAE,KAAK;AACzB,UAAI,CAAC,QAAQ,KAAK,WAAW,GAAG,EAAG;AAEnC,UAAI,KAAK,WAAW,IAAI,KAAK,KAAK,WAAW,IAAI,GAAG;AAClD,eAAO,KAAK,MAAM,CAAC,EAAE,KAAK;AAAA,MAC5B;AACA,UAAI,CAAC,KAAM;AAEX,YAAM,MAAM,KAAK,MAAM,IAAI,EAAE,CAAC;AAC9B,UAAI,CAAC,OAAO,eAAe,IAAI,GAAG,EAAG;AAErC,UAAI,YAAY,KAAK,GAAG,EAAG;AAE3B,UAAI,CAAC,MAAM,IAAI,GAAG,GAAG;AACnB,YAAI;AACF,sDAAa,SAAS,CAAC,GAAG,GAAG,EAAE,OAAO,OAAO,CAAC;AAC9C,gBAAM,IAAI,KAAK,IAAI;AAAA,QACrB,QAAQ;AACN,gBAAM,IAAI,KAAK,KAAK;AAAA,QACtB;AAAA,MACF;AAEA,UAAI,CAAC,MAAM,IAAI,GAAG,GAAG;AACnB,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM,MAAM,OAAO,IAAI;AAAA,UACvB,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,SAAS,iCAAiC,GAAG;AAAA,QAC/C,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,IAAM,WAAW,oBAAI,IAAI,CAAC,MAAM,MAAM,cAAc,cAAc,OAAO,KAAK,CAAC;AAC/E,IAAM,iBAAiB;AACvB,IAAM,aAAa,CAAC,OAAO,QAAQ,OAAO,QAAQ,QAAQ,QAAQ,MAAM;AAExE,SAAS,cAAc,MAAc,YAA6B;AAChE,QAAM,eAAW,0BAAQ,MAAM,UAAU;AAGzC,UAAI,2BAAW,QAAQ,KAAK,CAAC,SAAS,SAAS,GAAG,EAAG,QAAO;AAG5D,aAAW,OAAO,YAAY;AAC5B,YAAI,2BAAW,WAAW,GAAG,EAAG,QAAO;AAAA,EACzC;AAGA,aAAW,OAAO,YAAY;AAC5B,YAAI,+BAAW,0BAAQ,UAAU,UAAU,GAAG,CAAC,EAAG,QAAO;AAAA,EAC3D;AAEA,SAAO;AACT;AAEO,SAAS,aACd,QACA,UACe;AACf,QAAM,WAA0B,CAAC;AACjC,QAAM,cAAU,0BAAQ,QAAQ;AAEhC,aAAW,SAAS,OAAO,YAAY;AACrC,QAAI,CAAC,SAAS,IAAI,MAAM,IAAI,EAAG;AAE/B,UAAM,QAAQ,MAAM,QAAQ,MAAM,IAAI;AACtC,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAI;AACJ,qBAAe,YAAY;AAC3B,cAAQ,QAAQ,eAAe,KAAK,MAAM,CAAC,CAAC,OAAO,MAAM;AACvD,cAAM,aAAa,MAAM,CAAC,KAAK,MAAM,CAAC;AAEtC,YAAI,CAAC,WAAW,WAAW,IAAI,KAAK,CAAC,WAAW,WAAW,KAAK,EAAG;AAEnE,YAAI,CAAC,cAAc,SAAS,UAAU,GAAG;AACvC,mBAAS,KAAK;AAAA,YACZ,MAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM,MAAM,OAAO,IAAI;AAAA,YACvB,QAAQ,MAAM,QAAQ;AAAA,YACtB,UAAU;AAAA,YACV,SAAS,kCAAkC,UAAU;AAAA,UACvD,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;;;ACxXA,IAAAC,kBAAyC;AACzC,IAAAC,oBAAwB;;;ACoCjB,IAAM,iBAAyB;AAAA,EACpC,aAAa,EAAE,MAAM,KAAM,OAAO,IAAK;AAAA,EACvC,kBAAkB,CAAC,SAAS,WAAW,OAAO;AAAA,EAC9C,gBAAgB;AAAA,EAChB,eAAe;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,QAAQ,CAAC;AACX;AAEO,IAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AD7DO,SAAS,WAAW,KAAqB;AAE9C,QAAM,iBAAa,2BAAQ,KAAK,0BAA0B;AAC1D,UAAI,4BAAW,UAAU,GAAG;AAC1B,QAAI;AACF,YAAM,MAAM,KAAK,UAAM,8BAAa,YAAY,OAAO,CAAC;AACxD,aAAO,YAAY,GAAG;AAAA,IACxB,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,cAAU,2BAAQ,KAAK,cAAc;AAC3C,UAAI,4BAAW,OAAO,GAAG;AACvB,QAAI;AACF,YAAM,MAAM,KAAK,UAAM,8BAAa,SAAS,OAAO,CAAC;AACrD,UAAI,IAAI,kBAAkB;AACxB,eAAO,YAAY,IAAI,gBAAgB;AAAA,MACzC;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,EAAE,GAAG,eAAe;AAC7B;AAEA,SAAS,YAAY,WAAoC;AACvD,SAAO;AAAA,IACL,aAAa;AAAA,MACX,GAAG,eAAe;AAAA,MAClB,GAAG,UAAU;AAAA,IACf;AAAA,IACA,kBACE,UAAU,oBAAoB,eAAe;AAAA,IAC/C,gBAAgB,UAAU,kBAAkB,eAAe;AAAA,IAC3D,eAAe,UAAU,iBAAiB,eAAe;AAAA,IACzD,QAAQ,UAAU,UAAU,eAAe;AAAA,EAC7C;AACF;;;AE5CA,IAAAC,kBAA2B;AAC3B,IAAAC,oBAAwB;AAGjB,SAAS,qBAAqB,KAAuB;AAC1D,QAAM,QAAkB,CAAC;AACzB,aAAW,QAAQ,oBAAoB;AACrC,UAAM,eAAW,2BAAQ,KAAK,IAAI;AAClC,YAAI,4BAAW,QAAQ,GAAG;AACxB,YAAM,KAAK,QAAQ;AAAA,IACrB;AAAA,EACF;AACA,SAAO;AACT;;;ACbA,IAAAC,kBAA6B;AAoC7B,IAAM,eAAe;AACrB,IAAM,kBAAkB;AACxB,IAAM,kBAAkB;AACxB,IAAM,qBAAqB;AAC3B,IAAM,mBAAmB;AACzB,IAAM,sBAAsB;AAErB,SAAS,UAAU,UAA8B;AACtD,QAAM,cAAU,8BAAa,UAAU,OAAO;AAC9C,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAEhC,QAAM,QAAyB,CAAC;AAChC,QAAM,WAA+B,CAAC;AACtC,QAAM,WAAqB,CAAC;AAC5B,QAAM,aAA0B,CAAC;AACjC,QAAM,aAA2B,CAAC;AAElC,MAAI,cAAc;AAClB,MAAI,gBAAgB;AACpB,MAAI,mBAAmB;AACvB,MAAI,iBAAiB;AAErB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,OAAO,MAAM,CAAC;AACpB,UAAM,UAAU,IAAI;AAGpB,QAAI,CAAC,aAAa;AAChB,YAAM,aAAa,mBAAmB,KAAK,IAAI;AAC/C,UAAI,cAAc,KAAK,UAAU,EAAE,WAAW,KAAK,GAAG;AACpD,sBAAc;AACd,wBAAgB,WAAW,CAAC,KAAK;AACjC,2BAAmB;AACnB,yBAAiB;AACjB;AAAA,MACF;AAAA,IACF,OAAO;AACL,UAAI,iBAAiB,KAAK,IAAI,KAAK,KAAK,UAAU,MAAM,OAAO;AAC7D,mBAAW,KAAK;AAAA,UACd,SAAS;AAAA,UACT,MAAM;AAAA,UACN,MAAM;AAAA,QACR,CAAC;AACD,sBAAc;AACd;AAAA,MACF;AACA,2BAAqB,mBAAmB,OAAO,MAAM;AAAA,IACvD;AAGA,UAAM,eAAe,gBAAgB,KAAK,IAAI;AAC9C,QAAI,cAAc;AAChB,eAAS,KAAK,aAAa,CAAC,EAAE,KAAK,CAAC;AAAA,IACtC;AAGA,QAAI;AACJ,iBAAa,YAAY;AACzB,YAAQ,YAAY,aAAa,KAAK,IAAI,OAAO,MAAM;AACrD,YAAM,QAAQ,UAAU,CAAC;AAEzB,UAAI,MAAM,SAAS,KAAK,EAAG;AAC3B,YAAM,KAAK;AAAA,QACT;AAAA,QACA,MAAM;AAAA,QACN,QAAQ,UAAU,SAAS,UAAU,CAAC,EAAE,SAAS,MAAM,UAAU;AAAA,MACnE,CAAC;AAAA,IACH;AAGA,QAAI;AACJ,oBAAgB,YAAY;AAC5B,YAAQ,WAAW,gBAAgB,KAAK,IAAI,OAAO,MAAM;AACvD,eAAS,KAAK;AAAA,QACZ,OAAO,SAAS,CAAC;AAAA,QACjB,MAAM;AAAA,QACN,QAAQ,SAAS,QAAQ;AAAA,MAC3B,CAAC;AAAA,IACH;AAGA,QAAI,CAAC,aAAa;AAChB,UAAI;AACJ,0BAAoB,YAAY;AAChC,cAAQ,cAAc,oBAAoB,KAAK,IAAI,OAAO,MAAM;AAC9D,mBAAW,KAAK;AAAA,UACd,SAAS,YAAY,CAAC;AAAA,UACtB,MAAM;AAAA,UACN,QAAQ,YAAY,QAAQ;AAAA,QAC9B,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,OAAO,OAAO,UAAU,UAAU,YAAY,WAAW;AAC7E;;;ACxHO,SAAS,aAAa,UAAiC;AAC5D,MAAI,QAAQ;AACZ,aAAW,WAAW,UAAU;AAC9B,QAAI,QAAQ,aAAa,SAAS;AAChC,eAAS;AAAA,IACX,OAAO;AACL,eAAS;AAAA,IACX;AAAA,EACF;AACA,SAAO,KAAK,IAAI,GAAG,KAAK;AAC1B;;;ACrBA,IAAAC,oBAAyB;AAGlB,SAAS,WAAW,QAAoB,KAAqB;AAClE,QAAM,QAAkB,CAAC;AAEzB,aAAW,QAAQ,OAAO,OAAO;AAC/B,UAAM,cAAU,4BAAS,KAAK,KAAK,IAAI;AACvC,UAAM,KAAK;AAAA,IAAO,OAAO,aAAa,KAAK,KAAK,OAAO;AAEvD,QAAI,KAAK,SAAS,WAAW,GAAG;AAC9B,YAAM,KAAK,sBAAsB;AACjC;AAAA,IACF;AAEA,eAAW,KAAK,KAAK,UAAU;AAC7B,YAAM,OAAO,EAAE,aAAa,UAAU,MAAM;AAC5C,YAAM;AAAA,QACJ,OAAO,EAAE,IAAI,IAAI,EAAE,MAAM,KAAK,IAAI,IAAI,EAAE,OAAO,MAAM,EAAE,IAAI;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,EAAE;AACb,QAAM;AAAA,IACJ,KAAK,OAAO,aAAa,cAAc,OAAO,MAAM,YAAY,OAAO,QAAQ;AAAA,EACjF;AACA,QAAM,KAAK,EAAE;AAEb,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,WAAW,QAAoB,KAAqB;AAClE,QAAM,SAAS;AAAA,IACb,GAAG;AAAA,IACH,OAAO,OAAO,MAAM,IAAI,CAAC,OAAO;AAAA,MAC9B,GAAG;AAAA,MACH,UAAM,4BAAS,KAAK,EAAE,IAAI;AAAA,IAC5B,EAAE;AAAA,EACJ;AACA,SAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACvC;;;ACzCA,IAAAC,kBAA4C;AAarC,SAAS,QAAQ,UAA6B;AACnD,QAAM,eAAW,8BAAa,UAAU,OAAO;AAC/C,QAAM,UAAuB,CAAC;AAC9B,MAAI,UAAU;AAGd,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,MAAM,CAAC,MAAM,MAAM,CAAC,EAAE,QAAQ,GAAG;AACnC,cAAQ,KAAK,EAAE,MAAM,IAAI,GAAG,aAAa,8BAA8B,CAAC;AACxE,YAAM,CAAC,IAAI,MAAM,CAAC,EAAE,QAAQ;AAAA,IAC9B;AAAA,EACF;AACA,YAAU,MAAM,KAAK,IAAI;AAGzB,QAAM,YAAY,QAAQ,QAAQ,WAAW,CAAC,OAAO,WAAW;AAC9D,UAAM,SAAS,QAAQ,MAAM,GAAG,MAAM;AACtC,UAAM,UAAU,OAAO,MAAM,IAAI,EAAE,SAAS;AAC5C,YAAQ,KAAK,EAAE,MAAM,SAAS,aAAa,iCAAiC,CAAC;AAC7E,WAAO;AAAA,EACT,CAAC;AACD,YAAU;AAGV,MAAI,QAAQ,SAAS,KAAK,CAAC,QAAQ,SAAS,IAAI,GAAG;AACjD,YAAQ,KAAK;AAAA,MACX,MAAM,QAAQ,MAAM,IAAI,EAAE;AAAA,MAC1B,aAAa;AAAA,IACf,CAAC;AACD,eAAW;AAAA,EACb;AAEA,QAAM,QAAQ,YAAY;AAC1B,MAAI,OAAO;AACT,uCAAc,UAAU,OAAO;AAAA,EACjC;AAEA,SAAO,EAAE,MAAM,UAAU,OAAO,QAAQ;AAC1C;;;ARzBO,SAAS,SAAS,UAAkB,KAAyB;AAClE,QAAM,SAAS,WAAW,GAAG;AAC7B,QAAM,SAAS,UAAU,QAAQ;AAEjC,QAAM,WAA0B;AAAA,IAC9B,GAAG,WAAW,QAAQ,QAAQ;AAAA,IAC9B,GAAG,aAAa,QAAQ,QAAQ;AAAA,IAChC,GAAG,iBAAiB,QAAQ,UAAU,MAAM;AAAA,IAC5C,GAAG,WAAW,QAAQ,UAAU,MAAM;AAAA,IACtC,GAAG,sBAAsB,QAAQ,UAAU,MAAM;AAAA,IACjD,GAAG,gBAAgB,QAAQ,UAAU,MAAM;AAAA,IAC3C,GAAG,oBAAoB,QAAQ,QAAQ;AAAA,IACvC,GAAG,cAAc,QAAQ,QAAQ;AAAA,IACjC,GAAG,aAAa,QAAQ,QAAQ;AAAA,EAClC;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,OAAO,aAAa,QAAQ;AAAA,EAC9B;AACF;AAEO,SAAS,KAAK,KAAa,OAA8B;AAC9D,QAAM,cACJ,SAAS,MAAM,SAAS,IACpB,MAAM,IAAI,CAAC,UAAM,2BAAQ,KAAK,CAAC,CAAC,IAChC,qBAAqB,GAAG;AAE9B,MAAI,YAAY,WAAW,GAAG;AAC5B,WAAO,EAAE,OAAO,CAAC,GAAG,eAAe,GAAG,QAAQ,GAAG,UAAU,EAAE;AAAA,EAC/D;AAEA,QAAM,UAAU,YAAY,IAAI,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAEvD,QAAM,gBAAgB,QAAQ;AAAA,IAC5B,CAAC,KAAK,MAAM,MAAM,EAAE,SAAS;AAAA,IAC7B;AAAA,EACF;AACA,QAAM,SAAS,QAAQ;AAAA,IACrB,CAAC,KAAK,MAAM,MAAM,EAAE,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO,EAAE;AAAA,IACnE;AAAA,EACF;AACA,QAAM,WAAW,QAAQ;AAAA,IACvB,CAAC,KAAK,MACJ,MAAM,EAAE,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,SAAS,EAAE;AAAA,IAC3D;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,SAAS,eAAe,QAAQ,SAAS;AAC3D;","names":["import_node_path","import_node_fs","import_node_path","import_node_fs","import_node_path","import_node_fs","import_node_path","import_node_fs"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -83,7 +83,18 @@ declare function loadConfig(cwd: string): Config;
|
|
|
83
83
|
declare function formatText(result: LintResult, cwd: string): string;
|
|
84
84
|
declare function formatJson(result: LintResult, cwd: string): string;
|
|
85
85
|
|
|
86
|
+
interface FixChange {
|
|
87
|
+
line: number;
|
|
88
|
+
description: string;
|
|
89
|
+
}
|
|
90
|
+
interface FixResult {
|
|
91
|
+
file: string;
|
|
92
|
+
fixed: boolean;
|
|
93
|
+
changes: FixChange[];
|
|
94
|
+
}
|
|
95
|
+
declare function fixFile(filePath: string): FixResult;
|
|
96
|
+
|
|
86
97
|
declare function lintFile(filePath: string, cwd: string): FileResult;
|
|
87
98
|
declare function lint(cwd: string, files?: string[]): LintResult;
|
|
88
99
|
|
|
89
|
-
export { type CLIOptions, type Config, type FileResult, type LintFinding, type LintResult, computeScore, discoverContextFiles, formatJson, formatText, lint, lintFile, loadConfig, parseFile };
|
|
100
|
+
export { type CLIOptions, type Config, type FileResult, type FixChange, type FixResult, type LintFinding, type LintResult, computeScore, discoverContextFiles, fixFile, formatJson, formatText, lint, lintFile, loadConfig, parseFile };
|
package/dist/index.d.ts
CHANGED
|
@@ -83,7 +83,18 @@ declare function loadConfig(cwd: string): Config;
|
|
|
83
83
|
declare function formatText(result: LintResult, cwd: string): string;
|
|
84
84
|
declare function formatJson(result: LintResult, cwd: string): string;
|
|
85
85
|
|
|
86
|
+
interface FixChange {
|
|
87
|
+
line: number;
|
|
88
|
+
description: string;
|
|
89
|
+
}
|
|
90
|
+
interface FixResult {
|
|
91
|
+
file: string;
|
|
92
|
+
fixed: boolean;
|
|
93
|
+
changes: FixChange[];
|
|
94
|
+
}
|
|
95
|
+
declare function fixFile(filePath: string): FixResult;
|
|
96
|
+
|
|
86
97
|
declare function lintFile(filePath: string, cwd: string): FileResult;
|
|
87
98
|
declare function lint(cwd: string, files?: string[]): LintResult;
|
|
88
99
|
|
|
89
|
-
export { type CLIOptions, type Config, type FileResult, type LintFinding, type LintResult, computeScore, discoverContextFiles, formatJson, formatText, lint, lintFile, loadConfig, parseFile };
|
|
100
|
+
export { type CLIOptions, type Config, type FileResult, type FixChange, type FixResult, type LintFinding, type LintResult, computeScore, discoverContextFiles, fixFile, formatJson, formatText, lint, lintFile, loadConfig, parseFile };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { resolve as resolve4 } from "path";
|
|
3
3
|
|
|
4
4
|
// src/checkers.ts
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
5
6
|
import { existsSync, readFileSync } from "fs";
|
|
6
7
|
import { dirname, resolve } from "path";
|
|
7
8
|
function checkPaths(parsed, filePath) {
|
|
@@ -192,6 +193,134 @@ function checkContradictions(parsed, filePath) {
|
|
|
192
193
|
}
|
|
193
194
|
return findings;
|
|
194
195
|
}
|
|
196
|
+
var SHELL_BUILTINS = /* @__PURE__ */ new Set([
|
|
197
|
+
"cd",
|
|
198
|
+
"export",
|
|
199
|
+
"echo",
|
|
200
|
+
"source",
|
|
201
|
+
"set",
|
|
202
|
+
"unset",
|
|
203
|
+
"alias",
|
|
204
|
+
"unalias",
|
|
205
|
+
"type",
|
|
206
|
+
"readonly",
|
|
207
|
+
"declare",
|
|
208
|
+
"local",
|
|
209
|
+
"eval",
|
|
210
|
+
"exec",
|
|
211
|
+
"trap",
|
|
212
|
+
"return",
|
|
213
|
+
"exit",
|
|
214
|
+
"shift",
|
|
215
|
+
"wait",
|
|
216
|
+
"read",
|
|
217
|
+
"pushd",
|
|
218
|
+
"popd",
|
|
219
|
+
"dirs",
|
|
220
|
+
"ulimit",
|
|
221
|
+
"umask",
|
|
222
|
+
"getopts",
|
|
223
|
+
"hash",
|
|
224
|
+
"pwd",
|
|
225
|
+
"test",
|
|
226
|
+
"true",
|
|
227
|
+
"false",
|
|
228
|
+
"printf",
|
|
229
|
+
"let",
|
|
230
|
+
"if",
|
|
231
|
+
"then",
|
|
232
|
+
"else",
|
|
233
|
+
"fi",
|
|
234
|
+
"for",
|
|
235
|
+
"do",
|
|
236
|
+
"done",
|
|
237
|
+
"while",
|
|
238
|
+
"until",
|
|
239
|
+
"case",
|
|
240
|
+
"esac",
|
|
241
|
+
"in",
|
|
242
|
+
"function"
|
|
243
|
+
]);
|
|
244
|
+
var SHELL_LANGS = /* @__PURE__ */ new Set(["bash", "sh", "shell"]);
|
|
245
|
+
function checkCommands(parsed, filePath) {
|
|
246
|
+
const findings = [];
|
|
247
|
+
const cache = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const block of parsed.codeBlocks) {
|
|
249
|
+
if (!SHELL_LANGS.has(block.lang)) continue;
|
|
250
|
+
const lines = block.content.split("\n");
|
|
251
|
+
for (let i = 0; i < lines.length; i++) {
|
|
252
|
+
let line = lines[i].trim();
|
|
253
|
+
if (!line || line.startsWith("#")) continue;
|
|
254
|
+
if (line.startsWith("$ ") || line.startsWith("> ")) {
|
|
255
|
+
line = line.slice(2).trim();
|
|
256
|
+
}
|
|
257
|
+
if (!line) continue;
|
|
258
|
+
const cmd = line.split(/\s/)[0];
|
|
259
|
+
if (!cmd || SHELL_BUILTINS.has(cmd)) continue;
|
|
260
|
+
if (/^[A-Z_]+=/.test(cmd)) continue;
|
|
261
|
+
if (!cache.has(cmd)) {
|
|
262
|
+
try {
|
|
263
|
+
execFileSync("which", [cmd], { stdio: "pipe" });
|
|
264
|
+
cache.set(cmd, true);
|
|
265
|
+
} catch {
|
|
266
|
+
cache.set(cmd, false);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!cache.get(cmd)) {
|
|
270
|
+
findings.push({
|
|
271
|
+
file: filePath,
|
|
272
|
+
rule: "check:commands",
|
|
273
|
+
line: block.line + 1 + i,
|
|
274
|
+
column: 1,
|
|
275
|
+
severity: "warning",
|
|
276
|
+
message: `Command not found on system: "${cmd}"`
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return findings;
|
|
282
|
+
}
|
|
283
|
+
var JS_LANGS = /* @__PURE__ */ new Set(["ts", "js", "typescript", "javascript", "tsx", "jsx"]);
|
|
284
|
+
var IMPORT_FROM_RE = /(?:import\s+.*?\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
|
|
285
|
+
var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cjs"];
|
|
286
|
+
function resolveModule(base, importPath) {
|
|
287
|
+
const resolved = resolve(base, importPath);
|
|
288
|
+
if (existsSync(resolved) && !resolved.endsWith("/")) return true;
|
|
289
|
+
for (const ext of EXTENSIONS) {
|
|
290
|
+
if (existsSync(resolved + ext)) return true;
|
|
291
|
+
}
|
|
292
|
+
for (const ext of EXTENSIONS) {
|
|
293
|
+
if (existsSync(resolve(resolved, "index" + ext))) return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
function checkImports(parsed, filePath) {
|
|
298
|
+
const findings = [];
|
|
299
|
+
const baseDir = dirname(filePath);
|
|
300
|
+
for (const block of parsed.codeBlocks) {
|
|
301
|
+
if (!JS_LANGS.has(block.lang)) continue;
|
|
302
|
+
const lines = block.content.split("\n");
|
|
303
|
+
for (let i = 0; i < lines.length; i++) {
|
|
304
|
+
let match;
|
|
305
|
+
IMPORT_FROM_RE.lastIndex = 0;
|
|
306
|
+
while ((match = IMPORT_FROM_RE.exec(lines[i])) !== null) {
|
|
307
|
+
const importPath = match[1] || match[2];
|
|
308
|
+
if (!importPath.startsWith("./") && !importPath.startsWith("../")) continue;
|
|
309
|
+
if (!resolveModule(baseDir, importPath)) {
|
|
310
|
+
findings.push({
|
|
311
|
+
file: filePath,
|
|
312
|
+
rule: "check:imports",
|
|
313
|
+
line: block.line + 1 + i,
|
|
314
|
+
column: match.index + 1,
|
|
315
|
+
severity: "error",
|
|
316
|
+
message: `Import path does not resolve: "${importPath}"`
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return findings;
|
|
323
|
+
}
|
|
195
324
|
|
|
196
325
|
// src/config.ts
|
|
197
326
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
@@ -409,6 +538,41 @@ function formatJson(result, cwd) {
|
|
|
409
538
|
return JSON.stringify(output, null, 2);
|
|
410
539
|
}
|
|
411
540
|
|
|
541
|
+
// src/fixer.ts
|
|
542
|
+
import { readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
543
|
+
function fixFile(filePath) {
|
|
544
|
+
const original = readFileSync4(filePath, "utf-8");
|
|
545
|
+
const changes = [];
|
|
546
|
+
let content = original;
|
|
547
|
+
const lines = content.split("\n");
|
|
548
|
+
for (let i = 0; i < lines.length; i++) {
|
|
549
|
+
if (lines[i] !== lines[i].trimEnd()) {
|
|
550
|
+
changes.push({ line: i + 1, description: "Removed trailing whitespace" });
|
|
551
|
+
lines[i] = lines[i].trimEnd();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
content = lines.join("\n");
|
|
555
|
+
const collapsed = content.replace(/\n{3,}/g, (match, offset) => {
|
|
556
|
+
const before = content.slice(0, offset);
|
|
557
|
+
const lineNum = before.split("\n").length + 1;
|
|
558
|
+
changes.push({ line: lineNum, description: "Collapsed multiple blank lines" });
|
|
559
|
+
return "\n\n";
|
|
560
|
+
});
|
|
561
|
+
content = collapsed;
|
|
562
|
+
if (content.length > 0 && !content.endsWith("\n")) {
|
|
563
|
+
changes.push({
|
|
564
|
+
line: content.split("\n").length,
|
|
565
|
+
description: "Added trailing newline"
|
|
566
|
+
});
|
|
567
|
+
content += "\n";
|
|
568
|
+
}
|
|
569
|
+
const fixed = content !== original;
|
|
570
|
+
if (fixed) {
|
|
571
|
+
writeFileSync(filePath, content);
|
|
572
|
+
}
|
|
573
|
+
return { file: filePath, fixed, changes };
|
|
574
|
+
}
|
|
575
|
+
|
|
412
576
|
// src/index.ts
|
|
413
577
|
function lintFile(filePath, cwd) {
|
|
414
578
|
const config = loadConfig(cwd);
|
|
@@ -420,7 +584,9 @@ function lintFile(filePath, cwd) {
|
|
|
420
584
|
...checkVague(parsed, filePath, config),
|
|
421
585
|
...checkRequiredSections(parsed, filePath, config),
|
|
422
586
|
...checkStaleDates(parsed, filePath, config),
|
|
423
|
-
...checkContradictions(parsed, filePath)
|
|
587
|
+
...checkContradictions(parsed, filePath),
|
|
588
|
+
...checkCommands(parsed, filePath),
|
|
589
|
+
...checkImports(parsed, filePath)
|
|
424
590
|
];
|
|
425
591
|
return {
|
|
426
592
|
file: filePath,
|
|
@@ -451,6 +617,7 @@ function lint(cwd, files) {
|
|
|
451
617
|
export {
|
|
452
618
|
computeScore,
|
|
453
619
|
discoverContextFiles,
|
|
620
|
+
fixFile,
|
|
454
621
|
formatJson,
|
|
455
622
|
formatText,
|
|
456
623
|
lint,
|