368 lines
13 KiB
TypeScript
368 lines
13 KiB
TypeScript
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<string, unknown> =>
|
|
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<Awaited<ReturnType<typeof findProjectRecordBySlugOrId>>>): 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<ReturnType<typeof prisma.contextPack.findFirst>> & {
|
|
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<DbContextPackRecord>,
|
|
): Promise<ContextPackDto> => {
|
|
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> =>
|
|
Boolean(await findProjectRecordBySlugOrId(projectIdOrSlug));
|
|
|
|
export const listContextPacksForProject = async (
|
|
projectIdOrSlug: string,
|
|
): Promise<ContextPackDto[] | undefined> => {
|
|
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<ContextPackDto | undefined> => {
|
|
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<ContextPackSummaryDto | undefined> => {
|
|
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<ContextPackDto | undefined> => {
|
|
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<GeneratedPromptDto | undefined> => {
|
|
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,
|
|
};
|
|
};
|