import type { Prisma } from "@prisma/client"; import { prisma } from "../db/prisma.js"; import type { ContextPackDto, ContextPackSummaryDto, CreateContextPackRequestDto, EvidenceRefDto, GeneratePromptRequestDto, GeneratedPromptDto, ProjectDna, } from "../types/api.js"; const toPublicId = (value: { slug: string | null; readableId: string | null; id: string }) => value.slug ?? value.readableId ?? value.id; const toJson = (value: unknown): Prisma.InputJsonValue => JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; const asStringArray = (value: unknown): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; const asEvidenceRefs = (value: unknown): EvidenceRefDto[] => Array.isArray(value) ? (value as EvidenceRefDto[]) : []; type ContextPackMetadata = { dna?: ProjectDna; assumptions?: string[]; constraints?: string[]; evidence?: EvidenceRefDto[]; signals?: string[]; featureIds?: string[]; bugIds?: string[]; taskIds?: string[]; decisionIds?: string[]; memoryRuleIds?: string[]; architectureNotes?: string[]; regressions?: string[]; risks?: string[]; currentFocus?: ContextPackDto["currentFocus"]; }; const toMetadata = (value: Prisma.JsonValue | null): ContextPackMetadata => isRecord(value) ? (value as ContextPackMetadata) : {}; const findProjectRecordBySlugOrId = (projectIdOrSlug: string) => prisma.project.findFirst({ where: { deletedAt: null, OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }], }, include: { dna: true, }, }); const toProjectDna = (project: NonNullable>>): ProjectDna => ({ vision: project.dna?.vision ?? "", goals: asStringArray(project.dna?.goalsJson), nonGoals: asStringArray(project.dna?.nonGoalsJson), targetAudience: project.dna?.targetAudience ?? "", principles: asStringArray(project.dna?.principlesJson), successMetrics: Array.isArray(project.dna?.metricsJson) ? (project.dna?.metricsJson as ProjectDna["successMetrics"]) : [], maturityStage: (project.dna?.maturityStage as ProjectDna["maturityStage"]) ?? "Discovery", revision: project.dna?.revision ?? 1, updatedAt: project.dna?.updatedAt.toISOString() ?? project.updatedAt.toISOString(), }); type DbContextPackRecord = Awaited> & { sections?: Array<{ id: string; slug: string | null; title: string; sectionType: string; orderIndex: number; contentJson: Prisma.JsonValue; metadataJson: Prisma.JsonValue | null; }>; }; const toContextPackDto = async ( contextPack: NonNullable, ): Promise => { const project = await findProjectRecordBySlugOrId(contextPack.projectId); const metadata = toMetadata(contextPack.metadataJson); const quality = isRecord(contextPack.qualityJson) ? (contextPack.qualityJson as ContextPackDto["quality"]) : undefined; const trust = isRecord(contextPack.trustJson) ? (contextPack.trustJson as ContextPackDto["trust"]) : { source: "database context pack", confidence: contextPack.confidence, evidence: [], lastChecked: contextPack.updatedAt.toISOString(), approvalState: contextPack.approvalState, }; return { id: toPublicId(contextPack), projectId: project?.slug ?? contextPack.projectId, title: contextPack.title, category: contextPack.category as ContextPackDto["category"], dna: metadata.dna ?? (project ? toProjectDna(project) : { vision: "", goals: [], nonGoals: [], targetAudience: "", principles: [], successMetrics: [], maturityStage: "Discovery", revision: 0, updatedAt: contextPack.updatedAt.toISOString(), }), summary: contextPack.summary, assumptions: metadata.assumptions ?? [], constraints: metadata.constraints ?? [], evidence: metadata.evidence ?? [], signals: metadata.signals ?? [], featureIds: metadata.featureIds, bugIds: metadata.bugIds, taskIds: metadata.taskIds, decisionIds: metadata.decisionIds, memoryRuleIds: metadata.memoryRuleIds, architectureNotes: metadata.architectureNotes, regressions: metadata.regressions, risks: metadata.risks, quality, currentFocus: metadata.currentFocus, trust, generatedBy: contextPack.generatedBy as ContextPackDto["generatedBy"], createdAt: contextPack.createdAt.toISOString(), updatedAt: contextPack.updatedAt.toISOString(), }; }; export const projectExists = async (projectIdOrSlug: string): Promise => Boolean(await findProjectRecordBySlugOrId(projectIdOrSlug)); export const listContextPacksForProject = async ( projectIdOrSlug: string, ): Promise => { const project = await findProjectRecordBySlugOrId(projectIdOrSlug); if (!project) return undefined; const contextPacks = await prisma.contextPack.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: [{ updatedAt: "desc" }], include: { sections: { orderBy: { orderIndex: "asc" } } }, }); return Promise.all(contextPacks.map(toContextPackDto)); }; export const getContextPackBySlugOrId = async ( projectIdOrSlug: string, contextPackIdOrSlug: string, ): Promise => { const project = await findProjectRecordBySlugOrId(projectIdOrSlug); if (!project) return undefined; const contextPack = await prisma.contextPack.findFirst({ where: { projectId: project.id, deletedAt: null, OR: [ { id: contextPackIdOrSlug }, { slug: contextPackIdOrSlug }, { readableId: contextPackIdOrSlug }, ], }, include: { sections: { orderBy: { orderIndex: "asc" } } }, }); return contextPack ? toContextPackDto(contextPack) : undefined; }; export const getContextPackSummaryBySlugOrId = async ( projectIdOrSlug: string, contextPackIdOrSlug: string, ): Promise => { const contextPack = await getContextPackBySlugOrId(projectIdOrSlug, contextPackIdOrSlug); if (!contextPack) return undefined; return { projectId: contextPack.projectId, contextPackId: contextPack.id, title: contextPack.title, category: contextPack.category, summary: contextPack.summary, counts: { assumptions: contextPack.assumptions.length, constraints: contextPack.constraints.length, evidence: contextPack.evidence.length, features: contextPack.featureIds?.length ?? 0, bugs: contextPack.bugIds?.length ?? 0, tasks: contextPack.taskIds?.length ?? 0, decisions: contextPack.decisionIds?.length ?? 0, memoryRules: contextPack.memoryRuleIds?.length ?? 0, risks: contextPack.risks?.length ?? 0, }, qualityScore: contextPack.quality?.score, createdAt: contextPack.createdAt, updatedAt: contextPack.updatedAt, }; }; const toGeneratedBy = (sourceType: CreateContextPackRequestDto["sourceType"]): ContextPackDto["generatedBy"] => { if (sourceType === "check") return "check-run"; if (sourceType === "manual") return "manual"; return "user"; }; export const createContextPackForProject = async ( projectIdOrSlug: string, request: CreateContextPackRequestDto, ): Promise => { const project = await findProjectRecordBySlugOrId(projectIdOrSlug); if (!project) return undefined; const dna = toProjectDna(project); const features = await prisma.feature.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { name: "asc" }, }); const evidence = await prisma.evidence.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { lastSeen: "desc" }, }); const featureIds = request.includeFeatures === false ? [] : request.sourceType === "feature" && request.sourceId ? [request.sourceId] : features.map((feature) => feature.slug ?? feature.readableId ?? feature.id); const evidenceRefs: EvidenceRefDto[] = request.includeEvidence === false ? [] : evidence.slice(0, 10).map((item) => ({ id: `er-${item.slug ?? item.readableId ?? item.id}`, type: "analysis", refId: item.slug ?? item.readableId ?? item.id, label: item.reason, })); const createdAt = new Date(); const slug = `cp-${project.slug}-${request.sourceType}-${createdAt.getTime().toString(36)}`; const title = request.title?.trim() || `${project.name} ${request.sourceType.replace("_", " ")} context pack`; const metadata: ContextPackMetadata = { dna, assumptions: request.sourceType === "manual" ? ["Manual context source requires human review."] : ["Database-generated context should be verified before use."], constraints: ["Context pack generated from database-backed project data."], evidence: evidenceRefs, signals: ["database-generated", request.sourceType], featureIds, memoryRuleIds: [], architectureNotes: request.includeArchitecture === false ? [] : [`Database-backed architecture context for ${project.name}.`], risks: request.includeRisks === false ? [] : ["Database-generated context still needs review before implementation."], regressions: request.includeRegressions === false ? [] : [], }; const quality = { dnaIncluded: true, constitutionIncluded: false, constraintsIncluded: true, featuresIncluded: request.includeFeatures !== false, evidenceIncluded: request.includeEvidence !== false, risksIncluded: request.includeRisks !== false, memoryIncluded: request.includeMemoryRules !== false, doNotBreakIncluded: true, verificationIncluded: false, score: request.includeEvidence === false ? 58 : 74, }; const trust = { source: "database context-pack create", confidence: request.includeEvidence === false ? 64 : 82, evidence: evidenceRefs, lastChecked: createdAt.toISOString(), approvalState: "pending" as const, }; const contextPack = await prisma.contextPack.create({ data: { projectId: project.id, slug, readableId: slug, title, category: request.sourceType === "feature" ? "feature" : "quality", summary: `Created from database project ${project.slug} via ${request.sourceType} context source. DNA is always included.`, generatedBy: toGeneratedBy(request.sourceType), confidence: trust.confidence, approvalState: "pending", qualityJson: toJson(quality), trustJson: toJson(trust), metadataJson: toJson(metadata), createdBy: "runtime", updatedBy: "runtime", sourceType: "database_runtime", sections: { create: [ { projectId: project.id, slug: "dna", title: "Project DNA", sectionType: "dna", orderIndex: 1, contentJson: toJson(dna), }, { projectId: project.id, slug: "features", title: "Relevant features", sectionType: "features", orderIndex: 2, contentJson: toJson(featureIds), }, { projectId: project.id, slug: "evidence", title: "Evidence references", sectionType: "evidence", orderIndex: 3, contentJson: toJson(evidenceRefs), }, ], }, }, include: { sections: { orderBy: { orderIndex: "asc" } } }, }); return toContextPackDto(contextPack); }; export const generatePromptFromContextPack = async ( projectIdOrSlug: string, request: GeneratePromptRequestDto, ): Promise => { const contextPack = await getContextPackBySlugOrId(projectIdOrSlug, request.contextPackId); if (!contextPack) return undefined; const createdAt = new Date().toISOString(); const promptId = `prompt-${contextPack.projectId}-${Date.now().toString(36)}`; const expectedOutput = request.expectedOutput?.trim() || "Clear implementation notes and validation steps."; const generatedText = [ `Generated from context pack ${contextPack.id} (${contextPack.title}).`, `Target tool: ${request.targetTool}.`, `Persona: ${request.persona}.`, `Objective: ${request.objective.trim()}.`, `Expected output: ${expectedOutput}`, `Context summary: ${contextPack.summary}`, `Project vision: ${contextPack.dna.vision}`, `Relevant features: ${(contextPack.featureIds ?? []).join(", ") || "none"}.`, `Risks: ${(contextPack.risks ?? []).join(" ") || "none listed"}.`, "Use only the supplied context pack facts and mark uncertainty explicitly.", ].join("\n"); return { id: promptId, projectId: contextPack.projectId, contextPackId: contextPack.id, targetTool: request.targetTool, persona: request.persona, objective: request.objective.trim(), expectedOutput, generatedText, generatedFrom: "context-pack", createdAt, }; };