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.
110 lines
4.5 KiB
TypeScript
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 };
|
|
}
|
|
}
|