import { ApprovalState, EvidenceStrength, VerificationState, type Prisma } from "@prisma/client"; import { prisma } from "../db/prisma.js"; import type { AcceptedTruthItemDto, AcceptedTruthKind, AcceptedTruthListFilters, AcceptedTruthStatus, CreateOperatingLogEntryRequestDto, EvidenceStrength as EvidenceStrengthDto, OperatingLogEntryDto, OperatingLogListFilters, ProjectUpdateDto, ProjectUpdateListFilters, ProjectUpdateTargetArea, ReviewMutationResponseDto, } from "../types/api.js"; type ProjectRecord = { id: string; slug: string }; type ReviewRecord = Prisma.ReviewQueueItemGetPayload>; type TruthRecord = Prisma.AcceptedTruthItemGetPayload>; type UpdateRecord = Prisma.ProjectUpdateGetPayload>; type LogRecord = Prisma.OperatingLogEntryGetPayload>; const findProjectRecord = (projectIdOrSlug: string): Promise => prisma.project.findFirst({ where: { deletedAt: null, OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }], }, select: { id: true, slug: true }, }); const findReviewRecord = async ( projectId: string, reviewItemId: string, ): Promise => prisma.reviewQueueItem.findFirst({ where: { projectId, deletedAt: null, OR: [{ id: reviewItemId }, { slug: reviewItemId }, { readableId: reviewItemId }], }, }); const toEvidenceStrengthEnum = (value: EvidenceStrengthDto | string | null | undefined): EvidenceStrength => { if (value === "verified") return EvidenceStrength.verified; if (value === "strong") return EvidenceStrength.strong; if (value === "medium") return EvidenceStrength.medium; return EvidenceStrength.weak; }; const toEvidenceStrengthDto = (value: EvidenceStrength): EvidenceStrengthDto => value; const normalizeKind = (type: string): AcceptedTruthKind => { if (type === "memory_rule") return "memory_rule"; if (type === "documentation_update") return "documentation"; if (type === "open_question") return "open_question"; if (type === "feature") return "feature"; if (type === "bug") return "bug"; if (type === "task") return "task"; if (type === "decision") return "decision"; if (type === "knowledge") return "knowledge"; if (type === "risk") return "risk"; if (type === "assumption") return "assumption"; return "knowledge"; }; const targetAreaForKind = (kind: AcceptedTruthKind): ProjectUpdateTargetArea => { if (kind === "feature") return "feature-map"; if (kind === "bug") return "bugs"; if (kind === "task") return "tasks"; if (kind === "decision") return "decisions"; if (kind === "memory_rule") return "memory"; if (kind === "documentation") return "documentation"; if (kind === "risk") return "risks"; if (kind === "open_question") return "questions"; return "knowledge"; }; const toTruthDto = (record: TruthRecord, projectSlug: string): AcceptedTruthItemDto => ({ id: record.slug ?? record.readableId ?? record.id, projectId: projectSlug, sourceReviewItemId: record.sourceReviewItemId, kind: record.kind as AcceptedTruthKind, title: record.title, summary: record.summary, status: record.status as AcceptedTruthStatus, source: record.source, confidence: record.confidence, evidenceStrength: toEvidenceStrengthDto(record.evidenceStrength), createdAt: record.createdAt.toISOString(), updatedAt: record.updatedAt.toISOString(), }); const toProjectUpdateDto = (record: UpdateRecord, projectSlug: string): ProjectUpdateDto => ({ id: record.slug ?? record.readableId ?? record.id, projectId: projectSlug, sourceTruthItemId: record.sourceTruthItemId, kind: record.kind as AcceptedTruthKind, title: record.title, summary: record.summary, targetArea: record.targetArea as ProjectUpdateTargetArea, status: record.status as ProjectUpdateDto["status"], confidence: record.confidence, evidenceStrength: toEvidenceStrengthDto(record.evidenceStrength), createdAt: record.createdAt.toISOString(), updatedAt: record.updatedAt.toISOString(), ...(record.appliedAt ? { appliedAt: record.appliedAt.toISOString() } : {}), }); const linkedIdsFromJson = (value: Prisma.JsonValue): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; const toLogDto = (record: LogRecord, projectSlug: string): OperatingLogEntryDto => ({ id: record.slug ?? record.readableId ?? record.id, projectId: projectSlug, timestamp: record.timestamp.toISOString(), type: record.type as OperatingLogEntryDto["type"], title: record.title, summary: record.summary, linkedEntityIds: linkedIdsFromJson(record.linkedEntityIdsJson), source: record.source, importance: record.importance as OperatingLogEntryDto["importance"], createdAt: record.createdAt.toISOString(), updatedAt: record.updatedAt.toISOString(), }); const paginateMeta = ( items: T[], total: number, page: number, pageSize: number, sort?: string, filters?: Record, ) => ({ items, total, page, pageSize, hasNextPage: page < Math.ceil(total / pageSize), hasPreviousPage: page > 1, ...(sort ? { sort } : {}), ...(filters ? { filters: filters as Record } : {}), }); export const listAcceptedTruthFromDatabase = async ( projectIdOrSlug: string, filters: AcceptedTruthListFilters, options: { page: number; pageSize: number; sort?: string }, ) => { const project = await findProjectRecord(projectIdOrSlug); if (!project) return undefined; const where: Prisma.AcceptedTruthItemWhereInput = { projectId: project.id, deletedAt: null, ...(filters.kind?.length ? { kind: { in: filters.kind } } : {}), ...(filters.status?.length ? { status: { in: filters.status } } : {}), ...(filters.confidence !== undefined ? { confidence: filters.confidence } : {}), ...(filters.evidenceStrength?.length ? { evidenceStrength: { in: filters.evidenceStrength.map(toEvidenceStrengthEnum) } } : {}), ...(filters.sourceReviewItemId ? { sourceReviewItemId: filters.sourceReviewItemId } : {}), }; const total = await prisma.acceptedTruthItem.count({ where }); const records = await prisma.acceptedTruthItem.findMany({ where, orderBy: { createdAt: options.sort?.endsWith(":asc") ? "asc" : "desc" }, skip: (options.page - 1) * options.pageSize, take: options.pageSize, }); return paginateMeta(records.map((record) => toTruthDto(record, project.slug)), total, options.page, options.pageSize, options.sort, filters); }; export const materializeReviewItemAsTruthInDatabase = async ( projectIdOrSlug: string, reviewItemId: string, ): Promise => { const project = await findProjectRecord(projectIdOrSlug); if (!project) return undefined; const review = await findReviewRecord(project.id, reviewItemId); if (!review) return undefined; if (!["accepted", "converted", "merged"].includes(review.status)) { return { success: true, message: `Review item '${reviewItemId}' is not accepted, converted or merged yet.`, updatedEntityId: reviewItemId, affectedEntities: [reviewItemId], }; } const existing = await prisma.acceptedTruthItem.findFirst({ where: { projectId: project.id, sourceReviewItemId: review.id, deletedAt: null }, }); if (existing) { return { success: true, message: `Accepted truth already exists for review item '${reviewItemId}'.`, updatedEntityId: reviewItemId, createdEntityId: existing.slug ?? existing.readableId ?? existing.id, affectedEntities: [reviewItemId, existing.slug ?? existing.id], }; } const createdAt = new Date(); const kind = normalizeKind(review.createdEntityType ?? review.type); const slug = `truth-${project.slug}-${createdAt.getTime()}`; const truth = await prisma.acceptedTruthItem.create({ data: { projectId: project.id, slug, readableId: slug, sourceReviewItemId: review.id, kind, title: review.title, summary: review.description, status: review.status === "converted" ? "converted" : review.status === "merged" ? "merged" : "accepted", source: review.source, confidence: review.confidence, approvalState: ApprovalState.approved, evidenceStrength: review.evidenceStrength, verificationState: VerificationState.verified, payloadJson: { reviewItemId: review.id, reviewSlug: review.slug, suggestedAction: review.suggestedAction }, provenanceJson: { createdFrom: "P6 review materialization" }, verifiedAt: createdAt, verifiedBy: "api", }, }); await createOperatingLogEntryInDatabase(project.slug, { type: "accepted truth", title: `Accepted truth created: ${review.title}`, summary: review.description, linkedEntityIds: [review.id, truth.id], source: "review_queue", importance: review.priority === "critical" ? "critical" : review.priority === "important" ? "important" : "normal", }); return { success: true, message: `Review item '${reviewItemId}' materialized as accepted truth.`, updatedEntityId: reviewItemId, createdEntityId: truth.slug ?? truth.id, affectedEntities: [reviewItemId, truth.slug ?? truth.id], }; }; export const listProjectUpdatesFromDatabase = async ( projectIdOrSlug: string, filters: ProjectUpdateListFilters, options: { page: number; pageSize: number; sort?: string }, ) => { const project = await findProjectRecord(projectIdOrSlug); if (!project) return undefined; const where: Prisma.ProjectUpdateWhereInput = { projectId: project.id, deletedAt: null, ...(filters.status?.length ? { status: { in: filters.status } } : {}), ...(filters.targetArea?.length ? { targetArea: { in: filters.targetArea } } : {}), ...(filters.kind?.length ? { kind: { in: filters.kind } } : {}), ...(filters.sourceTruthItemId ? { sourceTruthItemId: filters.sourceTruthItemId } : {}), }; const total = await prisma.projectUpdate.count({ where }); const records = await prisma.projectUpdate.findMany({ where, orderBy: { createdAt: options.sort?.endsWith(":asc") ? "asc" : "desc" }, skip: (options.page - 1) * options.pageSize, take: options.pageSize, }); return paginateMeta(records.map((record) => toProjectUpdateDto(record, project.slug)), total, options.page, options.pageSize, options.sort, filters); }; export const createProjectUpdateFromTruthInDatabase = async ( projectIdOrSlug: string, truthItemId: string, ): Promise => { const project = await findProjectRecord(projectIdOrSlug); if (!project) return undefined; const truth = await prisma.acceptedTruthItem.findFirst({ where: { projectId: project.id, deletedAt: null, OR: [{ id: truthItemId }, { slug: truthItemId }, { readableId: truthItemId }], }, }); if (!truth) return undefined; const existing = await prisma.projectUpdate.findFirst({ where: { projectId: project.id, sourceTruthItemId: truth.id, deletedAt: null }, }); if (existing) { return { success: true, message: `Project update already exists for accepted truth '${truthItemId}'.`, updatedEntityId: truthItemId, createdEntityId: existing.slug ?? existing.id, affectedEntities: [truthItemId, existing.slug ?? existing.id], }; } const createdAt = new Date(); const slug = `update-${project.slug}-${createdAt.getTime()}`; const update = await prisma.projectUpdate.create({ data: { projectId: project.id, slug, readableId: slug, sourceTruthItemId: truth.id, kind: truth.kind, title: truth.title, summary: truth.summary, targetArea: targetAreaForKind(truth.kind as AcceptedTruthKind), status: "ready", confidence: truth.confidence, approvalState: ApprovalState.pending, evidenceStrength: truth.evidenceStrength, payloadJson: { acceptedTruthItemId: truth.id }, provenanceJson: { createdFrom: "P6 accepted truth" }, }, }); await createOperatingLogEntryInDatabase(project.slug, { type: "project update", title: `Project update prepared: ${truth.title}`, summary: truth.summary, linkedEntityIds: [truth.id, update.id], source: "accepted_truth", importance: truth.confidence >= 90 ? "important" : "normal", }); return { success: true, message: `Project update prepared from accepted truth '${truthItemId}'.`, updatedEntityId: truthItemId, createdEntityId: update.slug ?? update.id, affectedEntities: [truthItemId, update.slug ?? update.id], }; }; export const applyProjectUpdateInDatabase = async ( projectIdOrSlug: string, projectUpdateId: string, ): Promise => { const project = await findProjectRecord(projectIdOrSlug); if (!project) return undefined; const existing = await prisma.projectUpdate.findFirst({ where: { projectId: project.id, deletedAt: null, OR: [{ id: projectUpdateId }, { slug: projectUpdateId }, { readableId: projectUpdateId }], }, }); if (!existing) return undefined; const appliedAt = new Date(); const claimed = await prisma.projectUpdate.updateMany({ where: { id: existing.id, status: "ready" }, data: { status: "applied", approvalState: ApprovalState.approved, appliedAt, updatedBy: "api", }, }); if (claimed.count !== 1) return "PROJECT_UPDATE_ALREADY_APPLIED"; const updated = await prisma.projectUpdate.findUniqueOrThrow({ where: { id: existing.id } }); await createOperatingLogEntryInDatabase(project.slug, { type: "project update", title: `Project update applied: ${updated.title}`, summary: updated.summary, linkedEntityIds: [updated.id, updated.sourceTruthItemId], source: "project_update", importance: "important", }); return { success: true, message: `Project update '${projectUpdateId}' applied.`, updatedEntityId: updated.slug ?? updated.id, affectedEntities: [updated.slug ?? updated.id, updated.sourceTruthItemId], }; }; export const listOperatingLogEntriesFromDatabase = async ( projectIdOrSlug: string, filters: OperatingLogListFilters, options: { page: number; pageSize: number; sort?: string }, ) => { const project = await findProjectRecord(projectIdOrSlug); if (!project) return undefined; const where: Prisma.OperatingLogEntryWhereInput = { projectId: project.id, deletedAt: null, ...(filters.type?.length ? { type: { in: filters.type } } : {}), ...(filters.source?.length ? { source: { in: filters.source } } : {}), ...(filters.importance?.length ? { importance: { in: filters.importance } } : {}), }; const total = await prisma.operatingLogEntry.count({ where }); const records = await prisma.operatingLogEntry.findMany({ where, orderBy: { timestamp: options.sort?.endsWith(":asc") ? "asc" : "desc" }, skip: (options.page - 1) * options.pageSize, take: options.pageSize, }); const items = records .map((record) => toLogDto(record, project.slug)) .filter((entry) => !filters.linkedEntityId || entry.linkedEntityIds.includes(filters.linkedEntityId)); return paginateMeta(items, total, options.page, options.pageSize, options.sort, filters); }; export const createOperatingLogEntryInDatabase = async ( projectIdOrSlug: string, body: CreateOperatingLogEntryRequestDto, ): Promise => { const project = await findProjectRecord(projectIdOrSlug); if (!project) return undefined; const createdAt = new Date(); const slug = `log-${project.slug}-${createdAt.getTime()}-${Math.random().toString(36).slice(2, 8)}`; const record = await prisma.operatingLogEntry.create({ data: { projectId: project.id, slug, readableId: slug, timestamp: createdAt, type: body.type, title: body.title, summary: body.summary, linkedEntityIdsJson: body.linkedEntityIds ?? [], source: body.source ?? "manual", importance: body.importance ?? "normal", payloadJson: { createdFrom: "P6 operating log API" }, }, }); return toLogDto(record, project.slug); };