Project Files
src / validation / validator.ts
import { readFile, readdir } from "node:fs/promises";
import { join, extname } from "node:path";
import { statSync } from "node:fs";
import type { ValidationReport, RuleResult } from "./types.js";
import { parseCommandFile } from "./parser.js";
import { getAllRules } from "./rules.js";
export function validateCommand(
filePath: string,
content: string,
): ValidationReport {
const cmd = parseCommandFile(filePath, content);
const rules = getAllRules();
const results: RuleResult[] = [];
for (const rule of rules) {
try {
const r = rule.check(cmd);
if (r !== null) {
results.push(r);
}
} catch {
// rule check threw unexpectedly — skip
}
}
let errorCount = 0;
let warningCount = 0;
let infoCount = 0;
for (const r of results) {
if (r.severity === "error") errorCount++;
else if (r.severity === "warning") warningCount++;
else infoCount++;
}
// Score: start at 100, subtract penalties
let score = 100;
score -= errorCount * 25;
score -= warningCount * 5;
score -= infoCount * 1;
score = Math.max(0, Math.min(100, score));
return {
file: filePath,
filename: cmd.filename,
passed: errorCount === 0,
results,
score,
errorCount,
warningCount,
infoCount,
};
}
export async function validateDirectory(
dirPath: string,
): Promise<ValidationReport[]> {
const entries = await readdir(dirPath, { withFileTypes: true });
const mdFiles = entries
.filter((e) => e.isFile() && extname(e.name).toLowerCase() === ".md")
.map((e) => join(dirPath, e.name));
const reports: ValidationReport[] = [];
for (const filePath of mdFiles) {
try {
const content = await readFile(filePath, "utf-8");
reports.push(validateCommand(filePath, content));
} catch (err) {
reports.push({
file: filePath,
filename:
filePath.split(/[\\/]/).pop()?.replace(/\.md$/i, "") || "unknown",
passed: false,
results: [
{
ruleId: "file-read-error",
severity: "error",
message: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,
},
],
score: 0,
errorCount: 1,
warningCount: 0,
infoCount: 0,
});
}
}
return reports;
}
export function isDirectory(path: string): boolean {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}