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.
This commit is contained in:
asepharyana
2026-09-03 02:30:12 +07:00
parent 7e6ed34f0d
commit b5ebd07fa8
126 changed files with 157 additions and 166 deletions
@@ -0,0 +1,107 @@
/**
* Markdown file–backed `MemoryRepository`. Each memory is a `.md` file with
* YAML-ish frontmatter. Mirrors `apps/infrastructure/src/persistence/cms/memory_repo.rs`.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { type Memory, type MemoryRepository, memoryPath } from "@zesdex/cms";
import { randomUUID } from "node:crypto";
/** Escape newlines so they do not break line-oriented frontmatter. */
function escapeNewlines(s: string): string {
return s.replace(/\n/g, "\\n");
}
function unescapeNewlines(s: string): string {
return s.replace(/\\n/g, "\n");
}
function buildFrontmatter(memory: Memory): string {
const outcome = memory.outcome ? `outcome: ${escapeNewlines(memory.outcome)}\n` : "";
const scope = memory.scope ? `scope: ${escapeNewlines(memory.scope)}\n` : "";
const before = memory.before_snippet ? `before: ${escapeNewlines(memory.before_snippet)}\n` : "";
const after = memory.after_snippet ? `after: ${escapeNewlines(memory.after_snippet)}\n` : "";
const prov = memory.provenances.length > 0 ? `provenances: ${memory.provenances.join(", ")}\n` : "";
return `name: ${memory.name}\ndescription: ${memory.description}\nkind: ${memory.kind}\ncreated_at: ${memory.created_at}\nupdated_at: ${memory.updated_at}\nlifecycle: ${memory.lifecycle}\n${outcome}${scope}${before}${after}${prov}`;
}
function parseFrontmatter(front: string): Record<string, string> {
const map: Record<string, string> = {};
for (const line of front.split("\n")) {
const idx = line.indexOf(":");
if (idx === -1) continue;
map[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
}
return map;
}
function parseMemory(content: string): Memory {
let body = content;
if (body.startsWith("---\n")) body = body.slice(4);
const parts = body.split("\n---\n", 2);
const frontPart = parts[0];
const mdPart = parts[1];
if (frontPart === undefined || mdPart === undefined) throw new Error("missing frontmatter");
const front = parseFrontmatter(frontPart);
const md = mdPart.trim();
const prov = front.provenances ?? "";
return {
name: front.name ?? "",
description: front.description ?? "",
content: md,
kind: front.kind ?? "reference",
created_at: Number(front.created_at) || 0,
updated_at: Number(front.updated_at) || 0,
outcome: front.outcome ? unescapeNewlines(front.outcome) : undefined,
lifecycle: front.lifecycle ?? "new",
scope: front.scope ? unescapeNewlines(front.scope) : undefined,
before_snippet: front.before ? unescapeNewlines(front.before) : undefined,
after_snippet: front.after ? unescapeNewlines(front.after) : undefined,
provenances: prov === "" ? [] : prov.split(", "),
};
}
/** File-based `MemoryRepository` storing memories as `.md` files. */
export class MarkdownMemoryRepository implements MemoryRepository {
async list(memoryDir: string): Promise<string[]> {
let entries: string[];
try {
entries = fs.readdirSync(memoryDir);
} catch {
return [];
}
return entries
.filter((n) => n.endsWith(".md") && n !== "MEMORY.md")
.map((n) => n.slice(0, -3));
}
async load(memoryDir: string, name: string): Promise<Memory> {
const file = memoryPath(memoryDir, name);
const content = fs.readFileSync(file, "utf8");
try {
return parseMemory(content);
} catch (e) {
throw new Error(`failed to parse memory '${name}': ${(e as Error).message}`);
}
}
async save(memoryDir: string, memory: Memory): Promise<void> {
const file = memoryPath(memoryDir, memory.name);
fs.mkdirSync(path.dirname(file), { recursive: true });
const content = `---\n${buildFrontmatter(memory)}---\n\n${memory.content}`;
const tmp = path.join(path.dirname(file), `.${randomUUID()}.tmp`);
const fd = fs.openSync(tmp, "w");
try {
fs.writeFileSync(fd, content);
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
fs.renameSync(tmp, file);
}
async delete(memoryDir: string, name: string): Promise<void> {
const file = memoryPath(memoryDir, name);
if (fs.existsSync(file)) fs.unlinkSync(file);
}
}