Files
zesdex/src/features/agent/infrastructure/tools/best_practice/engine.ts
T

109 lines
4.5 KiB
TypeScript
Raw Normal View History

/**
* Best-practice engine — embedded skills, Conventional Commits validation,
* and lightweight architecture/quality scans. Mirrors `crate::best_practice`.
*/
import * as fs from "node:fs";
import * as path from "node:path";
interface Skill {
name: string;
description: string;
content: string;
}
const SKILLS: Skill[] = [
{
name: "clean-code",
description: "Clean Code principles and naming conventions",
content:
"Clean Code:\n- Use meaningful, intention-revealing names.\n- Functions should do one thing and be short.\n- Avoid duplication (DRY).\n- Comments should explain 'why', not 'what'.\n- Favor composition over deep inheritance.",
},
{
name: "commit-convention",
description: "Conventional Commits (Bahasa Indonesia) rules",
content:
"Conventional Commits:\n- Format: <type>(<scope>): <subject>\n- Types: feat, fix, chore, docs, refactor, test, style, perf, ci, build, revert\n- Subject: imperative, lowercase, no trailing period.\n- Example: 'feat(auth): tambah login PKCE'",
},
{
name: "layering",
description: "Clean Architecture layering rules (domain → application → infrastructure → interfaces)",
content:
"Clean Architecture:\n- domain: pure types, zero I/O.\n- application: ports + use cases.\n- infrastructure: concrete adapters.\n- interfaces: presentation.\n- Dependencies point inward; domain depends on nothing.",
},
];
const COMMIT_TYPES = ["feat", "fix", "chore", "docs", "refactor", "test", "style", "perf", "ci", "build", "revert"];
export class BestPracticeEngine {
listSkills(): string[] {
return SKILLS.map((s) => s.name);
}
skillSummaries(): Array<[string, string]> {
return SKILLS.map((s) => [s.name, s.description] as [string, string]);
}
getSkill(name: string): string | undefined {
return SKILLS.find((s) => s.name === name)?.content;
}
parseCommit(msg: string): { type_: string; scope: string | null; breaking: boolean; description: string } | null {
const m = /^([a-z]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/.exec(msg.trim().split("\n")[0] ?? "");
if (!m) return null;
return { type_: m[1]!, scope: m[2] ?? null, breaking: m[3] === "!", description: (m[4] ?? "").trim() };
}
validateCommit(msg: string): string[] {
const errors: string[] = [];
const first = (msg.trim().split("\n")[0] ?? "").trim();
if (first === "") { errors.push("Commit message is empty."); return errors; }
const parsed = this.parseCommit(first);
if (!parsed) {
errors.push("Does not match Conventional Commits: <type>(<scope>): <subject>");
return errors;
}
if (!COMMIT_TYPES.includes(parsed.type_)) {
errors.push(`Unknown type '${parsed.type_}'. Valid types: ${COMMIT_TYPES.join(", ")}`);
}
if (parsed.description === "") {
errors.push("Subject is empty.");
} else if (!/^[a-z]/.test(parsed.description)) {
errors.push("Subject should start with a lowercase letter.");
}
if (parsed.description.endsWith(".")) {
errors.push("Subject should not end with a period.");
}
if (first.length > 72) {
errors.push("Subject is longer than 72 characters.");
}
return errors;
}
suggestCommitTemplate(type_: string, scope: string | null): string {
if (!COMMIT_TYPES.includes(type_)) type_ = "feat";
return `${type_}${scope ? `(${scope})` : ""}: <imperative description>`;
}
auditLayering(ws: string): { filesScanned: number; violations: Array<{ severity: string; file: string; line: number; message: string }> } {
const violations: Array<{ severity: string; file: string; line: number; message: string }> = [];
let filesScanned = 0;
const scan = (dir: string) => {
let entries: fs.Dirent[];
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
for (const e of entries) {
if (e.name === ".git" || e.name === "node_modules" || e.name === "target") continue;
const full = path.join(dir, e.name);
if (e.isDirectory()) scan(full);
else if (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) {
filesScanned++;
const lines = fs.readFileSync(full, "utf8").split("\n");
lines.forEach((line, i) => {
if (line.includes("@zesdex/infrastructure") && full.includes("/domain/")) {
violations.push({ severity: "error", file: full, line: i + 1, message: "domain must not depend on infrastructure" });
}
});
}
}
};
scan(ws);
return { filesScanned, violations };
}
}