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.
91 lines
6.2 KiB
TypeScript
91 lines
6.2 KiB
TypeScript
import path from "node:path";
|
|
import type { FastifyInstance } from "fastify";
|
|
import { z } from "zod";
|
|
import { requirePermission, isPrincipalContext } from "../auth/authorization.js";
|
|
import type { ThtRunner } from "../tht/tht-runner.js";
|
|
import type { WorkspaceRegistry } from "../workspaces/registry.js";
|
|
import type { WorkspacePreprocessingService } from "../workspaces/preprocessing-service.js";
|
|
|
|
const params = z.object({ workspaceId: z.string().regex(/^[a-z][a-z0-9-]{2,62}$/),
|
|
evidenceId: z.string().regex(/^evidence:[a-z0-9]+(?:-[a-z0-9]+)*$/).optional() });
|
|
const query = z.object({
|
|
q: z.string().max(1000).optional(), kind: z.enum(["domain", "glossary", "enum", "example", "mapping", "normalization", "formula", "reference"]).optional(),
|
|
purpose: z.enum(["disambiguation", "rewriting", "schema_linking", "sql_generation"]).optional(),
|
|
status: z.enum(["new", "modified", "active", "removed", "review_required", "legacy", "invalid"]).optional(),
|
|
concept: z.string().max(300).optional(), table: z.string().max(300).optional(), column: z.string().max(300).optional(),
|
|
source: z.string().max(300).optional(), language: z.string().max(30).optional(),
|
|
sort: z.enum(["title", "id", "kind", "status"]).optional(), direction: z.enum(["asc", "desc"]).optional(),
|
|
page: z.coerce.number().int().positive().optional(), page_size: z.coerce.number().int().min(1).max(100).optional(),
|
|
}).strict();
|
|
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
|
|
export function evidenceRoutes(app: FastifyInstance, deps: {
|
|
runner: Pick<ThtRunner, "withPrincipal">; registry: Pick<WorkspaceRegistry, "list">;
|
|
registryRoot: string; hostRegistryRoot?: string;
|
|
service: Partial<Pick<WorkspacePreprocessingService, "consolidateEvidence" | "evidenceSources">>;
|
|
}) {
|
|
app.post("/workspaces/:workspaceId/evidence/sources", async (request, reply) => {
|
|
const principal = requirePermission(request, reply, "evidence.manage");
|
|
if (!isPrincipalContext(principal)) return reply;
|
|
try {
|
|
const { workspaceId } = params.parse(request.params);
|
|
const body = z.discriminatedUnion("action", [
|
|
z.object({ action: z.literal("refresh") }).strict(),
|
|
z.object({ action: z.literal("decide"), sourceId: z.string().regex(/^[a-f0-9]{64}$/),
|
|
revision: z.string().regex(/^[a-f0-9]{64}$/), decision: z.enum(["keep", "replace"]) }).strict(),
|
|
]).parse(request.body);
|
|
if (!(await deps.registry.list()).some(w => w.id === workspaceId)) return reply.code(404).send({ code: "workspace_invalid" });
|
|
if (!deps.service.evidenceSources) throw new Error("Evidence service unavailable");
|
|
return await deps.service.evidenceSources({ workspaceId, ...body, actor: principal.subject });
|
|
} catch (error) {
|
|
return reply.code(error instanceof z.ZodError ? 400 : 503).send({ code: "evidence_unavailable",
|
|
message: error instanceof z.ZodError ? "Invalid source request." : "Evidence source service is unavailable." });
|
|
}
|
|
});
|
|
app.get<{ Params: { workspaceId: string; evidenceId?: string } }>("/workspaces/:workspaceId/evidence", read);
|
|
app.get<{ Params: { workspaceId: string; evidenceId?: string } }>("/workspaces/:workspaceId/evidence/:evidenceId", read);
|
|
async function read(request: import("fastify").FastifyRequest, reply: import("fastify").FastifyReply) {
|
|
const principal = requirePermission(request, reply, "evidence.manage");
|
|
if (!isPrincipalContext(principal)) return reply;
|
|
try {
|
|
const { workspaceId, evidenceId } = params.parse(request.params);
|
|
const filters = query.parse(request.query);
|
|
if (!(await deps.registry.list()).some(w => w.id === workspaceId)) return reply.code(404).send({ code: "workspace_invalid" });
|
|
const root = path.join(deps.registryRoot, "repo", workspaceId);
|
|
const result = await deps.runner.withPrincipal(principal).runWithRuntimeSnapshot(
|
|
["evidence", "admin", "--workspace", workspaceId],
|
|
JSON.stringify({ root, query: { ...filters, ...(evidenceId ? { id: evidenceId } : {}) } }),
|
|
);
|
|
const payload = JSON.parse(result.stdout);
|
|
if (result.code !== 0) return reply.code(503).send(payload);
|
|
if (evidenceId && !payload.item) return reply.code(404).send({ code: "evidence_not_found", message: "Evidence was not found." });
|
|
const host = deps.hostRegistryRoot;
|
|
const hostPath = host && (path.isAbsolute(host) || path.win32.isAbsolute(host))
|
|
? (path.win32.isAbsolute(host) && !path.isAbsolute(host) ? path.win32 : path).join(host, "repo") : null;
|
|
const hostJoin = hostPath && path.win32.isAbsolute(hostPath) && !path.isAbsolute(hostPath) ? path.win32.join : path.join;
|
|
return { ...payload, location: { repository: hostPath, workspace: hostPath ? hostJoin(hostPath, workspaceId) : null,
|
|
runtime_workspace: root, host: "Installation host", command: `tht workspace evidence consolidate --workspace ${workspaceId}`,
|
|
git_commands: hostPath ? [
|
|
`git -C ${quote(hostPath)} status --short -- ${quote(workspaceId + "/evidence")}`,
|
|
`git -C ${quote(hostPath)} diff -- ${quote(workspaceId + "/evidence")}`,
|
|
`git -C ${quote(hostPath)} add -A -- ${quote(workspaceId + "/evidence")}`,
|
|
`git -C ${quote(hostPath)} commit --only -m 'Curate Evidence' -- ${quote(workspaceId + "/evidence")}`,
|
|
`git -C ${quote(hostPath)} push`,
|
|
] : [] } };
|
|
} catch (error) {
|
|
return reply.code(error instanceof z.ZodError ? 400 : 503).send({ code: "evidence_unavailable",
|
|
message: error instanceof z.ZodError ? "Evidence filters are invalid." : "Evidence archive is unavailable." });
|
|
}
|
|
}
|
|
app.post("/workspaces/:workspaceId/evidence/consolidate", async (request, reply) => {
|
|
const principal = requirePermission(request, reply, "evidence.manage");
|
|
if (!isPrincipalContext(principal)) return reply;
|
|
try {
|
|
const { workspaceId } = params.parse(request.params);
|
|
if (!(await deps.registry.list()).some(w => w.id === workspaceId)) return reply.code(404).send({ code: "workspace_invalid" });
|
|
if (!deps.service.consolidateEvidence) throw new Error("Evidence service unavailable");
|
|
return await deps.service.consolidateEvidence({ workspaceId });
|
|
} catch { return reply.code(503).send({ code: "evidence_unavailable", message: "Evidence consolidation is unavailable." }); }
|
|
});
|
|
}
|