import type { Prisma } from "@prisma/client"; import { prisma } from "../db/prisma.js"; import type { CurrentStateSummaryDto, FocusAreaDto, HealthSummaryDto, ProjectDetailDto, ProjectDna, ProjectStatus, ProjectSummaryDto, CreateProjectRequestDto, UpdateActiveContextPackRequestDto, UpdateProjectRequestDto, WorkspaceReadDto, WorkspaceStateDto, } from "../types/api.js"; import { mergeProjectSetupState, projectSetupActiveContextPackId, projectSetupWorkspaceState, sanitizeProjectSetupState, } from "./projectSetupState.js"; import { humanAttentionReviewWhere } from "./reviewAttentionPolicy.js"; import { projectHealth } from "../services/projectHealthSemantics.js"; const projectRelationsInclude = { dna: true, workspaceState: true, giteaRepositories: { select: { id: true, syncStatus: true, lastSyncAt: true } }, sourceItems: { orderBy: { updatedAt: "desc" as const } }, repositoryFindings: { where: { status: { notIn: ["resolved", "suppressed"] } }, select: { severity: true, status: true, lastSeenAt: true } }, _count: { select: { features: { where: { deletedAt: null } }, contextPacks: { where: { deletedAt: null } }, deliveryPlans: true, documentationItems: { where: { deletedAt: null } }, knowledgeItems: { where: { deletedAt: null } }, reviewQueueItems: { where: humanAttentionReviewWhere }, initiatives: { where: { deletedAt: null, reviewItemId: null, approvalState: { in: ["pending", "blocked"] } } }, }, }, } as const satisfies Prisma.ProjectInclude; type ProjectWithP2Relations = Prisma.ProjectGetPayload<{ include: typeof projectRelationsInclude }>; type WorkspaceJsonPayload = { workspaceState?: WorkspaceStateDto; focusAreas?: FocusAreaDto[]; activeFocusAreaId?: string; activeContextPackId?: string; currentStateSummary?: CurrentStateSummaryDto; healthSummary?: HealthSummaryDto; setupState?: Record; }; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; const slugify = (value: string): string => { const slug = value .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return slug || "project"; }; const toStringArray = (value: Prisma.JsonValue): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; const isDisposableProject = (project: Pick) => /\b(smoke|disposable|test project|proof project)\b/i.test(`${project.name} ${project.description}`); const toDna = ( dna: NonNullable, ): ProjectDna => ({ vision: dna.vision, goals: toStringArray(dna.goalsJson), nonGoals: toStringArray(dna.nonGoalsJson), targetAudience: dna.targetAudience, principles: toStringArray(dna.principlesJson), successMetrics: Array.isArray(dna.metricsJson) ? (dna.metricsJson as ProjectDna["successMetrics"]) : [], maturityStage: dna.maturityStage as ProjectDna["maturityStage"], revision: dna.revision, updatedAt: dna.updatedAt.toISOString(), }); const projectLastSync = (project: ProjectWithP2Relations) => { const timestamps = [ project.updatedAt.getTime(), ...project.giteaRepositories.flatMap((repository) => repository.lastSyncAt ? [repository.lastSyncAt.getTime()] : []), ...project.repositoryFindings.map((finding) => finding.lastSeenAt.getTime()), ]; return new Date(Math.max(...timestamps)).toISOString(); }; const toProjectSummary = ( project: ProjectWithP2Relations, ): ProjectSummaryDto => { const health = projectHealth(project); const workspacePayload = project.workspaceState ? toWorkspacePayload(project.workspaceState.notesJson) : {}; const configuredSkills = Array.isArray(workspacePayload.setupState?.codexSkills) ? workspacePayload.setupState.codexSkills : []; return { id: project.slug, name: project.name, description: project.description, health: health.score, healthStatus: health.healthStatus, lastSync: projectLastSync(project), domain: project.domain, status: project.status as ProjectStatus, createdAt: project.createdAt.toISOString(), updatedAt: project.updatedAt.toISOString(), activeContextPackId: project.activeContextPackId ?? undefined, resourceCounts: { repositories: project.giteaRepositories.length, readyRepositories: project.giteaRepositories.filter((repository) => repository.syncStatus === "ready").length, sources: project.sourceItems.filter((source) => source.status !== "archived").length, documentation: project._count.documentationItems, pendingReviews: project._count.reviewQueueItems, attentionItems: project._count.reviewQueueItems + project._count.initiatives, activeFindings: health.activeFindingCount, criticalFindings: health.criticalCount, warningFindings: health.warningCount, regressedFindings: health.regressedCount, contextPacks: project._count.contextPacks, features: project._count.features, deliveryPlans: project._count.deliveryPlans, knowledge: project._count.knowledgeItems, enabledSkills: configuredSkills.filter((skill) => isRecord(skill) && skill.enabled === true).length, }, }; }; const toWorkspacePayload = ( value: Prisma.JsonValue, ): WorkspaceJsonPayload => { if (!isRecord(value)) return {}; return value as WorkspaceJsonPayload; }; const toWorkspaceState = ( project: ProjectWithP2Relations, ): WorkspaceStateDto | undefined => { if (!project.workspaceState) return undefined; const payload = toWorkspacePayload(project.workspaceState.notesJson); if (payload.workspaceState) return payload.workspaceState; return { projectId: project.slug, activeContextPackId: project.workspaceState.activeContextPackId ?? undefined, primaryObjective: project.workspaceState.primaryObjective, focusAreas: [], openTasksCount: 0, openBugCount: 0, activeSection: project.workspaceState .activeSection as WorkspaceStateDto["activeSection"], updatedAt: project.workspaceState.updatedAt.toISOString(), notes: [], }; }; const toProjectDetail = ( project: ProjectWithP2Relations, ): ProjectDetailDto | undefined => { if (!project.dna) return undefined; const workspacePayload = project.workspaceState ? toWorkspacePayload(project.workspaceState.notesJson) : {}; return mergeProjectSetupState(workspacePayload.setupState, { ...toProjectSummary(project), dna: toDna(project.dna), constraints: Array.isArray(workspacePayload.setupState?.constraints) ? workspacePayload.setupState.constraints as ProjectDetailDto["constraints"] : [], principles: Array.isArray(workspacePayload.setupState?.principles) ? workspacePayload.setupState.principles as ProjectDetailDto["principles"] : [], initiatives: [], featureGroups: Array.isArray(workspacePayload.setupState?.featureGroups) ? workspacePayload.setupState.featureGroups as ProjectDetailDto["featureGroups"] : [], memoryRules: Array.isArray(workspacePayload.setupState?.memoryRules) ? workspacePayload.setupState.memoryRules as ProjectDetailDto["memoryRules"] : [], promptTemplates: Array.isArray(workspacePayload.setupState?.promptTemplates) ? workspacePayload.setupState.promptTemplates as ProjectDetailDto["promptTemplates"] : [], sourceLibrary: project.sourceItems.map((source) => ({ id: source.id, projectId: project.slug, title: source.title, kind: source.kind, inputMode: source.inputMode, originalName: source.path ?? undefined, url: source.url ?? undefined, text: source.content, summary: source.summary, status: source.status, suggestedDocumentationTargets: [], suggestedContextPackIds: [], extractedAssetIds: [], createdAt: source.createdAt.toISOString(), updatedAt: source.updatedAt.toISOString(), repositoryPath: source.path ?? undefined, repositoryBranch: source.branch ?? undefined, commitSha: source.commitSha ?? undefined, lastSyncedAt: source.lastSyncedAt?.toISOString(), attribution: { createdBy: "imported", sourceRun: source.commitSha ?? undefined, approvalState: "approved" }, })), workspaceState: toWorkspaceState(project), } as ProjectDetailDto & Record); }; const defaultDnaForProject = ( input: CreateProjectRequestDto, ): Omit => ({ vision: input.dna?.vision || input.primaryGoal || input.description, goals: input.dna?.goals ?? (input.primaryGoal ? [input.primaryGoal] : []), nonGoals: input.dna?.nonGoals ?? [], targetAudience: input.dna?.targetAudience || input.targetAudience || "Delivery teams", principles: input.dna?.principles ?? [], successMetrics: input.dna?.successMetrics ?? [], maturityStage: input.dna?.maturityStage ?? "Discovery", revision: input.dna?.revision ?? 1, }); const defaultWorkspacePayload = ( projectSlug: string, input: CreateProjectRequestDto, ): WorkspaceJsonPayload => { const setupWorkspace = projectSetupWorkspaceState(input.setupState); const workspaceState = { ...setupWorkspace, projectId: projectSlug, primaryObjective: typeof setupWorkspace?.primaryObjective === "string" ? setupWorkspace.primaryObjective : input.primaryGoal || input.description, focusAreas: Array.isArray(setupWorkspace?.focusAreas) ? setupWorkspace.focusAreas : [], openTasksCount: typeof setupWorkspace?.openTasksCount === "number" ? setupWorkspace.openTasksCount : 0, openBugCount: typeof setupWorkspace?.openBugCount === "number" ? setupWorkspace.openBugCount : 0, activeSection: typeof setupWorkspace?.activeSection === "string" ? setupWorkspace.activeSection : "workspace", updatedAt: new Date().toISOString(), notes: Array.isArray(setupWorkspace?.notes) ? setupWorkspace.notes : [], ...(projectSetupActiveContextPackId(input.setupState) ? { activeContextPackId: projectSetupActiveContextPackId(input.setupState) } : {}), } as WorkspaceStateDto; return { workspaceState, focusAreas: workspaceState.focusAreas, healthSummary: { health: 100, healthStatus: "healthy", openTasksCount: workspaceState.openTasksCount, openBugCount: workspaceState.openBugCount, riskCount: 0, pendingReviewCount: 0, }, setupState: sanitizeProjectSetupState(input.setupState), }; }; const findProjectRecordBySlugOrId = (projectIdOrSlug: string) => prisma.project.findFirst({ where: { deletedAt: null, OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }], }, include: projectRelationsInclude, }); export const listProjects = async (): Promise => { const projects = await prisma.project.findMany({ where: { deletedAt: null, }, orderBy: { updatedAt: "desc", }, include: projectRelationsInclude, }); return projects.filter((project) => !isDisposableProject(project)).map(toProjectSummary); }; export const createProject = async ( input: CreateProjectRequestDto, ): Promise => { const slug = slugify(input.id || input.name); const existing = await prisma.project.findFirst({ where: { slug, deletedAt: null, }, }); if (existing) { throw new Error("PROJECT_SLUG_ALREADY_EXISTS"); } const dna = defaultDnaForProject(input); const workspacePayload = defaultWorkspacePayload(slug, input); const project = await prisma.project.create({ data: { slug, readableId: slug, name: input.name.trim(), description: input.description.trim(), domain: input.domain?.trim() || "general", status: "active", health: 100, healthStatus: "healthy", activeContextPackId: projectSetupActiveContextPackId(input.setupState), createdBy: "frontend", updatedBy: "frontend", sourceType: "project_setup", dna: { create: { vision: dna.vision, goalsJson: dna.goals, nonGoalsJson: dna.nonGoals, targetAudience: dna.targetAudience, principlesJson: dna.principles, metricsJson: dna.successMetrics, maturityStage: dna.maturityStage, revision: dna.revision, createdBy: "frontend", updatedBy: "frontend", sourceType: "project_setup", }, }, workspaceState: { create: { primaryObjective: workspacePayload.workspaceState?.primaryObjective ?? input.primaryGoal ?? input.description.trim(), activeContextPackId: projectSetupActiveContextPackId(input.setupState), activeSection: workspacePayload.workspaceState?.activeSection ?? "workspace", notesJson: workspacePayload as Prisma.InputJsonValue, createdBy: "frontend", updatedBy: "frontend", sourceType: "project_setup", }, }, }, include: projectRelationsInclude, }); const detail = toProjectDetail(project); if (!detail) { throw new Error("PROJECT_CREATE_FAILED"); } return detail; }; export const updateProject = async ( projectIdOrSlug: string, input: UpdateProjectRequestDto, ): Promise => { const existing = await findProjectRecordBySlugOrId(projectIdOrSlug); if (!existing) return undefined; const project = await prisma.project.update({ where: { id: existing.id, }, data: { ...(input.name !== undefined ? { name: input.name.trim() } : {}), ...(input.description !== undefined ? { description: input.description.trim() } : {}), ...(input.domain !== undefined ? { domain: input.domain.trim() || "general" } : {}), ...(input.status !== undefined ? { status: input.status } : {}), updatedBy: "frontend", }, include: projectRelationsInclude, }); return toProjectDetail(project); }; export const updateActiveContextPack = async ( projectIdOrSlug: string, input: UpdateActiveContextPackRequestDto, ): Promise => { const existing = await findProjectRecordBySlugOrId(projectIdOrSlug); if (!existing?.dna) return undefined; const currentPayload = existing.workspaceState ? toWorkspacePayload(existing.workspaceState.notesJson) : {}; const currentWorkspace = toWorkspaceState(existing); const updatedAt = new Date().toISOString(); const nextWorkspaceState: WorkspaceStateDto = { projectId: existing.slug, primaryObjective: currentWorkspace?.primaryObjective ?? existing.workspaceState?.primaryObjective ?? existing.description, focusAreas: currentWorkspace?.focusAreas ?? currentPayload.focusAreas ?? [], openTasksCount: currentWorkspace?.openTasksCount ?? 0, openBugCount: currentWorkspace?.openBugCount ?? 0, activeSection: currentWorkspace?.activeSection ?? "context-packs", updatedAt, notes: currentWorkspace?.notes ?? [], activeContextPackId: input.contextPackId, }; const payload: WorkspaceJsonPayload = { ...currentPayload, workspaceState: nextWorkspaceState, focusAreas: currentPayload.focusAreas ?? nextWorkspaceState.focusAreas, activeContextPackId: input.contextPackId, }; await prisma.project.update({ where: { id: existing.id, }, data: { activeContextPackId: input.contextPackId, updatedBy: "frontend", workspaceState: { upsert: { create: { activeContextPackId: input.contextPackId, primaryObjective: nextWorkspaceState.primaryObjective, activeSection: nextWorkspaceState.activeSection, notesJson: payload as Prisma.InputJsonValue, createdBy: "frontend", updatedBy: "frontend", sourceType: "workspace_update", }, update: { activeContextPackId: input.contextPackId, primaryObjective: nextWorkspaceState.primaryObjective, activeSection: nextWorkspaceState.activeSection, notesJson: payload as Prisma.InputJsonValue, updatedBy: "frontend", sourceType: "workspace_update", }, }, }, }, }); return getProjectWorkspace(existing.slug); }; export const getProjectBySlugOrId = async ( projectIdOrSlug: string, ): Promise => { const project = await findProjectRecordBySlugOrId(projectIdOrSlug); if (!project) return undefined; return toProjectDetail(project); }; export const getProjectDNA = async ( projectIdOrSlug: string, ): Promise => { const project = await findProjectRecordBySlugOrId(projectIdOrSlug); if (!project?.dna) return undefined; return toDna(project.dna); }; export const getProjectWorkspace = async ( projectIdOrSlug: string, ): Promise => { const project = await findProjectRecordBySlugOrId(projectIdOrSlug); if (!project?.dna) return undefined; const workspaceState = toWorkspaceState(project); const payload = project.workspaceState ? toWorkspacePayload(project.workspaceState.notesJson) : {}; const activeContextPackId = workspaceState?.activeContextPackId ?? project.activeContextPackId ?? undefined; const operationalHealth = projectHealth(project); return { project: toProjectSummary(project), dna: toDna(project.dna), activeFocusAreaId: payload.activeFocusAreaId, activeContextPackId, focusAreas: payload.focusAreas ?? workspaceState?.focusAreas ?? [], workspaceState, currentStateSummary: payload.currentStateSummary, healthSummary: { ...(payload.healthSummary ?? {}), health: operationalHealth.score, healthStatus: operationalHealth.healthStatus, openTasksCount: workspaceState?.openTasksCount ?? 0, openBugCount: workspaceState?.openBugCount ?? 0, riskCount: payload.healthSummary?.riskCount ?? 0, pendingReviewCount: payload.healthSummary?.pendingReviewCount ?? 0, }, }; };