/** * 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: (): \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: (): "); 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})` : ""}: `; } 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 }; } }