Files
zesdex/src/features/agent/infrastructure/tools/best_practice/engine.ts
T
asepharyana b5ebd07fa8 refactor: rombak arsitektur file ke modular clean-arch (vertikal per-feature)
Organisasi ulang layer-first (domain/application/infrastructure) menjadi
vertikal per-feature, behavior tidak berubah:

- src/shared/domain/  -> fondasi bersama (ex-domain/core): message, provider,
  tool_call, tool_result, store, error, usage, conversation
- src/features/agent/ -> LLM turn loop: domain (TurnEvent, AgentTurnParams) +
  application (turn_service, ports) + infrastructure (llm LlmClient + tools)
- src/features/cms/   -> settings/conversation/memory persistence: domain +
  application (services) + infrastructure (persistence repos)
- src/features/subagent/ -> orchestration: domain (AccessTier) + infrastructure
  (engine runner, context, provider, delegate, spawn_tools)
- src/features/workflow/ -> multi-phase + hive-mind: domain + infrastructure
- src/interfaces/     -> cli + tui (outer ring) tetap

Subagent-runner (engine.ts runAgent, division.ts toolsFor) dipindah ke
features/subagent (bukan agent tools) utk memutus coupling agent<->subagent.

Alias tsconfig: @zesdex/domain -> shared/domain, + @zesdex/agent,
@zesdex/agent-infra, @zesdex/cms, @zesdex/subagent, @zesdex/workflow, @zesdex/shared.
Bersihkan dead: application/ports PasswordService/TokenService/AuthService.
compose.ts (composition root) & bootstrap repoint ke barrel feature.

Gates: tsc --noEmit 0 error, bun test 70 pass/0 fail, bun build 147 modules,
./dist/zesdex --headless -> real turn OK.
2026-09-03 02:30:12 +07:00

110 lines
4.5 KiB
TypeScript

/**
* 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 };
}
}