Publish documentation / publish (push) Successful in 1m27s
Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation. Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
95 lines
5.0 KiB
TypeScript
95 lines
5.0 KiB
TypeScript
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<ThtRunner, "withPrincipal">;
|
|
registry: Pick<WorkspaceRegistry, "list" | "read">;
|
|
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<string, number> = {
|
|
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"));
|
|
}
|