import type { Prisma } from "@prisma/client"; import { ApprovalState, EvidenceStrength, VerificationState } from "@prisma/client"; import { prisma } from "../db/prisma.js"; import type { KnowledgeItemDto, KnowledgeListFilters, KnowledgeMutationRequestDto, MemoryRuleDto, MemoryRuleListFilters, MemoryRuleMutationRequestDto, } from "../types/api.js"; export type DocumentationItemDto = { id: string; projectId: string; title: string; kind: string; status: string; linkedFeatureIds: string[]; lastUpdated?: string; primaryContextPackId?: string; selectedContextPackIds?: string[]; contextSelectionSummary?: string; approvalState: "pending" | "approved" | "rejected" | "blocked"; content?: string; createdAt: string; updatedAt: string; }; type ApprovalStateInput = ApprovalState | "pending" | "approved" | "rejected" | "blocked"; export type DocumentationListFilters = { kind?: string[]; status?: string[]; approvalState?: string[]; search?: string; }; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; const toStringArray = (value: unknown): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; const toJson = (value: unknown): Prisma.InputJsonValue => JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue; export const projectExists = async (projectIdOrSlug: string): Promise => { const project = await prisma.project.findFirst({ where: { OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }, { readableId: projectIdOrSlug }], deletedAt: null }, select: { id: true }}); return Boolean(project); }; const getProject = async (projectIdOrSlug: string) => { const project = await prisma.project.findFirst({ where: { OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }, { readableId: projectIdOrSlug }], deletedAt: null }}); if (!project) throw new Error(`Project '${projectIdOrSlug}' not found`); return project; }; const metadata = (value: Prisma.JsonValue | null | undefined): Record => isRecord(value) ? value : {}; const trust = (source: string, confidence: number, approvalState: ApprovalState, createdAt: Date) => ({ source, confidence, evidence: [], lastChecked: createdAt.toISOString(), approvalState, }); export const toKnowledgeDto = (item: { slug: string | null; readableId: string | null; project: { slug: string }; title: string; category: string; content: string; source: string; confidence: number; metadataJson: Prisma.JsonValue | null; trustJson: Prisma.JsonValue | null; approvalState: ApprovalState; createdAt: Date; updatedAt: Date; }): KnowledgeItemDto => { const meta = metadata(item.metadataJson); const trustJson = isRecord(item.trustJson) ? item.trustJson : trust(item.source, item.confidence, item.approvalState, item.updatedAt); return { id: item.slug ?? item.readableId ?? item.title, projectId: item.project.slug, title: item.title, category: item.category, content: item.content, linkedFeatureIds: toStringArray(meta.linkedFeatureIds), source: item.source, confidence: item.confidence, includeInContextPacks: toStringArray(meta.includeInContextPacks), trust: trustJson as KnowledgeItemDto["trust"], createdAt: item.createdAt.toISOString(), updatedAt: item.updatedAt.toISOString(), }; }; export const toMemoryRuleDto = (rule: { slug: string | null; readableId: string | null; project: { slug: string }; title: string; rule: string; scope: string; category: string; severity: string; includeInContextPacks: boolean; confidence: number; trustJson: Prisma.JsonValue | null; provenanceJson: Prisma.JsonValue | null; approvalState: ApprovalState; createdAt: Date; updatedAt: Date; }): MemoryRuleDto => { const meta = metadata(rule.provenanceJson); const trustJson = isRecord(rule.trustJson) ? rule.trustJson : trust("database memory rule", rule.confidence, rule.approvalState, rule.updatedAt); return { id: rule.slug ?? rule.readableId ?? rule.title, projectId: rule.project.slug, title: rule.title, rule: rule.rule, scope: rule.scope as MemoryRuleDto["scope"], category: rule.category as MemoryRuleDto["category"], severity: rule.severity as MemoryRuleDto["severity"], linkedFeatureIds: toStringArray(meta.linkedFeatureIds), includeInContextPacks: toStringArray(meta.includeInContextPacks), includeInContextPacksDefault: rule.includeInContextPacks, trust: trustJson as MemoryRuleDto["trust"], createdAt: rule.createdAt.toISOString(), updatedAt: rule.updatedAt.toISOString(), }; }; export const toDocumentationDto = (doc: { slug: string | null; readableId: string | null; project: { slug: string }; title: string; kind: string; status: string; approvalState: ApprovalState; content: string | null; metadataJson: Prisma.JsonValue | null; createdAt: Date; updatedAt: Date; }): DocumentationItemDto => { const meta = metadata(doc.metadataJson); return { id: doc.slug ?? doc.readableId ?? doc.title, projectId: doc.project.slug, title: doc.title, kind: doc.kind, status: doc.status, linkedFeatureIds: toStringArray(meta.linkedFeatureIds), lastUpdated: typeof meta.lastUpdated === "string" ? meta.lastUpdated : doc.updatedAt.toISOString(), primaryContextPackId: typeof meta.primaryContextPackId === "string" ? meta.primaryContextPackId : undefined, selectedContextPackIds: toStringArray(meta.selectedContextPackIds), contextSelectionSummary: typeof meta.contextSelectionSummary === "string" ? meta.contextSelectionSummary : undefined, approvalState: doc.approvalState, content: doc.content ?? undefined, createdAt: doc.createdAt.toISOString(), updatedAt: doc.updatedAt.toISOString(), }; }; const filterSearch = (haystack: string, search?: string) => !search || haystack.toLowerCase().includes(search.toLowerCase()); export const listKnowledgeForProject = async (projectId: string, filters: KnowledgeListFilters = {}): Promise => { const project = await getProject(projectId); const rows = await prisma.knowledgeItem.findMany({ where: { projectId: project.id, deletedAt: null }, include: { project: { select: { slug: true } } }, orderBy: { updatedAt: "desc" }}); return rows.map(toKnowledgeDto).filter((item) => (!filters.category?.length || filters.category.includes(item.category)) && (!filters.source?.length || filters.source.includes(item.source)) && (filters.confidence === undefined || item.confidence === filters.confidence) && (!filters.featureId || item.linkedFeatureIds.includes(filters.featureId)) && filterSearch(`${item.title} ${item.content} ${item.category}`, filters.search) ); }; export const getKnowledgeBySlugOrId = async (projectId: string, knowledgeId: string): Promise => { const project = await getProject(projectId); const row = await prisma.knowledgeItem.findFirst({ where: { projectId: project.id, OR: [{ id: knowledgeId }, { slug: knowledgeId }, { readableId: knowledgeId }], deletedAt: null }, include: { project: { select: { slug: true } } }}); return row ? toKnowledgeDto(row) : undefined; }; export const createKnowledgeItem = async (projectId: string, body: KnowledgeMutationRequestDto & { title: string; content: string }): Promise => { const project = await getProject(projectId); const now = new Date(); const confidence = body.confidence ?? 75; const slug = `kn-${project.slug}-${Date.now().toString(36)}`; const row = await prisma.knowledgeItem.create({ data: { projectId: project.id, slug, readableId: slug, title: body.title.trim(), category: body.category?.trim() || "Technical", content: body.content.trim(), source: body.source?.trim() || "manual", confidence, includeInContextPacks: false, approvalState: ApprovalState.pending, evidenceStrength: EvidenceStrength.medium, verificationState: VerificationState.unverified, metadataJson: toJson({ linkedFeatureIds: body.linkedFeatureIds ?? [], includeInContextPacks: [] }), trustJson: toJson(trust("database knowledge create", confidence, ApprovalState.pending, now)), createdBy: "api", updatedBy: "api", sourceType: "database_api" }, include: { project: { select: { slug: true } } }}); return toKnowledgeDto(row); }; export const updateKnowledgeItem = async (projectId: string, knowledgeId: string, body: KnowledgeMutationRequestDto): Promise => { const project = await getProject(projectId); const current = await prisma.knowledgeItem.findFirst({ where: { projectId: project.id, OR: [{ id: knowledgeId }, { slug: knowledgeId }, { readableId: knowledgeId }], deletedAt: null }}); if (!current) return undefined; const meta = metadata(current.metadataJson); const confidence = body.confidence ?? current.confidence; const row = await prisma.knowledgeItem.update({ where: { id: current.id }, data: { ...(body.title !== undefined ? { title: body.title.trim() } : {}), ...(body.category !== undefined ? { category: body.category.trim() } : {}), ...(body.content !== undefined ? { content: body.content.trim() } : {}), ...(body.source !== undefined ? { source: body.source.trim() } : {}), ...(body.confidence !== undefined ? { confidence } : {}), ...(body.linkedFeatureIds !== undefined ? { metadataJson: toJson({ ...meta, linkedFeatureIds: body.linkedFeatureIds }) } : {}), updatedBy: "api", trustJson: toJson(trust("database knowledge update", confidence, current.approvalState, new Date())) }, include: { project: { select: { slug: true } } }}); return toKnowledgeDto(row); }; export const includeKnowledgeInContextPack = async (projectId: string, knowledgeId: string, contextPackId: string): Promise => { const project = await getProject(projectId); const contextPack = await prisma.contextPack.findFirst({ where: { projectId: project.id, OR: [{ id: contextPackId }, { slug: contextPackId }, { readableId: contextPackId }], deletedAt: null }}); if (!contextPack) throw new Error("CONTEXT_PACK_NOT_FOUND"); const current = await prisma.knowledgeItem.findFirst({ where: { projectId: project.id, OR: [{ id: knowledgeId }, { slug: knowledgeId }, { readableId: knowledgeId }], deletedAt: null }}); if (!current) return undefined; const meta = metadata(current.metadataJson); const list = new Set(toStringArray(meta.includeInContextPacks)); list.add(contextPack.slug ?? contextPack.readableId ?? contextPack.id); const row = await prisma.knowledgeItem.update({ where: { id: current.id }, data: { includeInContextPacks: true, metadataJson: toJson({ ...meta, includeInContextPacks: [...list] }), updatedBy: "api" }, include: { project: { select: { slug: true } } }}); return toKnowledgeDto(row); }; export const listMemoryRulesForProject = async (projectId: string, filters: MemoryRuleListFilters = {}): Promise => { const project = await getProject(projectId); const rows = await prisma.memoryRule.findMany({ where: { projectId: project.id, deletedAt: null }, include: { project: { select: { slug: true } } }, orderBy: { updatedAt: "desc" }}); return rows.map(toMemoryRuleDto).filter((item) => (!filters.scope?.length || filters.scope.includes(item.scope)) && (!filters.category?.length || filters.category.includes(item.category)) && (!filters.severity?.length || filters.severity.includes(item.severity)) && (filters.includeInContextPacks === undefined || item.includeInContextPacksDefault === filters.includeInContextPacks) && (!filters.featureId || item.linkedFeatureIds.includes(filters.featureId)) && filterSearch(`${item.title} ${item.rule} ${item.category}`, filters.search) ); }; export const getMemoryRuleBySlugOrId = async (projectId: string, ruleId: string): Promise => { const project = await getProject(projectId); const row = await prisma.memoryRule.findFirst({ where: { projectId: project.id, OR: [{ id: ruleId }, { slug: ruleId }, { readableId: ruleId }], deletedAt: null }, include: { project: { select: { slug: true } } }}); return row ? toMemoryRuleDto(row) : undefined; }; export const createMemoryRuleItem = async (projectId: string, body: MemoryRuleMutationRequestDto & { rule: string }): Promise => { const project = await getProject(projectId); const slug = `R-${project.slug.toUpperCase()}-${Date.now().toString(36)}`; const confidence = 78; const now = new Date(); const row = await prisma.memoryRule.create({ data: { projectId: project.id, slug, readableId: slug, title: body.title?.trim() || body.rule.trim().slice(0, 60), rule: body.rule.trim(), scope: body.scope ?? "global", category: body.category ?? "Technical", severity: body.severity ?? "important", includeInContextPacks: body.includeInContextPacks ?? false, confidence, approvalState: ApprovalState.pending, evidenceStrength: EvidenceStrength.medium, verificationState: VerificationState.unverified, provenanceJson: toJson({ linkedFeatureIds: body.linkedFeatureIds ?? [], includeInContextPacks: [] }), trustJson: toJson(trust("database memory rule create", confidence, ApprovalState.pending, now)), createdBy: "api", updatedBy: "api", sourceType: "database_api" }, include: { project: { select: { slug: true } } }}); return toMemoryRuleDto(row); }; export const updateMemoryRuleItem = async (projectId: string, ruleId: string, body: MemoryRuleMutationRequestDto): Promise => { const project = await getProject(projectId); const current = await prisma.memoryRule.findFirst({ where: { projectId: project.id, OR: [{ id: ruleId }, { slug: ruleId }, { readableId: ruleId }], deletedAt: null }}); if (!current) return undefined; const meta = metadata(current.provenanceJson); const row = await prisma.memoryRule.update({ where: { id: current.id }, data: { ...(body.title !== undefined ? { title: body.title.trim() } : {}), ...(body.rule !== undefined ? { rule: body.rule.trim() } : {}), ...(body.scope !== undefined ? { scope: body.scope } : {}), ...(body.category !== undefined ? { category: body.category } : {}), ...(body.severity !== undefined ? { severity: body.severity } : {}), ...(body.includeInContextPacks !== undefined ? { includeInContextPacks: body.includeInContextPacks } : {}), ...(body.linkedFeatureIds !== undefined ? { provenanceJson: toJson({ ...meta, linkedFeatureIds: body.linkedFeatureIds }) } : {}), updatedBy: "api", trustJson: toJson(trust("database memory rule update", current.confidence, current.approvalState, new Date())) }, include: { project: { select: { slug: true } } }}); return toMemoryRuleDto(row); }; export const includeMemoryRuleInContextPack = async (projectId: string, ruleId: string, contextPackId: string): Promise => { const project = await getProject(projectId); const contextPack = await prisma.contextPack.findFirst({ where: { projectId: project.id, OR: [{ id: contextPackId }, { slug: contextPackId }, { readableId: contextPackId }], deletedAt: null }}); if (!contextPack) throw new Error("CONTEXT_PACK_NOT_FOUND"); const current = await prisma.memoryRule.findFirst({ where: { projectId: project.id, OR: [{ id: ruleId }, { slug: ruleId }, { readableId: ruleId }], deletedAt: null }}); if (!current) return undefined; const meta = metadata(current.provenanceJson); const list = new Set(toStringArray(meta.includeInContextPacks)); list.add(contextPack.slug ?? contextPack.readableId ?? contextPack.id); const row = await prisma.memoryRule.update({ where: { id: current.id }, data: { includeInContextPacks: true, provenanceJson: toJson({ ...meta, includeInContextPacks: [...list] }), updatedBy: "api" }, include: { project: { select: { slug: true } } }}); return toMemoryRuleDto(row); }; export const listDocumentationForProject = async (projectId: string, filters: DocumentationListFilters = {}): Promise => { const project = await getProject(projectId); const rows = await prisma.documentationItem.findMany({ where: { projectId: project.id, deletedAt: null }, include: { project: { select: { slug: true } } }, orderBy: { updatedAt: "desc" }}); return rows.map(toDocumentationDto).filter((item) => (!filters.kind?.length || filters.kind.includes(item.kind)) && (!filters.status?.length || filters.status.includes(item.status)) && (!filters.approvalState?.length || filters.approvalState.includes(item.approvalState)) && filterSearch(`${item.title} ${item.kind} ${item.content ?? ""}`, filters.search) ); }; export const getDocumentationBySlugOrId = async (projectId: string, docId: string): Promise => { const project = await getProject(projectId); const row = await prisma.documentationItem.findFirst({ where: { projectId: project.id, OR: [{ id: docId }, { slug: docId }, { readableId: docId }], deletedAt: null }, include: { project: { select: { slug: true } } }}); return row ? toDocumentationDto(row) : undefined; }; export const createDocumentationItem = async (projectId: string, body: { title: string; kind?: string; status?: string; linkedFeatureIds?: string[]; content?: string; approvalState?: ApprovalStateInput; primaryContextPackId?: string; selectedContextPackIds?: string[]; contextSelectionSummary?: string }): Promise => { const project = await getProject(projectId); const slug = `doc-${project.slug}-${Date.now().toString(36)}`; const now = new Date(); const row = await prisma.documentationItem.create({ data: { projectId: project.id, slug, readableId: slug, title: body.title.trim(), kind: body.kind?.trim() || "current state report", status: body.status?.trim() || "draft", approvalState: body.approvalState ?? ApprovalState.pending, content: body.content?.trim(), metadataJson: toJson({ linkedFeatureIds: body.linkedFeatureIds ?? [], lastUpdated: now.toISOString(), primaryContextPackId: body.primaryContextPackId, selectedContextPackIds: body.selectedContextPackIds ?? [], contextSelectionSummary: body.contextSelectionSummary }), trustJson: toJson(trust("database documentation create", 75, body.approvalState ?? ApprovalState.pending, now)), provenanceJson: toJson({ source: "database_api" }), createdBy: "api", updatedBy: "api", sourceType: "database_api" }, include: { project: { select: { slug: true } } }}); return toDocumentationDto(row); }; export const updateDocumentationItem = async (projectId: string, docId: string, body: { title?: string; kind?: string; status?: string; linkedFeatureIds?: string[]; content?: string; approvalState?: ApprovalStateInput; primaryContextPackId?: string; selectedContextPackIds?: string[]; contextSelectionSummary?: string }): Promise => { const project = await getProject(projectId); const current = await prisma.documentationItem.findFirst({ where: { projectId: project.id, OR: [{ id: docId }, { slug: docId }, { readableId: docId }], deletedAt: null }}); if (!current) return undefined; const meta = metadata(current.metadataJson); const row = await prisma.documentationItem.update({ where: { id: current.id }, data: { ...(body.title !== undefined ? { title: body.title.trim() } : {}), ...(body.kind !== undefined ? { kind: body.kind.trim() } : {}), ...(body.status !== undefined ? { status: body.status.trim() } : {}), ...(body.approvalState !== undefined ? { approvalState: body.approvalState } : {}), ...(body.content !== undefined ? { content: body.content.trim() } : {}), metadataJson: toJson({ ...meta, ...(body.linkedFeatureIds !== undefined ? { linkedFeatureIds: body.linkedFeatureIds } : {}), ...(body.primaryContextPackId !== undefined ? { primaryContextPackId: body.primaryContextPackId } : {}), ...(body.selectedContextPackIds !== undefined ? { selectedContextPackIds: body.selectedContextPackIds } : {}), ...(body.contextSelectionSummary !== undefined ? { contextSelectionSummary: body.contextSelectionSummary } : {}), lastUpdated: new Date().toISOString(), }), updatedBy: "api" }, include: { project: { select: { slug: true } } }}); return toDocumentationDto(row); };