import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; import { isPrincipalContext, requirePermission } from "../auth/authorization.js"; import type { ThtRunner } from "../tht/tht-runner.js"; import type { WorkspaceRegistry } from "../workspaces/registry.js"; import type { SemanticRuntimeConfig } from "../workspaces/runtime-renderer.js"; const paramsSchema = z.object({ workspaceId: z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/), cardId: z.string().regex(/^mem-[0-9a-f-]{36}$/).optional(), }); const family = z.enum(["domain_clarification", "sql_rule", "solved_question", "explained_error"]); const cardSchema = z.object({ family, subject: z.string().trim().min(1).max(1000), detail: z.string().max(50000).default(""), scope: z.string().trim().min(1).max(10000), rationale: z.string().max(10000).default(""), question: z.string().max(10000).default(""), sql: z.string().max(100000).default(""), concepts: z.array(z.string().trim().min(1).max(200)).max(100).default([]), dependencies: z.array(z.object({ database: z.string().trim().min(1).max(200), schema_name: z.string().max(200).default(""), table: z.string().max(200).default(""), column: z.string().max(200).default(""), }).strict()).max(200).default([]), links: z.array(z.object({ target_id: z.string().min(1).max(100), meaning: z.string().trim().min(1).max(1000), }).strict()).max(200).default([]), }).strict(); const querySchema = z.object({ q: z.string().max(1000).optional(), family: family.optional(), concept: z.string().max(200).optional(), database: z.string().max(200).optional(), table: z.string().max(200).optional(), column: z.string().max(200).optional(), origin: z.enum(["manual", "workflow"]).optional(), updated_after: z.iso.datetime({ offset: true }).optional(), updated_before: z.iso.datetime({ offset: true }).optional(), page: z.coerce.number().int().positive().optional(), page_size: z.coerce.number().int().min(1).max(100).optional(), sort: z.enum(["updated_at", "created_at", "subject", "family"]).optional(), direction: z.enum(["asc", "desc"]).optional(), }).strict(); export function memoryRoutes(app: FastifyInstance, deps: { runner: Pick; registry: Pick; runtime: SemanticRuntimeConfig; }) { const invoke = (action: string) => async (request: FastifyRequest, reply: FastifyReply) => { const principal = requirePermission(request, reply, "memory.manage"); if (!isPrincipalContext(principal)) return reply; try { const { workspaceId, cardId } = paramsSchema.parse(request.params); const input = action === "list" ? querySchema.parse(request.query) : action === "create" || action === "update" ? { card: cardSchema.parse(request.body), ...(cardId ? { id: cardId } : {}) } : cardId ? { id: cardId } : {}; // Registry identity is enough: administrative browsing must not require a DWH binding. const workspaces = await deps.registry.list(); if (!workspaces.some(workspace => workspace.id === workspaceId)) { return reply.code(404).send({ code: "workspace_invalid", message: "Workspace was not found." }); } const { workspace } = await deps.registry.read(workspaceId); const result = await deps.runner.withPrincipal(principal).runWithRuntimeSnapshot( ["memory", "admin", "--workspace", workspaceId], JSON.stringify({ action, request: input, runtime: { ...deps.runtime, memoryLanguage: workspace.workspace.language, } }), ); let payload; try { payload = JSON.parse(result.stdout); } catch { return reply.code(503).send({ code: "memory_unavailable", message: "Memory archive is unavailable." }); } if (result.code !== 0) { const codes: Record = { memory_invalid: 400, memory_forbidden: 403, memory_not_found: 404, memory_conflict: 409, memory_unavailable: 503, }; const code = typeof payload?.code === "string" && payload.code in codes ? payload.code : "memory_unavailable"; return reply.code(codes[code]).send({ code, message: "Memory operation could not be completed." }); } return reply.code(action === "create" ? 201 : 200).send(payload); } catch (error) { if (error instanceof z.ZodError) { return reply.code(400).send({ code: "memory_invalid", message: "Memory request is invalid." }); } return reply.code(503).send({ code: "memory_unavailable", message: "Memory archive is unavailable." }); } }; app.get("/workspaces/:workspaceId/memory", invoke("list")); app.post("/workspaces/:workspaceId/memory", invoke("create")); app.get("/workspaces/:workspaceId/memory/pending", invoke("pending")); app.get("/workspaces/:workspaceId/memory/:cardId", invoke("show")); app.put("/workspaces/:workspaceId/memory/:cardId", invoke("update")); app.delete("/workspaces/:workspaceId/memory/:cardId", invoke("delete")); app.post("/workspaces/:workspaceId/memory/:cardId/retry", invoke("retry")); }