Initial curated projectbrain source from 8a7b1e0651dbdab97d744cbd2e653ab3970ac6c5

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:43:26 +02:00
commit 5aa3e8d685
700 files changed
+132801

No files matched your search

@@ -0,0 +1,314 @@
import { createHash } from "node:crypto";
import type { AppEnv } from "../config/env.js";
import { prisma } from "../db/prisma.js";
import { getGiteaClient, listLinkedRepositories, type LinkedRepository } from "../integrations/gitea/service.js";
import type { GiteaRepository } from "../integrations/gitea/types.js";
import { chooseAnalysisStrategy, type AnalysisStrategy } from "../services/analysisStrategy.js";
export type PortfolioAnalysisRequest = {
projectIds?: string[];
repositoryIds?: string[];
includeBranches: boolean;
includePullRequests: boolean;
includeActions: boolean;
changedPaths?: string[];
strategy?: AnalysisStrategy;
strategyReason?: string;
};
const TERMINAL_STATUSES = ["succeeded", "partial", "failed", "cancelled"] as const;
const MAX_REPOSITORIES = 200;
export const projectAnalysisItemScope = (projectIdOrSlug: string) => ({ OR: [{ projectId: projectIdOrSlug }, { projectSlug: projectIdOrSlug }] });
const canonicalRequest = (input: Partial<PortfolioAnalysisRequest>): PortfolioAnalysisRequest => ({
projectIds: [...new Set((input.projectIds ?? []).map((value) => value.trim()).filter(Boolean))].sort(),
repositoryIds: [...new Set((input.repositoryIds ?? []).map((value) => value.trim()).filter(Boolean))].sort(),
includeBranches: input.includeBranches ?? true,
includePullRequests: input.includePullRequests ?? true,
includeActions: input.includeActions ?? true,
...(input.changedPaths?.length ? { changedPaths: [...new Set(input.changedPaths.map((value) => value.trim()).filter(Boolean))].slice(0, 200) } : {}),
...(input.strategy ? { strategy: input.strategy } : {}),
...(input.strategyReason ? { strategyReason: input.strategyReason } : {}),
});
export type AnalysisTarget = Pick<LinkedRepository, "id" | "projectId" | "projectSlug" | "projectName" | "owner" | "repository" | "fullName" | "defaultBranch" | "accessMode"> & {
repositoryLinkId?: string;
};
const matchesRepository = (target: AnalysisTarget, request: PortfolioAnalysisRequest) => {
return !request.repositoryIds?.length || request.repositoryIds.includes(target.id) || request.repositoryIds.includes(target.fullName);
};
export const resolveAnalysisTargets = (
links: LinkedRepository[],
repositories: GiteaRepository[],
request: PortfolioAnalysisRequest,
): AnalysisTarget[] => {
let targets: AnalysisTarget[];
if (request.projectIds?.length) {
targets = links
.filter((link) => request.projectIds!.includes(link.projectId) || request.projectIds!.includes(link.projectSlug))
.map((link) => ({ ...link, repositoryLinkId: link.id }));
} else {
const byFullName = new Map(links.map((link) => [link.fullName.toLowerCase(), link]));
targets = repositories.map((repository) => {
const link = byFullName.get(repository.full_name.toLowerCase());
return {
id: link?.id ?? `gitea:${repository.full_name}`,
repositoryLinkId: link?.id,
projectId: link?.projectId ?? "",
projectSlug: link?.projectSlug ?? "gitea-all",
projectName: link?.projectName ?? "Gitea portfolio",
owner: repository.owner.login,
repository: repository.name,
fullName: repository.full_name,
defaultBranch: repository.default_branch || "main",
accessMode: link?.accessMode ?? (repository.permissions?.push || repository.permissions?.admin ? "read_write" : "read_only"),
};
});
}
return targets.filter((target) => matchesRepository(target, request)).sort((left, right) => left.fullName.localeCompare(right.fullName));
};
const jobSelection = {
id: true,
kind: true,
status: true,
actor: true,
progressCurrent: true,
progressTotal: true,
summaryText: true,
summaryJson: true,
errorCode: true,
cancelRequestedAt: true,
queuedAt: true,
startedAt: true,
heartbeatAt: true,
completedAt: true,
expiresAt: true,
createdAt: true,
updatedAt: true,
} as const;
export const serializeAnalysisJob = (job: Record<string, unknown>) => {
const serialized = Object.fromEntries(
Object.entries(job).map(([key, value]) => [key, value instanceof Date ? value.toISOString() : value]),
);
const status = typeof serialized.status === "string" ? serialized.status : "unknown";
const terminal = TERMINAL_STATUSES.includes(status as (typeof TERMINAL_STATUSES)[number]);
const stateVersion = createHash("sha256").update(JSON.stringify({
id: serialized.id,
status,
progressCurrent: serialized.progressCurrent,
progressTotal: serialized.progressTotal,
summaryText: serialized.summaryText,
errorCode: serialized.errorCode,
updatedAt: serialized.updatedAt,
})).digest("hex").slice(0, 24);
const nextAction = status === "succeeded"
? "read-results"
: status === "partial"
? "read-results-and-inspect-failures"
: status === "failed"
? "inspect-summary-or-restart"
: status === "cancelled"
? "restart-if-needed"
: "wait-for-change";
return { ...serialized, stateVersion, terminal, pollAfterMs: terminal ? 0 : status === "queued" ? 5_000 : 4_000, nextAction };
};
export const createPortfolioAnalysisJob = async (
env: AppEnv,
actor: string,
input: Partial<PortfolioAnalysisRequest>,
) => {
const request = canonicalRequest(input);
const links = await listLinkedRepositories();
const repositories = request.projectIds?.length ? [] : await getGiteaClient(env).listRepositories();
const targets = resolveAnalysisTargets(links, repositories, request);
if (!targets.length) throw new Error("ANALYSIS_REPOSITORIES_NOT_FOUND");
if (targets.length > MAX_REPOSITORIES) throw new Error("ANALYSIS_REPOSITORY_LIMIT_EXCEEDED");
const lastFullScan = await prisma.analysisJob.findFirst({ where: { status: "succeeded", items: { some: { repositoryLinkId: { in: targets.map((target) => target.repositoryLinkId ?? target.id) } } } }, orderBy: { completedAt: "desc" }, select: { completedAt: true } });
const analysisPlan = chooseAnalysisStrategy({ changedPaths: request.changedPaths, lastFullScanAt: lastFullScan?.completedAt?.toISOString(), budgetItems: targets.length });
const persistedRequest = { ...request, strategy: request.strategy ?? analysisPlan.strategy, strategyReason: request.strategyReason ?? analysisPlan.reason, repositoryIds: targets.map((target) => target.id) };
const requestHash = createHash("sha256").update(JSON.stringify(persistedRequest)).digest("hex");
const activeDedupeKey = createHash("sha256").update(`${actor}:portfolio_health:${requestHash}`).digest("hex");
const existing = await prisma.analysisJob.findUnique({ where: { activeDedupeKey }, select: jobSelection });
if (existing) return { job: serializeAnalysisJob(existing), deduplicated: true };
try {
const job = await prisma.analysisJob.create({
data: {
actor,
requestJson: persistedRequest as never,
requestHash,
activeDedupeKey,
progressTotal: targets.length,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
items: {
create: targets.map((target, sequence) => ({
repositoryLinkId: target.repositoryLinkId,
projectId: target.projectId || undefined,
projectSlug: target.projectSlug,
projectName: target.projectName,
owner: target.owner,
repository: target.repository,
defaultBranch: target.defaultBranch,
accessMode: target.accessMode,
sequence,
})),
},
},
select: jobSelection,
});
return { job: serializeAnalysisJob(job), deduplicated: false };
} catch (error) {
const raced = await prisma.analysisJob.findUnique({ where: { activeDedupeKey }, select: jobSelection });
if (raced) return { job: serializeAnalysisJob(raced), deduplicated: true };
throw error;
}
};
export const getAnalysisJob = async (actor: string, jobId: string) => {
const job = await prisma.analysisJob.findFirst({ where: { id: jobId, actor }, select: jobSelection });
if (!job) throw new Error("ANALYSIS_JOB_NOT_FOUND");
return serializeAnalysisJob(job);
};
export const listAnalysisJobs = async (actor: string, limit = 10) => {
const jobs = await prisma.analysisJob.findMany({
where: { actor },
orderBy: { createdAt: "desc" },
take: Math.min(Math.max(limit, 1), 25),
select: jobSelection,
});
return jobs.map((job) => serializeAnalysisJob(job));
};
export const listProjectAnalysisJobs = async (projectId: string, limit = 10) => {
const jobs = await prisma.analysisJob.findMany({
where: { items: { some: projectAnalysisItemScope(projectId) } },
orderBy: { createdAt: "desc" },
take: Math.min(Math.max(limit, 1), 25),
select: jobSelection,
});
return jobs.map((job) => serializeAnalysisJob(job));
};
export const getProjectAnalysisJob = async (projectId: string, jobId: string) => {
const job = await prisma.analysisJob.findFirst({ where: { id: jobId, items: { some: projectAnalysisItemScope(projectId) } }, select: jobSelection });
if (!job) throw new Error("ANALYSIS_JOB_NOT_FOUND");
return serializeAnalysisJob(job);
};
export const getPortfolioAnalysisJob = async (jobId: string) => {
const job = await prisma.analysisJob.findUnique({ where: { id: jobId }, select: jobSelection });
if (!job) throw new Error("ANALYSIS_JOB_NOT_FOUND");
return serializeAnalysisJob(job);
};
export const cancelAnalysisJob = async (actor: string, jobId: string) => {
const job = await prisma.analysisJob.findFirst({ where: { id: jobId, actor }, select: { id: true, status: true } });
if (!job) throw new Error("ANALYSIS_JOB_NOT_FOUND");
if (TERMINAL_STATUSES.includes(job.status as (typeof TERMINAL_STATUSES)[number])) return getAnalysisJob(actor, jobId);
const now = new Date();
await prisma.$transaction([
prisma.analysisJob.update({
where: { id: job.id },
data: job.status === "queued"
? { status: "cancelled", cancelRequestedAt: now, completedAt: now, activeDedupeKey: null, summaryText: "Analyse geannuleerd voordat de worker startte." }
: { cancelRequestedAt: now },
}),
...(job.status === "queued" ? [prisma.analysisJobItem.updateMany({ where: { jobId: job.id, status: "pending" }, data: { status: "cancelled", completedAt: now } })] : []),
]);
return getAnalysisJob(actor, jobId);
};
export const cancelProjectAnalysisJob = async (projectId: string, jobId: string) => {
const job = await prisma.analysisJob.findFirst({ where: { id: jobId, items: { some: projectAnalysisItemScope(projectId) } }, select: { id: true, actor: true } });
if (!job) throw new Error("ANALYSIS_JOB_NOT_FOUND");
return cancelAnalysisJob(job.actor, job.id);
};
export const cancelPortfolioAnalysisJob = async (jobId: string) => {
const job = await prisma.analysisJob.findUnique({ where: { id: jobId }, select: { actor: true } });
if (!job) throw new Error("ANALYSIS_JOB_NOT_FOUND");
return cancelAnalysisJob(job.actor, jobId);
};
type AnalysisResultScope = { actor?: string; projectId?: string; portfolio?: boolean };
const getScopedAnalysisJobResults = async (
scope: AnalysisResultScope,
jobId: string,
input: { cursor?: string; pageSize?: number; characterBudget?: number } = {},
) => {
const where = scope.projectId
? { id: jobId, items: { some: projectAnalysisItemScope(scope.projectId) } }
: scope.portfolio
? { id: jobId }
: { id: jobId, actor: scope.actor ?? "" };
const job = await prisma.analysisJob.findFirst({ where, select: jobSelection });
if (!job) throw new Error("ANALYSIS_JOB_NOT_FOUND");
const cursor = Number.parseInt(input.cursor ?? "0", 10);
if (!Number.isInteger(cursor) || cursor < 0) throw new Error("ANALYSIS_CURSOR_INVALID");
const pageSize = Math.min(Math.max(input.pageSize ?? 5, 1), 10);
const characterBudget = Math.min(Math.max(input.characterBudget ?? 24_000, 4_000), 32_000);
const rows = await prisma.analysisJobItem.findMany({
where: { jobId, ...(scope.projectId ? projectAnalysisItemScope(scope.projectId) : {}) },
orderBy: { sequence: "asc" },
skip: cursor,
take: pageSize + 1,
});
let usedCharacters = 0;
const items: Array<Record<string, unknown>> = [];
for (const row of rows.slice(0, pageSize)) {
const detail = row.detailJson && typeof row.detailJson === "object" ? row.detailJson : undefined;
const detailCharacters = detail ? JSON.stringify(detail).length : 0;
const includeDetail = usedCharacters + detailCharacters <= characterBudget;
items.push({
repositoryId: row.repositoryLinkId ?? `gitea:${row.owner}/${row.repository}`,
projectId: row.projectId,
projectSlug: row.projectSlug,
projectName: row.projectName,
repository: `${row.owner}/${row.repository}`,
status: row.status,
summary: row.summary,
findingCount: row.findingCount,
errorCode: row.errorCode,
...(includeDetail && detail ? { detail } : { detailTruncated: Boolean(detail) }),
});
if (includeDetail) usedCharacters += detailCharacters;
}
const consumed = items.length;
const hasMore = rows.length > pageSize;
return {
job: serializeAnalysisJob(job),
items,
cursor: String(cursor),
pageSize,
characterBudget,
usedCharacters,
nextCursor: hasMore ? String(cursor + consumed) : undefined,
};
};
export const getAnalysisJobResults = async (
actor: string,
jobId: string,
input: { cursor?: string; pageSize?: number; characterBudget?: number } = {},
projectId?: string,
) => getScopedAnalysisJobResults(projectId ? { projectId } : { actor }, jobId, input);
export const getProjectAnalysisJobResults = async (
projectId: string,
jobId: string,
input: { cursor?: string; pageSize?: number; characterBudget?: number } = {},
) => getAnalysisJobResults("", jobId, input, projectId);
export const getPortfolioAnalysisJobResults = async (
jobId: string,
input: { cursor?: string; pageSize?: number; characterBudget?: number } = {},
) => getScopedAnalysisJobResults({ portfolio: true }, jobId, input);
@@ -0,0 +1,18 @@
export type AutonomyExecutionTarget = {
surface: "gitea";
claimMode: "branch";
completionMode: "validated_merge";
repositoryLinkId: string;
owner: string;
repository: string;
fullName: string;
defaultBranch: string;
branch: string;
};
export const hasGiteaAutonomyCompletionEvidence = (evidence: string[] | undefined) => {
const values = evidence ?? [];
return values.some((entry) => entry.startsWith("gitea:auto:merge:"))
&& values.some((entry) => entry.startsWith("gitea:auto:head:"))
&& values.some((entry) => entry.startsWith("gitea:auto:validation:"));
};
@@ -0,0 +1,797 @@
import { autonomyProfileSettings, type AutonomyProfile } from "@projectbrain/contracts";
import { randomUUID } from "node:crypto";
import type { Prisma } from "@prisma/client";
import { prisma } from "../db/prisma.js";
import { autonomyRetryDelayMinutes, classifyAutonomyBlocker, recoverStoredBlockerKind } from "../services/autonomyRecovery.js";
import { addDiscoveredTasks, hasOpenAutonomyExecutionItems, isAutonomyItemInExecutionScope, orderAutonomyRepairs, workPrompt } from "../services/autonomyWork.js";
import { getFeatureDelivery } from "./featureDeliveryRepository.js";
import { createBrainstormForProject } from "./brainstormRepository.js";
import { parseRoadmapMarkdown } from "../services/autonomyRoadmap.js";
import { decideRecovery } from "../services/recoveryPolicy.js";
import { hasGiteaAutonomyCompletionEvidence, type AutonomyExecutionTarget } from "./autonomyExecutionEvidence.js";
export { parseRoadmapMarkdown };
export { autonomyRetryDelayMinutes, classifyAutonomyBlocker };
export { hasGiteaAutonomyCompletionEvidence };
export type { AutonomyExecutionTarget };
export type AutonomyStatus = "off" | "waiting_roadmap" | "running" | "blocked" | "completed";
export type AutonomyWorkKind = "brainstorm" | "implement" | "validate_phase";
export type AutonomyWorkOutcome = "success" | "failed" | "blocked";
export type RoadmapItemStatus = "pending" | "in_progress" | "completed" | "blocked";
export type RoadmapPhaseStatus = "pending" | "active" | "brainstorming" | "executing" | "validating" | "completed" | "blocked";
export type AutonomyRoadmapItem = {
id: string;
title: string;
instruction?: string;
source?: "roadmap" | "user_assignment" | "brainstorm" | "recovery" | "finding" | "delivery" | "release" | "remediation" | "feature" | "bug";
createdAt?: string;
status: RoadmapItemStatus;
attempts: number;
priority?: number;
scope?: "required" | "committed" | "stretch" | "future";
risk?: "low" | "normal" | "medium" | "high" | "critical";
estimatedScope?: number;
releaseBlocking?: boolean;
waitingExternal?: boolean;
touchedPaths?: string[];
domain?: string;
deliveryId?: string;
dependsOn?: string[];
lastResult?: string;
lastError?: string;
completionEvidence?: string[];
supersededBy?: string;
};
export type AutonomyRoadmapPhase = {
id: string;
title: string;
objective: string;
status: RoadmapPhaseStatus;
brainstormIterations: number;
brainstormConverged: boolean;
validationFailures: number;
items: AutonomyRoadmapItem[];
};
export type AutonomyRoadmap = {
id: string;
title: string;
sourceMarkdown: string;
status: "active" | "completed" | "blocked";
currentPhaseIndex: number;
phases: AutonomyRoadmapPhase[];
importedAt: string;
completedAt?: string;
};
export type AutonomyLease = {
token: string;
kind: AutonomyWorkKind;
actor: string;
runId?: string;
roadmapId: string;
phaseId: string;
itemId?: string;
createdAt: string;
expiresAt: string;
};
export type AutonomyCompletedLease = {
token: string;
outcome: AutonomyWorkOutcome;
summary: string;
addedTasks: string[];
completedAt: string;
};
export type AutonomyRun = {
id: string;
actor: string;
startedAt: string;
reportedWorkItems: number;
};
export type AutonomyEvent = {
id: string;
at: string;
type: string;
actor: string;
summary: string;
};
export type AutonomyState = {
version: 1;
profile: AutonomyProfile;
enabled: boolean;
status: AutonomyStatus;
autoBrainstorm: boolean;
maxBrainstormIterations: number;
maxConsecutiveFailures: number;
leaseMinutes: number;
maxWorkItemsPerRun: number;
maxRunMinutes: number;
pauseAfterCurrent: boolean;
schedulingPolicy: "balanced" | "downstream-impact";
roadmap?: AutonomyRoadmap;
activeLease?: AutonomyLease;
blocker?: { at: string; phaseId?: string; itemId?: string; reason: string; kind: "human" | "retryable"; retryAt?: string; recoveryStage?: "retry" | "cooldown" | "alternate_recovery" | "escalated"; recoveryAttempts?: number; recoveryStartedAt?: string };
recoveryStartedAt?: string;
recoveryAttempts?: number;
maxRecoveryMinutes?: number;
runs: AutonomyRun[];
completedLeases: AutonomyCompletedLease[];
events: AutonomyEvent[];
updatedAt: string;
updatedBy: string;
};
export type AutonomyWork = {
token: string;
kind: AutonomyWorkKind;
projectId: string;
roadmapId: string;
roadmapTitle: string;
phaseId: string;
phaseTitle: string;
phaseObjective: string;
itemId?: string;
itemTitle?: string;
deliveryId?: string;
execution?: AutonomyExecutionTarget;
prompt: string;
expiresAt: string;
};
export type AutonomyReportInput = {
token: string;
outcome: AutonomyWorkOutcome;
summary: string;
deliveryId?: string;
discoveredTasks?: string[];
brainstorm?: {
contextSummary?: string;
ideas?: Array<{ id: string; title: string; description: string; score?: number }>;
outcomes?: string[];
selectedIdeaIds?: string[];
nextActions?: string[];
};
blockerType?: "human" | "retryable";
retryAfterMinutes?: number;
};
export type EnqueueAutonomyTaskInput = { title?: string; instruction: string };
const DEFAULTS = {
profile: "normal" as const,
autoBrainstorm: true,
maxBrainstormIterations: 5,
maxConsecutiveFailures: 3,
leaseMinutes: 30,
maxWorkItemsPerRun: 8,
maxRunMinutes: 45,
maxRecoveryMinutes: 120,
} as const;
const asRecord = (value: unknown): Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
const asString = (value: unknown, fallback = "") => typeof value === "string" ? value : fallback;
const asBoolean = (value: unknown, fallback: boolean) => typeof value === "boolean" ? value : fallback;
const asInt = (value: unknown, fallback: number, min: number, max: number) => {
const parsed = typeof value === "number" && Number.isInteger(value) ? value : fallback;
return Math.max(min, Math.min(max, parsed));
};
const json = (value: unknown): Prisma.InputJsonValue => JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
const nowIso = () => new Date().toISOString();
const event = (type: string, actor: string, summary: string): AutonomyEvent => ({ id: randomUUID(), at: nowIso(), type, actor, summary });
const appendEvent = (state: AutonomyState, next: AutonomyEvent): AutonomyState => ({ ...state, events: [...state.events, next].slice(-100) });
const defaultState = (actor = "system"): AutonomyState => ({
version: 1,
enabled: false,
status: "off",
profile: DEFAULTS.profile,
autoBrainstorm: DEFAULTS.autoBrainstorm,
maxBrainstormIterations: DEFAULTS.maxBrainstormIterations,
maxConsecutiveFailures: DEFAULTS.maxConsecutiveFailures,
leaseMinutes: DEFAULTS.leaseMinutes,
maxWorkItemsPerRun: DEFAULTS.maxWorkItemsPerRun,
maxRunMinutes: DEFAULTS.maxRunMinutes,
pauseAfterCurrent: false,
schedulingPolicy: "balanced",
maxRecoveryMinutes: DEFAULTS.maxRecoveryMinutes,
recoveryAttempts: 0,
runs: [],
completedLeases: [],
events: [],
updatedAt: nowIso(),
updatedBy: actor,
});
const normalizeItem = (value: unknown): AutonomyRoadmapItem | undefined => {
const row = asRecord(value);
if (!asString(row.id) || !asString(row.title)) return undefined;
const status = ["pending", "in_progress", "completed", "blocked"].includes(asString(row.status)) ? asString(row.status) as RoadmapItemStatus : "pending";
return {
id: asString(row.id), title: asString(row.title), status, attempts: asInt(row.attempts, 0, 0, 100),
...(asString(row.instruction) ? { instruction: asString(row.instruction) } : {}),
...(["roadmap", "user_assignment", "brainstorm", "recovery", "finding", "delivery", "release", "remediation", "feature", "bug"].includes(asString(row.source)) ? { source: asString(row.source) as AutonomyRoadmapItem["source"] } : {}),
...(asString(row.createdAt) ? { createdAt: asString(row.createdAt) } : {}),
...(asString(row.deliveryId) ? { deliveryId: asString(row.deliveryId) } : {}),
...(typeof row.priority === "number" ? { priority: asInt(row.priority, 50, 0, 100) } : {}),
...(["required", "committed", "stretch", "future"].includes(asString(row.scope)) ? { scope: asString(row.scope) as AutonomyRoadmapItem["scope"] } : {}),
...(["low", "normal", "medium", "high", "critical"].includes(asString(row.risk)) ? { risk: asString(row.risk) as AutonomyRoadmapItem["risk"] } : {}),
...(typeof row.estimatedScope === "number" ? { estimatedScope: asInt(row.estimatedScope, 0, 0, 10) } : {}),
...(typeof row.releaseBlocking === "boolean" ? { releaseBlocking: row.releaseBlocking } : {}),
...(typeof row.waitingExternal === "boolean" ? { waitingExternal: row.waitingExternal } : {}),
...(Array.isArray(row.touchedPaths) ? { touchedPaths: row.touchedPaths.filter((path): path is string => typeof path === "string").slice(0, 100) } : {}),
...(asString(row.domain) ? { domain: asString(row.domain) } : {}),
...(Array.isArray(row.dependsOn) ? { dependsOn: row.dependsOn.filter((id): id is string => typeof id === "string") } : {}),
...(asString(row.lastResult) ? { lastResult: asString(row.lastResult) } : {}),
...(asString(row.lastError) ? { lastError: asString(row.lastError) } : {}),
...(Array.isArray(row.completionEvidence) ? { completionEvidence: row.completionEvidence.filter((value): value is string => typeof value === "string").slice(0, 50) } : {}),
...(asString(row.supersededBy) ? { supersededBy: asString(row.supersededBy) } : {}),
};
};
const normalizePhase = (value: unknown): AutonomyRoadmapPhase | undefined => {
const row = asRecord(value);
if (!asString(row.id) || !asString(row.title)) return undefined;
const statuses = ["pending", "active", "brainstorming", "executing", "validating", "completed", "blocked"];
return {
id: asString(row.id),
title: asString(row.title),
objective: asString(row.objective, asString(row.title)),
status: statuses.includes(asString(row.status)) ? asString(row.status) as RoadmapPhaseStatus : "pending",
brainstormIterations: asInt(row.brainstormIterations, 0, 0, 100),
brainstormConverged: asBoolean(row.brainstormConverged, false),
validationFailures: asInt(row.validationFailures, 0, 0, 100),
items: Array.isArray(row.items) ? row.items.map(normalizeItem).filter((item): item is AutonomyRoadmapItem => Boolean(item)) : [],
};
};
const normalizeCompletedLease = (value: unknown): AutonomyCompletedLease | undefined => {
const row = asRecord(value);
const outcome = asString(row.outcome);
if (!asString(row.token) || !["success", "failed", "blocked"].includes(outcome)) return undefined;
return {
token: asString(row.token),
outcome: outcome as AutonomyWorkOutcome,
summary: asString(row.summary),
addedTasks: Array.isArray(row.addedTasks) ? row.addedTasks.filter((item): item is string => typeof item === "string") : [],
completedAt: asString(row.completedAt, nowIso()),
};
};
const normalizeRun = (value: unknown): AutonomyRun | undefined => {
const row = asRecord(value);
if (!asString(row.id) || !asString(row.actor) || !asString(row.startedAt)) return undefined;
return {
id: asString(row.id),
actor: asString(row.actor),
startedAt: asString(row.startedAt),
reportedWorkItems: asInt(row.reportedWorkItems, 0, 0, 10_000),
};
};
export const normalizeAutonomyState = (raw: unknown, actor = "system"): AutonomyState => {
const row = asRecord(raw);
if (!Object.keys(row).length) return defaultState(actor);
const roadmapRow = asRecord(row.roadmap);
const phases = Array.isArray(roadmapRow.phases) ? roadmapRow.phases.map(normalizePhase).filter((phase): phase is AutonomyRoadmapPhase => Boolean(phase)) : [];
const roadmap: AutonomyRoadmap | undefined = asString(roadmapRow.id) && phases.length ? {
id: asString(roadmapRow.id), title: asString(roadmapRow.title, "Roadmap"), sourceMarkdown: asString(roadmapRow.sourceMarkdown),
status: ["active", "completed", "blocked"].includes(asString(roadmapRow.status)) ? asString(roadmapRow.status) as AutonomyRoadmap["status"] : "active",
currentPhaseIndex: asInt(roadmapRow.currentPhaseIndex, 0, 0, Math.max(0, phases.length - 1)), phases,
importedAt: asString(roadmapRow.importedAt, nowIso()), ...(asString(roadmapRow.completedAt) ? { completedAt: asString(roadmapRow.completedAt) } : {}),
} : undefined;
const leaseRow = asRecord(row.activeLease);
const leaseKind = asString(leaseRow.kind);
const activeLease: AutonomyLease | undefined = asString(leaseRow.token) && asString(leaseRow.phaseId) && ["brainstorm", "implement", "validate_phase"].includes(leaseKind) ? {
token: asString(leaseRow.token), kind: leaseKind as AutonomyWorkKind, actor: asString(leaseRow.actor),
...(asString(leaseRow.runId) ? { runId: asString(leaseRow.runId) } : {}),
roadmapId: asString(leaseRow.roadmapId), phaseId: asString(leaseRow.phaseId), ...(asString(leaseRow.itemId) ? { itemId: asString(leaseRow.itemId) } : {}),
createdAt: asString(leaseRow.createdAt), expiresAt: asString(leaseRow.expiresAt),
} : undefined;
const blockerRow = asRecord(row.blocker);
const enabled = asBoolean(row.enabled, false);
const blockerKind = recoverStoredBlockerKind(asString(blockerRow.reason), asString(blockerRow.kind));
const blockedAt = Date.parse(asString(blockerRow.at));
const recoveryStage = ["retry", "cooldown", "alternate_recovery", "escalated"].includes(asString(blockerRow.recoveryStage)) ? asString(blockerRow.recoveryStage) as NonNullable<AutonomyState["blocker"]>["recoveryStage"] : undefined;
const blocker = asString(blockerRow.reason) ? {
at: asString(blockerRow.at, nowIso()),
reason: asString(blockerRow.reason),
kind: blockerKind,
...(blockerKind === "retryable" ? { retryAt: asString(blockerRow.retryAt) || new Date((Number.isFinite(blockedAt) ? blockedAt : Date.now()) + 15 * 60_000).toISOString() } : {}),
...(asString(blockerRow.phaseId) ? { phaseId: asString(blockerRow.phaseId) } : {}),
...(asString(blockerRow.itemId) ? { itemId: asString(blockerRow.itemId) } : {}),
...(recoveryStage ? { recoveryStage } : {}),
...(typeof blockerRow.recoveryAttempts === "number" ? { recoveryAttempts: asInt(blockerRow.recoveryAttempts, 0, 0, 100) } : {}),
...(asString(blockerRow.recoveryStartedAt) ? { recoveryStartedAt: asString(blockerRow.recoveryStartedAt) } : {}),
} : undefined;
const status: AutonomyStatus = !enabled
? "off"
: !roadmap
? "waiting_roadmap"
: blocker || roadmap.status === "blocked"
? "blocked"
: roadmap.status === "completed"
? "completed"
: "running";
const leaseMatchesRoadmap = Boolean(enabled && activeLease && roadmap && activeLease.roadmapId === roadmap.id && roadmap.phases.some((phase) => phase.id === activeLease.phaseId && (!activeLease.itemId || phase.items.some((item) => item.id === activeLease.itemId))));
return {
version: 1,
enabled, status,
profile: ["conservative", "normal", "autonomous", "custom"].includes(asString(row.profile)) ? asString(row.profile) as AutonomyProfile : DEFAULTS.profile,
autoBrainstorm: asBoolean(row.autoBrainstorm, DEFAULTS.autoBrainstorm),
maxBrainstormIterations: asInt(row.maxBrainstormIterations, DEFAULTS.maxBrainstormIterations, 1, 10),
maxConsecutiveFailures: asInt(row.maxConsecutiveFailures, DEFAULTS.maxConsecutiveFailures, 1, 10),
leaseMinutes: asInt(row.leaseMinutes, DEFAULTS.leaseMinutes, 5, 120),
maxWorkItemsPerRun: asInt(row.maxWorkItemsPerRun, DEFAULTS.maxWorkItemsPerRun, 1, 20),
maxRunMinutes: asInt(row.maxRunMinutes, DEFAULTS.maxRunMinutes, 5, 55),
pauseAfterCurrent: asBoolean(row.pauseAfterCurrent, false),
schedulingPolicy: row.schedulingPolicy === "downstream-impact" ? "downstream-impact" : "balanced",
maxRecoveryMinutes: asInt(row.maxRecoveryMinutes, DEFAULTS.maxRecoveryMinutes, 15, 1_440),
recoveryAttempts: asInt(row.recoveryAttempts, 0, 0, 100),
...(roadmap ? { roadmap } : {}), ...(leaseMatchesRoadmap && activeLease ? { activeLease } : {}),
...(blocker ? { blocker } : {}),
runs: Array.isArray(row.runs) ? row.runs.map(normalizeRun).filter((entry): entry is AutonomyRun => Boolean(entry)).slice(-50) : [],
completedLeases: Array.isArray(row.completedLeases) ? row.completedLeases.map(normalizeCompletedLease).filter((entry): entry is AutonomyCompletedLease => Boolean(entry)).slice(-50) : [],
events: Array.isArray(row.events) ? row.events.map((entry) => {
const e = asRecord(entry); return { id: asString(e.id, randomUUID()), at: asString(e.at, nowIso()), type: asString(e.type, "event"), actor: asString(e.actor, "system"), summary: asString(e.summary) };
}).slice(-100) : [],
updatedAt: asString(row.updatedAt, nowIso()), updatedBy: asString(row.updatedBy, actor),
};
};
const getProjectRow = async (projectIdOrSlug: string) => {
const project = await prisma.project.findFirst({ where: { deletedAt: null, OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }, { readableId: projectIdOrSlug }] }, include: { workspaceState: true } });
if (!project) throw new Error("PROJECT_NOT_FOUND");
return project;
};
export type AutonomyProjectRow = Awaited<ReturnType<typeof getProjectRow>>;
export const getAutonomyProject = getProjectRow;
const readState = (project: Awaited<ReturnType<typeof getProjectRow>>, actor = "system") => {
const notes = asRecord(project.workspaceState?.notesJson);
return normalizeAutonomyState(notes.autonomy, actor);
};
export const readAutonomyState = readState;
export const releaseActiveAutonomyLease = (state: AutonomyState, actor: string) => {
if (!state.activeLease || !state.roadmap) return state;
const next = structuredClone(state) as AutonomyState;
const lease = next.activeLease!;
const phase = next.roadmap!.phases.find((entry) => entry.id === lease.phaseId);
const item = lease.itemId ? phase?.items.find((entry) => entry.id === lease.itemId) : undefined;
if (item?.status === "in_progress") item.status = "pending";
if (phase && !["completed", "blocked"].includes(phase.status)) phase.status = "active";
delete next.activeLease;
return appendEvent(next, event("autonomy.lease.released", actor, `Released ${lease.kind} lease because Auto Mode was disabled.`));
};
// Update only autonomy with a monotonic CAS version; preserve unrelated notes.
const saveState = async (project: Awaited<ReturnType<typeof getProjectRow>>, state: AutonomyState, actor: string) => {
const storedAutonomy = asRecord(asRecord(project.workspaceState?.notesJson).autonomy);
const storedUpdatedAt = asString(storedAutonomy.updatedAt);
const priorTimestamp = Date.parse(storedUpdatedAt);
const savedAt = new Date(Math.max(Date.now(), Number.isFinite(priorTimestamp) ? priorTimestamp + 1 : 0));
const next: AutonomyState = { ...state, updatedAt: savedAt.toISOString(), updatedBy: actor };
const serialized = JSON.stringify(next);
if (project.workspaceState) {
const updated = storedUpdatedAt
? await prisma.$executeRaw`
UPDATE "WorkspaceState"
SET
"notesJson" = jsonb_set(COALESCE("notesJson"::jsonb, '{}'::jsonb), '{autonomy}', ${serialized}::jsonb, true),
"updatedBy" = ${actor},
"sourceType" = 'autonomy',
"updatedAt" = ${savedAt}
WHERE "projectId" = ${project.id}
AND "notesJson"->'autonomy'->>'updatedAt' = ${storedUpdatedAt}
`
: await prisma.$executeRaw`
UPDATE "WorkspaceState"
SET
"notesJson" = jsonb_set(COALESCE("notesJson"::jsonb, '{}'::jsonb), '{autonomy}', ${serialized}::jsonb, true),
"updatedBy" = ${actor},
"sourceType" = 'autonomy',
"updatedAt" = ${savedAt}
WHERE "projectId" = ${project.id}
AND COALESCE("notesJson"->'autonomy'->>'updatedAt', '') = ''
`;
if (updated !== 1) throw new Error("AUTO_MODE_CONFLICT");
} else {
try {
await prisma.workspaceState.create({
data: { projectId: project.id, primaryObjective: project.description, activeSection: "workspace", notesJson: json({ autonomy: next }), createdBy: actor, updatedBy: actor, sourceType: "autonomy", updatedAt: savedAt },
});
} catch (error) {
if (asString(asRecord(error).code) === "P2002") throw new Error("AUTO_MODE_CONFLICT", { cause: error });
throw error;
}
}
return next;
};
export const saveAutonomyState = saveState;
export const appendAutonomyEvent = appendEvent;
export const createAutonomyEvent = event;
const assignmentTitle = (input: EnqueueAutonomyTaskInput) => {
const explicit = input.title?.trim();
if (explicit) return explicit.slice(0, 200);
const firstLine = input.instruction.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "Nieuwe ProjectBrain-opdracht";
return firstLine.slice(0, 160);
};
export const enqueueAutonomyTaskInState = (current: AutonomyState, input: EnqueueAutonomyTaskInput, actor = "owner") => {
const next = structuredClone(current) as AutonomyState;
const createdAt = nowIso();
const task: AutonomyRoadmapItem = {
id: `assignment-${randomUUID()}`,
title: assignmentTitle(input),
instruction: input.instruction.trim(),
source: "user_assignment",
createdAt,
status: "pending",
attempts: 0,
};
const createAssignmentPhase = (): AutonomyRoadmapPhase => ({
id: `projectbrain-opdrachten-${randomUUID()}`,
title: "ProjectBrain-opdrachten",
objective: "Werk de opdrachten uit de interne ProjectBrain-takenlijst veilig, controleerbaar en in volgorde af.",
status: "active",
brainstormIterations: 0,
brainstormConverged: false,
validationFailures: 0,
items: [task],
});
if (!next.roadmap || next.roadmap.status === "completed") {
next.roadmap = {
id: `roadmap-${randomUUID()}`,
title: "ProjectBrain-opdrachten",
sourceMarkdown: `# ProjectBrain-opdrachten\n\n## Opdrachten\n- [ ] ${task.title}`,
status: "active",
currentPhaseIndex: 0,
phases: [createAssignmentPhase()],
importedAt: createdAt,
};
delete next.blocker;
delete next.activeLease;
} else {
const currentPhase = next.roadmap.phases[next.roadmap.currentPhaseIndex];
if (currentPhase && currentPhase.status !== "completed") {
currentPhase.items.unshift(task);
if (!["blocked", "executing", "brainstorming", "validating"].includes(currentPhase.status)) currentPhase.status = "active";
} else {
const phase = createAssignmentPhase();
next.roadmap.phases.splice(Math.min(next.roadmap.currentPhaseIndex + 1, next.roadmap.phases.length), 0, phase);
}
next.roadmap.sourceMarkdown = `${next.roadmap.sourceMarkdown.trim()}\n- [ ] ${task.title}`.trim();
}
if (next.enabled && next.roadmap.status !== "blocked" && !next.blocker) next.status = "running";
else if (!next.enabled) next.status = "off";
const state = appendEvent(next, event("autonomy.task.enqueued", actor, `Queued ProjectBrain assignment '${task.title}' with user priority.`));
return { state, task };
};
const expireLeaseIfNeeded = (state: AutonomyState) => {
if (!state.activeLease || new Date(state.activeLease.expiresAt).getTime() > Date.now()) return state;
const next = structuredClone(state) as AutonomyState;
const lease = next.activeLease!;
const phase = next.roadmap?.phases.find((entry) => entry.id === lease.phaseId);
const item = lease.itemId ? phase?.items.find((entry) => entry.id === lease.itemId) : undefined;
if (item?.status === "in_progress") item.status = "pending";
if (phase && !["completed", "blocked"].includes(phase.status)) phase.status = "active";
delete next.activeLease;
return appendEvent(next, event("autonomy.lease.expired", "system", `Expired ${lease.kind} lease for ${phase?.title ?? lease.phaseId}.`));
};
export const retryAutonomyBlockerIfDue = (state: AutonomyState, now = Date.now()) => {
if (!state.blocker || state.blocker.kind !== "retryable" || state.blocker.recoveryStage === "escalated" || !state.blocker.retryAt || Date.parse(state.blocker.retryAt) > now || !state.roadmap) return state;
const next = structuredClone(state) as AutonomyState;
const blocker = next.blocker!;
const recoveryStartedAt = Date.parse(blocker.recoveryStartedAt ?? next.recoveryStartedAt ?? blocker.at);
const recoveryLimit = Math.max(15, next.maxRecoveryMinutes ?? DEFAULTS.maxRecoveryMinutes) * 60_000;
if (Number.isFinite(recoveryStartedAt) && now - recoveryStartedAt >= recoveryLimit) {
const phase = blocker.phaseId ? next.roadmap!.phases.find((entry) => entry.id === blocker.phaseId) : undefined;
if (phase) phase.status = "blocked";
next.roadmap!.status = "blocked";
next.status = "blocked";
next.blocker = {
...blocker,
recoveryStage: "escalated",
recoveryAttempts: Math.max(blocker.recoveryAttempts ?? 0, next.recoveryAttempts ?? 0),
recoveryStartedAt: new Date(recoveryStartedAt).toISOString(),
reason: `${blocker.reason} Automatic recovery exceeded its wall-clock limit and needs escalation.`,
};
delete next.blocker.retryAt;
return appendEvent(next, event("autonomy.recovery.escalated", "system", "Automatic recovery exceeded its wall-clock limit."));
}
const phase = blocker.phaseId
? next.roadmap!.phases.find((entry) => entry.id === blocker.phaseId)
: next.roadmap!.phases[next.roadmap!.currentPhaseIndex];
const item = blocker.itemId ? phase?.items.find((entry) => entry.id === blocker.itemId) : undefined;
if (item?.status === "blocked") item.status = "pending";
if (phase?.status === "blocked") phase.status = "active";
next.roadmap!.status = "active";
next.status = next.enabled ? "running" : "off";
delete next.blocker;
delete next.activeLease;
return appendEvent(next, event("autonomy.blocker.retry_ready", "system", "Retryable blocker cooldown elapsed; work is claimable again."));
};
export const evaluateAutonomyRunBudget = (state: AutonomyState, actor: string, runId: string, now = Date.now()) => {
const run = state.runs.find((entry) => entry.id === runId);
if (run && run.actor !== actor) return { allowed: false, reason: "owner_mismatch" as const, run };
if (!run) return { allowed: true, reason: undefined, run: { id: runId, actor, startedAt: new Date(now).toISOString(), reportedWorkItems: 0 } satisfies AutonomyRun };
if (run.reportedWorkItems >= state.maxWorkItemsPerRun) return { allowed: false, reason: "item_limit" as const, run };
if (Date.parse(run.startedAt) + state.maxRunMinutes * 60_000 <= now) return { allowed: false, reason: "time_limit" as const, run };
return { allowed: true, reason: undefined, run };
};
export const getAutonomyState = async (projectIdOrSlug: string) => {
const project = await getProjectRow(projectIdOrSlug);
const original = readState(project);
const state = retryAutonomyBlockerIfDue(expireLeaseIfNeeded(original));
if (original.activeLease?.token === state.activeLease?.token && original.blocker?.retryAt === state.blocker?.retryAt) return state;
return saveState(project, state, "system");
};
export const configureAutonomy = async (projectIdOrSlug: string, patch: Partial<Pick<AutonomyState, "enabled" | "profile" | "autoBrainstorm" | "maxBrainstormIterations" | "maxConsecutiveFailures" | "leaseMinutes" | "maxWorkItemsPerRun" | "maxRunMinutes" | "maxRecoveryMinutes" | "pauseAfterCurrent" | "schedulingPolicy">>, actor = "owner") => {
const project = await getProjectRow(projectIdOrSlug);
let state = retryAutonomyBlockerIfDue(expireLeaseIfNeeded(readState(project, actor)));
const requestedProfile = patch.profile && Object.hasOwn(autonomyProfileSettings, patch.profile) ? patch.profile : undefined;
const selectedProfileSettings = requestedProfile ? autonomyProfileSettings[requestedProfile] : undefined;
state = {
...state,
...(requestedProfile ? { profile: requestedProfile } : {}),
...(selectedProfileSettings ?? {}),
...(typeof patch.autoBrainstorm === "boolean" ? { autoBrainstorm: patch.autoBrainstorm } : {}),
...(typeof patch.maxBrainstormIterations === "number" ? { maxBrainstormIterations: Math.max(1, Math.min(10, Math.trunc(patch.maxBrainstormIterations))) } : {}),
...(typeof patch.maxConsecutiveFailures === "number" ? { maxConsecutiveFailures: Math.max(1, Math.min(10, Math.trunc(patch.maxConsecutiveFailures))) } : {}),
...(typeof patch.leaseMinutes === "number" ? { leaseMinutes: Math.max(5, Math.min(120, Math.trunc(patch.leaseMinutes))) } : {}),
...(typeof patch.maxWorkItemsPerRun === "number" ? { maxWorkItemsPerRun: Math.max(1, Math.min(20, Math.trunc(patch.maxWorkItemsPerRun))) } : {}),
...(typeof patch.maxRunMinutes === "number" ? { maxRunMinutes: Math.max(5, Math.min(55, Math.trunc(patch.maxRunMinutes))) } : {}),
...(typeof patch.maxRecoveryMinutes === "number" ? { maxRecoveryMinutes: Math.max(15, Math.min(1_440, Math.trunc(patch.maxRecoveryMinutes))) } : {}),
...(typeof patch.pauseAfterCurrent === "boolean" ? { pauseAfterCurrent: patch.pauseAfterCurrent } : {}),
...(patch.schedulingPolicy === "balanced" || patch.schedulingPolicy === "downstream-impact" ? { schedulingPolicy: patch.schedulingPolicy } : {}),
};
if (typeof patch.enabled === "boolean") {
if (!patch.enabled && state.activeLease && state.roadmap) state = releaseActiveAutonomyLease(state, actor);
state.enabled = patch.enabled;
if (!patch.enabled) state.status = "off";
else if (state.roadmap?.status === "completed") state.status = "completed";
else if (state.blocker || state.roadmap?.status === "blocked") state.status = "blocked";
else state.status = state.roadmap ? "running" : "waiting_roadmap";
state = appendEvent(state, event(patch.enabled ? "autonomy.enabled" : "autonomy.disabled", actor, patch.enabled ? "Auto Mode enabled." : "Auto Mode disabled."));
}
return saveState(project, state, actor);
};
export const importAutonomyRoadmap = async (projectIdOrSlug: string, input: { title?: string; markdown: string }, actor = "owner") => {
const project = await getProjectRow(projectIdOrSlug);
let state = expireLeaseIfNeeded(readState(project, actor));
if (state.roadmap?.sourceMarkdown.trim() === input.markdown.trim()) {
if (input.title?.trim() && input.title.trim() !== state.roadmap.title) {
state.roadmap.title = input.title.trim();
return saveState(project, state, actor);
}
return state;
}
if (state.activeLease) throw new Error("AUTO_MODE_WORK_ACTIVE");
const roadmap = parseRoadmapMarkdown(input.markdown, input.title);
state = { ...state, roadmap, activeLease: undefined, blocker: undefined, completedLeases: [], status: state.enabled ? "running" : "off" };
state = appendEvent(state, event("autonomy.roadmap.imported", actor, `Imported '${roadmap.title}' with ${roadmap.phases.length} phase(s) and ${roadmap.phases.reduce((count, phase) => count + phase.items.length, 0)} item(s).`));
return saveState(project, state, actor);
};
export const resumeAutonomy = async (projectIdOrSlug: string, actor = "owner") => {
const project = await getProjectRow(projectIdOrSlug);
let state = expireLeaseIfNeeded(readState(project, actor));
if (!state.roadmap || state.roadmap.status === "completed") return state;
if (state.status !== "blocked" && state.roadmap.status !== "blocked" && !state.blocker) return state;
const blockedPhase = state.blocker?.phaseId ? state.roadmap.phases.find((phase) => phase.id === state.blocker?.phaseId) : state.roadmap.phases[state.roadmap.currentPhaseIndex];
if (blockedPhase) {
if (blockedPhase.status === "blocked") blockedPhase.status = "active";
blockedPhase.validationFailures = 0;
if (state.blocker?.itemId) {
const blockedItem = blockedPhase.items.find((item) => item.id === state.blocker?.itemId);
if (blockedItem?.status === "blocked") { blockedItem.status = "pending"; delete blockedItem.lastError; }
}
}
state.roadmap.status = "active";
delete state.blocker;
delete state.activeLease;
state.recoveryAttempts = 0;
delete state.recoveryStartedAt;
state.status = state.enabled ? "running" : "off";
state = appendEvent(state, event("autonomy.resumed", actor, "Auto Mode blocker cleared; roadmap is resumable."));
return saveState(project, state, actor);
};
export const canResumeAutonomyLease = (lease: AutonomyLease, actor: string, runId?: string) => {
if (lease.actor !== actor) return false;
if (lease.runId || runId) return Boolean(lease.runId && runId && lease.runId === runId);
return true;
};
export const isAutonomyLeaseOwner = (lease: AutonomyLease, actor: string) => lease.actor === actor;
const nextLease = (state: AutonomyState, actor: string, kind: AutonomyWorkKind, phase: AutonomyRoadmapPhase, item?: AutonomyRoadmapItem, runId?: string): AutonomyLease => {
const createdAt = new Date();
return { token: randomUUID(), kind, actor, ...(runId ? { runId } : {}), roadmapId: state.roadmap!.id, phaseId: phase.id, ...(item ? { itemId: item.id } : {}), createdAt: createdAt.toISOString(), expiresAt: new Date(createdAt.getTime() + state.leaseMinutes * 60_000).toISOString() };
};
export const createAutonomyLease = nextLease;
const applyRetryableBlocker = (state: AutonomyState, phase: AutonomyRoadmapPhase, summary: string, failureCount: number, item?: AutonomyRoadmapItem, requestedDelay?: number) => {
const blockedAt = Date.now();
const recoveryStartedAt = state.recoveryStartedAt ?? new Date(blockedAt).toISOString();
const recoveryAttempts = (state.recoveryAttempts ?? 0) + 1;
const decision = decideRecovery({
failureCount,
recoveryAttempts,
recoveryStartedAt,
maxRecoveryMinutes: state.maxRecoveryMinutes ?? DEFAULTS.maxRecoveryMinutes,
now: blockedAt,
});
state.recoveryStartedAt = recoveryStartedAt;
state.recoveryAttempts = recoveryAttempts;
if (decision.stage === "alternate_recovery") {
if (item) { item.status = "pending"; item.lastError = `${summary} Alternate recovery is being attempted.`; }
phase.status = "active";
state.roadmap!.status = "active";
state.status = state.enabled ? "running" : "off";
delete state.blocker;
return decision;
}
if (item) { item.status = "blocked"; item.lastError = summary; }
phase.status = "blocked";
state.roadmap!.status = "blocked";
state.status = "blocked";
const retryAfterMinutes = decision.exhausted
? undefined
: Math.max(5, Math.min(1_440, requestedDelay ?? decision.retryAfterMinutes ?? autonomyRetryDelayMinutes(failureCount)));
state.blocker = {
at: new Date(blockedAt).toISOString(), phaseId: phase.id, ...(item ? { itemId: item.id } : {}), reason: summary,
kind: "retryable", ...(retryAfterMinutes ? { retryAt: new Date(blockedAt + retryAfterMinutes * 60_000).toISOString() } : {}),
recoveryStage: decision.stage, recoveryAttempts, recoveryStartedAt,
};
return decision;
};
const applyHumanBlocker = (state: AutonomyState, phase: AutonomyRoadmapPhase, summary: string, item?: AutonomyRoadmapItem) => {
if (item) { item.status = "blocked"; item.lastError = summary; }
phase.status = "blocked";
state.roadmap!.status = "blocked";
state.status = "blocked";
state.blocker = { at: nowIso(), phaseId: phase.id, ...(item ? { itemId: item.id } : {}), reason: summary, kind: "human" };
};
export const reportAutonomyWork = async (projectIdOrSlug: string, input: AutonomyReportInput, actor = "chatgpt") => {
const project = await getProjectRow(projectIdOrSlug);
let state = readState(project, actor);
const replay = state.completedLeases.find((entry) => entry.token === input.token);
if (replay) return { state, addedTasks: replay.addedTasks, replayed: true, runId: undefined };
const lease = state.activeLease;
if (!lease || lease.token !== input.token) throw new Error("AUTO_MODE_LEASE_STALE");
if (!isAutonomyLeaseOwner(lease, actor)) throw new Error("AUTO_MODE_LEASE_OWNER_MISMATCH");
if (!state.roadmap || lease.roadmapId !== state.roadmap.id) throw new Error("AUTO_MODE_ROADMAP_CHANGED");
const phase = state.roadmap.phases.find((entry) => entry.id === lease.phaseId);
if (!phase) throw new Error("AUTO_MODE_PHASE_NOT_FOUND");
const item = lease.itemId ? phase.items.find((entry) => entry.id === lease.itemId) : undefined;
const deliveryId = input.deliveryId ?? item?.deliveryId;
const reconciledGiteaCompletion = item ? hasGiteaAutonomyCompletionEvidence(item.completionEvidence) : false;
if (lease.kind === "implement" && input.outcome === "success" && !deliveryId && !reconciledGiteaCompletion) throw new Error("AUTO_MODE_DELIVERY_REQUIRED");
if (deliveryId) {
const delivery = await getFeatureDelivery(project.slug, deliveryId);
if (item?.deliveryId && item.deliveryId !== delivery.id) throw new Error("AUTO_MODE_DELIVERY_ALREADY_LINKED");
if (input.outcome === "success" && lease.kind === "implement" && (!['merged', 'verified', 'released'].includes(delivery.spec.status) || !delivery.spec.pullRequest?.mergeCommitSha || !delivery.spec.validationEvidence)) throw new Error("AUTO_MODE_DELIVERY_NOT_MERGED");
if (item) item.deliveryId = delivery.id;
}
if (lease.kind === "validate_phase" && input.outcome === "success") {
for (const completed of phase.items.filter((entry) => entry.deliveryId && isAutonomyItemInExecutionScope(entry))) {
const delivery = await getFeatureDelivery(project.slug, completed.deliveryId!);
if (!["merged", "verified", "released"].includes(delivery.spec.status) || !delivery.spec.validationEvidence || !delivery.spec.pullRequest?.mergeCommitSha) throw new Error("AUTO_MODE_DELIVERY_NOT_MERGED");
}
}
const repairTasks = input.outcome === "success" ? [] : input.discoveredTasks ?? [];
const brainstormTasks = input.brainstorm?.nextActions ?? [];
const newTasks = [
...addDiscoveredTasks(phase, repairTasks, { scope: "committed", source: "recovery" }),
...addDiscoveredTasks(phase, brainstormTasks),
];
const hasRepairs = item && repairTasks.length > 0 && input.outcome !== "success" && classifyAutonomyBlocker(input.summary, input.blockerType) !== "human" && orderAutonomyRepairs(phase, item, repairTasks);
if (hasRepairs) {
item.status = "pending"; item.lastError = input.summary;
phase.status = "active"; state.status = "running"; state.roadmap.status = "active";
delete state.blocker;
} else if (input.outcome === "blocked") {
if (!item) phase.validationFailures += 1;
const failureCount = Math.max(1, item ? item.attempts : phase.validationFailures);
const blockerKind = classifyAutonomyBlocker(input.summary, input.blockerType);
if (blockerKind === "human") applyHumanBlocker(state, phase, input.summary, item);
else applyRetryableBlocker(state, phase, input.summary, failureCount, item, input.retryAfterMinutes);
} else if (lease.kind === "brainstorm") {
phase.brainstormIterations += 1;
if (input.outcome === "failed") {
phase.validationFailures += 1;
if (phase.validationFailures >= state.maxConsecutiveFailures) applyRetryableBlocker(state, phase, input.summary, phase.validationFailures, undefined, input.retryAfterMinutes);
else phase.status = "active";
} else if (newTasks.length && hasOpenAutonomyExecutionItems(phase)) {
phase.validationFailures = 0;
phase.brainstormConverged = false;
phase.status = "executing";
} else {
phase.validationFailures = 0;
phase.brainstormConverged = true;
phase.status = "validating";
}
await createBrainstormForProject(project.slug, {
title: `Auto brainstorm · ${phase.title} · round ${phase.brainstormIterations}`,
prompt: workPrompt("brainstorm", phase), scope: "roadmap_phase", mode: "autonomy_loop",
status: input.outcome === "failed" ? "failed" : hasOpenAutonomyExecutionItems(phase) ? "selected" : "converged", targetEntityId: phase.id, targetEntityType: "roadmap_phase", generatedBy: actor,
contextSummary: input.brainstorm?.contextSummary?.trim() || input.summary,
ideas: input.brainstorm?.ideas ?? [], clusters: [],
outcomes: (input.brainstorm?.outcomes ?? [input.summary]).map((summary, index) => ({ id: `outcome-${index + 1}`, summary })),
selectedIdeaIds: input.brainstorm?.selectedIdeaIds ?? [], nextActions: newTasks,
});
} else if (lease.kind === "implement") {
if (!item) throw new Error("AUTO_MODE_ITEM_NOT_FOUND");
if (input.outcome === "success") {
phase.validationFailures = 0;
item.status = "completed";
item.lastResult = input.summary;
delete item.lastError;
if (input.deliveryId) item.deliveryId = input.deliveryId;
phase.status = "active";
} else {
item.lastError = input.summary;
if (item.attempts >= state.maxConsecutiveFailures) applyRetryableBlocker(state, phase, input.summary, item.attempts, item, input.retryAfterMinutes);
else { item.status = "pending"; phase.status = "active"; }
}
} else {
if (input.outcome === "success" && !hasOpenAutonomyExecutionItems(phase)) {
phase.status = "completed";
phase.brainstormConverged = true;
phase.validationFailures = 0;
if (state.roadmap.currentPhaseIndex < state.roadmap.phases.length - 1) {
state.roadmap.currentPhaseIndex += 1;
const next = state.roadmap.phases[state.roadmap.currentPhaseIndex];
if (next.status !== "completed") next.status = "active";
} else {
state.roadmap.status = "completed";
state.roadmap.completedAt = nowIso();
state.status = "completed";
}
} else {
phase.validationFailures += 1;
phase.brainstormConverged = !hasOpenAutonomyExecutionItems(phase);
if (phase.validationFailures >= state.maxConsecutiveFailures && !hasOpenAutonomyExecutionItems(phase)) applyRetryableBlocker(state, phase, input.summary, phase.validationFailures, undefined, input.retryAfterMinutes);
else phase.status = hasOpenAutonomyExecutionItems(phase) ? "executing" : "active";
}
}
if (input.outcome === "success") {
state.recoveryAttempts = 0;
delete state.recoveryStartedAt;
}
delete state.activeLease;
if (lease.runId) {
const run = state.runs.find((entry) => entry.id === lease.runId);
if (run) run.reportedWorkItems += 1;
else state.runs = [...state.runs, { id: lease.runId, actor: lease.actor, startedAt: lease.createdAt, reportedWorkItems: 1 }].slice(-50);
}
state.completedLeases = [...state.completedLeases, { token: lease.token, outcome: input.outcome, summary: input.summary, addedTasks: newTasks, completedAt: nowIso() }].slice(-50);
state = appendEvent(state, event(`autonomy.work.${input.outcome}`, actor, `${lease.kind} · ${phase.title}: ${input.summary}${newTasks.length ? ` Added ${newTasks.length} new task(s).` : ""}`));
const completedRoadmap = state.roadmap;
if (completedRoadmap?.status === "completed") state = appendEvent(state, event("autonomy.roadmap.completed", actor, `Roadmap '${completedRoadmap.title}' completed.`));
state = await saveState(project, state, actor);
return { state, addedTasks: newTasks, replayed: false, runId: lease.runId };
};
@@ -0,0 +1,625 @@
import type { Prisma } from "@prisma/client";
import { ApprovalState, EvidenceStrength, ReviewStatus, VerificationState } from "@prisma/client";
import { prisma } from "../db/prisma.js";
import { listContextPacksForProject } from "./contextPacksRepository.js";
import { getProjectBySlugOrId } from "./projectsRepository.js";
import {
createBlueprintImpactReportDto,
createBlueprintNodeRunPreviewDto,
createBlueprintPlanDto,
createBlueprintRepoExportBundleDto,
type BlueprintDepthDto,
type BlueprintNodeDto,
type BlueprintPlanDto,
} from "../services/blueprintEngine.js";
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 toStringArray = (value: unknown): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
type BlueprintDelegate = {
create: (args: unknown) => Promise<unknown>;
findFirst: (args: unknown) => Promise<unknown>;
update: (args: unknown) => Promise<unknown>;
updateMany: (args: unknown) => Promise<unknown>;
};
type BlueprintPrisma = {
blueprintGraph: BlueprintDelegate;
blueprintNode: BlueprintDelegate;
blueprintNodeRun: BlueprintDelegate;
};
const blueprintDb = prisma as unknown as BlueprintPrisma;
const findProjectRecord = (projectIdOrSlug: string) => prisma.project.findFirst({
where: { deletedAt: null, OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }, { readableId: projectIdOrSlug }] },
select: { id: true, slug: true, activeContextPackId: true },
});
type BlueprintGraphRecord = {
id: string;
slug: string | null;
readableId: string | null;
projectId: string;
project?: { slug: string };
archetypeId: string;
archetypeLabel: string;
depth: string;
status: string;
documentRoot: string;
intakeSummary: string;
assumptionsJson: Prisma.JsonValue;
exportManifestJson: Prisma.JsonValue;
recommendedNextActionsJson: Prisma.JsonValue;
metadataJson: Prisma.JsonValue | null;
createdAt: Date;
updatedAt: Date;
nodes?: BlueprintNodeRecord[];
};
type BlueprintNodeRecord = {
id: string;
templateId: string;
kind: string;
title: string;
cluster: string;
status: string;
generationOrder: number;
requiredDepth: string;
dependsOnJson: Prisma.JsonValue;
dependencyTitlesJson: Prisma.JsonValue;
blockedByJson: Prisma.JsonValue;
outputPath: string;
purpose: string;
requiredContextJson: Prisma.JsonValue;
qualityChecksJson: Prisma.JsonValue;
producesJson: Prisma.JsonValue;
promptPreview: string;
contextSelectionJson: Prisma.JsonValue;
documentationItemId: string | null;
reviewItemId: string | null;
latestRunId: string | null;
staleReason: string | null;
createdAt: Date;
updatedAt: Date;
};
type BlueprintNodeRunRecord = {
id: string;
projectId: string;
graphId: string;
nodeId: string;
status: string;
generatedBy: string;
inputSnapshotJson: Prisma.JsonValue;
prompt: string;
draftMarkdown: string;
qualityReportJson: Prisma.JsonValue;
documentationItemId: string | null;
reviewItemId: string | null;
createdAt: Date;
updatedAt: Date;
};
const asContextSelection = (value: Prisma.JsonValue) => isRecord(value) ? value : { selectedContextPackIds: [], candidates: [], decisionSummary: "No context selection metadata stored." };
const recordToNodeDto = (record: BlueprintNodeRecord, projectSlug: string): BlueprintPlanDto["nodes"][number] => ({
id: record.templateId,
title: record.title,
cluster: record.cluster as BlueprintPlanDto["nodes"][number]["cluster"],
requiredDepth: record.requiredDepth as BlueprintDepthDto,
dependsOn: toStringArray(record.dependsOnJson),
outputPath: record.outputPath,
purpose: record.purpose,
requiredContext: toStringArray(record.requiredContextJson),
qualityChecks: toStringArray(record.qualityChecksJson),
produces: toStringArray(record.producesJson),
projectId: projectSlug,
dependencyTitles: toStringArray(record.dependencyTitlesJson),
blockedBy: toStringArray(record.blockedByJson),
generationOrder: record.generationOrder,
status: record.status,
contextSelection: asContextSelection(record.contextSelectionJson) as BlueprintNodeDto["contextSelection"],
promptPreview: record.promptPreview,
exportReady: record.status !== "blocked",
documentationItemId: record.documentationItemId ?? undefined,
reviewItemId: record.reviewItemId ?? undefined,
latestRunId: record.latestRunId ?? undefined,
staleReason: record.staleReason ?? undefined,
} as BlueprintPlanDto["nodes"][number]);
const recordToPlanDto = (record: BlueprintGraphRecord): BlueprintPlanDto => {
const projectSlug = record.project?.slug ?? record.projectId;
const metadata = isRecord(record.metadataJson) ? record.metadataJson : {};
const nodes = (record.nodes ?? []).sort((left, right) => left.generationOrder - right.generationOrder).map((node) => recordToNodeDto(node, projectSlug));
const clusterIds = [...new Set(nodes.map((node) => node.cluster))];
const clusters = clusterIds.map((clusterId) => {
const clusterNodes = nodes.filter((node) => node.cluster === clusterId);
return {
id: clusterId,
label: String(clusterId).split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" "),
planned: clusterNodes.length,
ready: clusterNodes.filter((node) => node.status !== "blocked").length,
blocked: clusterNodes.filter((node) => node.status === "blocked").length,
exportPaths: clusterNodes.map((node) => node.outputPath),
};
});
return {
id: record.slug ?? record.readableId ?? record.id,
projectId: projectSlug,
archetype: {
id: record.archetypeId as BlueprintPlanDto["archetype"]["id"],
label: record.archetypeLabel,
summary: typeof metadata.archetypeSummary === "string" ? metadata.archetypeSummary : "Persisted blueprint archetype.",
signals: toStringArray(metadata.archetypeSignals),
recommendedDepth: (typeof metadata.recommendedDepth === "string" ? metadata.recommendedDepth : record.depth) as BlueprintDepthDto,
clusters: toStringArray(metadata.archetypeClusters) as BlueprintPlanDto["archetype"]["clusters"],
},
depth: record.depth as BlueprintDepthDto,
status: record.status as BlueprintPlanDto["status"],
generatedAt: record.createdAt.toISOString(),
intakeSummary: record.intakeSummary,
assumptions: toStringArray(record.assumptionsJson),
documentRoot: record.documentRoot,
totalNodes: nodes.length,
readyNodes: nodes.filter((node) => node.status !== "blocked").length,
blockedNodes: nodes.filter((node) => node.status === "blocked").length,
clusters,
nodes,
exportManifest: toStringArray(record.exportManifestJson),
recommendedNextActions: toStringArray(record.recommendedNextActionsJson),
};
};
export const createAndPersistBlueprintPlan = async (projectIdOrSlug: string, depth?: BlueprintDepthDto): Promise<BlueprintPlanDto | undefined> => {
const projectRecord = await findProjectRecord(projectIdOrSlug);
if (!projectRecord) return undefined;
const project = await getProjectBySlugOrId(projectIdOrSlug);
if (!project) return undefined;
const packs = await listContextPacksForProject(projectIdOrSlug) ?? [];
const plan = createBlueprintPlanDto(project, packs, depth);
const now = Date.now().toString(36);
const slug = `bp-${projectRecord.slug}-${plan.archetype.id}-${plan.depth}-${now}`;
const graph = await blueprintDb.blueprintGraph.create({
data: {
projectId: projectRecord.id,
slug,
readableId: slug,
archetypeId: plan.archetype.id,
archetypeLabel: plan.archetype.label,
depth: plan.depth,
status: plan.status,
documentRoot: plan.documentRoot,
intakeSummary: plan.intakeSummary,
assumptionsJson: toJson(plan.assumptions),
exportManifestJson: toJson(plan.exportManifest),
recommendedNextActionsJson: toJson(plan.recommendedNextActions),
metadataJson: toJson({ archetypeSummary: plan.archetype.summary, archetypeSignals: plan.archetype.signals, archetypeClusters: plan.archetype.clusters, recommendedDepth: plan.archetype.recommendedDepth, sourcePlanId: plan.id }),
createdBy: "api",
updatedBy: "api",
sourceType: "blueprint_engine_v52",
nodes: {
create: plan.nodes.map((node) => ({
projectId: projectRecord.id,
templateId: node.id,
kind: node.id,
title: node.title,
cluster: node.cluster,
status: node.status,
generationOrder: node.generationOrder,
requiredDepth: node.requiredDepth,
dependsOnJson: toJson(node.dependsOn),
dependencyTitlesJson: toJson(node.dependencyTitles),
blockedByJson: toJson(node.blockedBy),
outputPath: node.outputPath,
purpose: node.purpose,
requiredContextJson: toJson(node.requiredContext),
qualityChecksJson: toJson(node.qualityChecks),
producesJson: toJson(node.produces),
promptPreview: node.promptPreview,
contextSelectionJson: toJson(node.contextSelection),
createdBy: "api",
updatedBy: "api",
sourceType: "blueprint_engine_v52",
})),
},
},
include: { project: { select: { slug: true } }, nodes: true },
}) as BlueprintGraphRecord;
await prisma.operatingLogEntry.create({
data: {
projectId: projectRecord.id,
slug: `log-${projectRecord.slug}-blueprint-plan-${Date.now().toString(36)}`,
readableId: `log-${projectRecord.slug}-blueprint-plan-${Date.now().toString(36)}`,
type: "blueprint_plan_created",
title: "Blueprint plan persisted",
summary: `${plan.totalNodes} documentation nodes planned for ${plan.archetype.label} at ${plan.depth} depth.`,
linkedEntityIdsJson: toJson([graph.slug ?? graph.id]),
source: "blueprint_engine_v52",
importance: "normal",
payloadJson: toJson({ graphId: graph.slug ?? graph.id, archetypeId: plan.archetype.id, depth: plan.depth, nodes: plan.totalNodes }),
},
});
return recordToPlanDto(graph);
};
export const getLatestBlueprintPlan = async (projectIdOrSlug: string): Promise<BlueprintPlanDto | undefined> => {
const projectRecord = await findProjectRecord(projectIdOrSlug);
if (!projectRecord) return undefined;
const graph = await blueprintDb.blueprintGraph.findFirst({
where: { projectId: projectRecord.id, deletedAt: null },
orderBy: { createdAt: "desc" },
include: { project: { select: { slug: true } }, nodes: true },
}) as BlueprintGraphRecord | null;
return graph ? recordToPlanDto(graph) : undefined;
};
export const getBlueprintPlanById = async (projectIdOrSlug: string, graphId: string): Promise<BlueprintPlanDto | undefined> => {
const projectRecord = await findProjectRecord(projectIdOrSlug);
if (!projectRecord) return undefined;
const graph = await blueprintDb.blueprintGraph.findFirst({
where: { projectId: projectRecord.id, deletedAt: null, OR: [{ id: graphId }, { slug: graphId }, { readableId: graphId }] },
include: { project: { select: { slug: true } }, nodes: true },
}) as BlueprintGraphRecord | null;
return graph ? recordToPlanDto(graph) : undefined;
};
export const runBlueprintNode = async (projectIdOrSlug: string, graphId: string, nodeId: string, options: { createDocumentation?: boolean; sendToReview?: boolean } = {}) => {
const projectRecord = await findProjectRecord(projectIdOrSlug);
if (!projectRecord) return undefined;
const graphRecord = await blueprintDb.blueprintGraph.findFirst({
where: { projectId: projectRecord.id, deletedAt: null, OR: [{ id: graphId }, { slug: graphId }, { readableId: graphId }] },
include: { project: { select: { slug: true } }, nodes: true },
}) as BlueprintGraphRecord | null;
if (!graphRecord) return undefined;
const plan = recordToPlanDto(graphRecord);
const node = plan.nodes.find((candidate) => candidate.id === nodeId || candidate.id === nodeId);
if (!node || node.status === "blocked") return undefined;
const preview = createBlueprintNodeRunPreviewDto(plan, node);
let documentationItemId: string | undefined;
let reviewItemId: string | undefined;
if (options.createDocumentation) {
const slug = `doc-${projectRecord.slug}-${node.id}-${Date.now().toString(36)}`;
const doc = await prisma.documentationItem.create({
data: {
projectId: projectRecord.id,
slug,
readableId: slug,
title: node.title,
kind: "blueprint_document",
status: options.sendToReview ? "draft_review" : "draft",
approvalState: ApprovalState.pending,
content: preview.draftMarkdown,
metadataJson: toJson({
blueprintGraphId: graphRecord.slug ?? graphRecord.id,
blueprintNodeId: node.id,
outputPath: node.outputPath,
linkedFeatureIds: [],
primaryContextPackId: node.contextSelection.primaryContextPackId,
selectedContextPackIds: node.contextSelection.selectedContextPackIds,
contextSelectionSummary: node.contextSelection.decisionSummary,
lastUpdated: new Date().toISOString(),
}),
trustJson: toJson({ source: "blueprint node run", confidence: preview.qualityReport.score, evidence: [], lastChecked: new Date().toISOString(), approvalState: "pending" }),
provenanceJson: toJson({ createdFrom: "blueprint_node_run", graphId: graphRecord.slug ?? graphRecord.id, nodeId: node.id }),
createdBy: "api",
updatedBy: "api",
sourceType: "blueprint_engine_v52",
},
});
documentationItemId = doc.slug ?? doc.id;
}
if (options.sendToReview) {
const slug = `rq-${projectRecord.slug}-blueprint-${node.id}-${Date.now().toString(36)}`;
const review = await prisma.reviewQueueItem.create({
data: {
projectId: projectRecord.id,
slug,
readableId: slug,
type: "documentation_update",
title: `Review blueprint draft: ${node.title}`,
description: preview.draftMarkdown.slice(0, 4000),
priority: "normal",
status: ReviewStatus.pending,
source: "blueprint_engine_v52",
confidence: preview.qualityReport.score,
approvalState: ApprovalState.pending,
evidenceStrength: EvidenceStrength.medium,
verificationState: VerificationState.needs_evidence,
suggestedAction: "Review, edit and accept this generated blueprint document before exporting or treating it as project truth.",
targetEntityType: "documentation_item",
targetEntityId: documentationItemId,
payloadJson: toJson({ graphId: graphRecord.slug ?? graphRecord.id, nodeId: node.id, outputPath: node.outputPath, draftMarkdown: preview.draftMarkdown, qualityReport: preview.qualityReport, contextSelection: node.contextSelection }),
provenanceJson: toJson({ createdFrom: "blueprint_node_run", graphId: graphRecord.slug ?? graphRecord.id, nodeId: node.id }),
createdBy: "api",
updatedBy: "api",
sourceType: "blueprint_engine_v52",
},
});
reviewItemId = review.slug ?? review.id;
}
const nodeRecord = (graphRecord.nodes ?? []).find((candidate) => candidate.templateId === node.id);
if (!nodeRecord) return undefined;
const run = await blueprintDb.blueprintNodeRun.create({
data: {
projectId: projectRecord.id,
graphId: graphRecord.id,
nodeId: nodeRecord.id,
status: "needs_review",
generatedBy: "deterministic",
inputSnapshotJson: toJson(preview.inputSnapshot),
prompt: preview.prompt,
draftMarkdown: preview.draftMarkdown,
qualityReportJson: toJson(preview.qualityReport),
documentationItemId,
reviewItemId,
metadataJson: toJson({ outputPath: node.outputPath, contextSelection: node.contextSelection }),
createdBy: "api",
updatedBy: "api",
sourceType: "blueprint_engine_v52",
},
}) as BlueprintNodeRunRecord;
await blueprintDb.blueprintNode.updateMany({
where: { graphId: graphRecord.id, templateId: node.id },
data: { status: options.sendToReview ? "needs_review" : "pending", latestRunId: run.id, documentationItemId, reviewItemId, updatedBy: "api" },
});
await prisma.operatingLogEntry.create({
data: {
projectId: projectRecord.id,
slug: `log-${projectRecord.slug}-blueprint-run-${Date.now().toString(36)}`,
readableId: `log-${projectRecord.slug}-blueprint-run-${Date.now().toString(36)}`,
type: "blueprint_node_run_created",
title: `Blueprint draft generated: ${node.title}`,
summary: `Generated a review-gated blueprint draft for ${node.outputPath}.`,
linkedEntityIdsJson: toJson([graphRecord.slug ?? graphRecord.id, node.id, documentationItemId, reviewItemId].filter(Boolean)),
source: "blueprint_engine_v52",
importance: "normal",
payloadJson: toJson({ graphId: graphRecord.slug ?? graphRecord.id, nodeId: node.id, documentationItemId, reviewItemId, qualityScore: preview.qualityReport.score }),
},
});
return {
...preview,
id: run.id,
graphId: graphRecord.slug ?? graphRecord.id,
documentationItemId,
reviewItemId,
generatedAt: run.createdAt.toISOString(),
};
};
export const getBlueprintRepoExportBundle = async (projectIdOrSlug: string, graphId: string, options: { includeDrafts?: boolean } = {}) => {
const projectRecord = await findProjectRecord(projectIdOrSlug);
if (!projectRecord) return undefined;
const graph = await blueprintDb.blueprintGraph.findFirst({
where: { projectId: projectRecord.id, deletedAt: null, OR: [{ id: graphId }, { slug: graphId }, { readableId: graphId }] },
include: { project: { select: { slug: true } }, nodes: true },
}) as BlueprintGraphRecord | null;
if (!graph) return undefined;
const plan = recordToPlanDto(graph);
const docIds = (graph.nodes ?? []).map((node) => node.documentationItemId).filter((item): item is string => Boolean(item));
const docs = docIds.length
? await prisma.documentationItem.findMany({
where: { projectId: projectRecord.id, deletedAt: null, OR: docIds.flatMap((id) => [{ id }, { slug: id }, { readableId: id }]) },
})
: [];
const draftByNodeId: Record<string, string> = {};
for (const node of graph.nodes ?? []) {
const doc = docs.find((candidate) => candidate.id === node.documentationItemId || candidate.slug === node.documentationItemId || candidate.readableId === node.documentationItemId);
if (doc?.content) draftByNodeId[node.templateId] = doc.content;
}
const bundle = createBlueprintRepoExportBundleDto(plan, { includeDrafts: options.includeDrafts, draftByNodeId });
await prisma.operatingLogEntry.create({
data: {
projectId: projectRecord.id,
slug: `log-${projectRecord.slug}-blueprint-export-${Date.now().toString(36)}`,
readableId: `log-${projectRecord.slug}-blueprint-export-${Date.now().toString(36)}`,
type: "blueprint_export_prepared",
title: "Blueprint repo export prepared",
summary: `${bundle.files.length} blueprint export file(s) prepared; ${bundle.skippedCount} node(s) skipped.`,
linkedEntityIdsJson: toJson([graph.slug ?? graph.id, ...bundle.files.map((file) => file.nodeId)]),
source: "blueprint_engine_v53",
importance: bundle.skippedCount ? "important" : "normal",
payloadJson: toJson({ graphId: graph.slug ?? graph.id, includeDrafts: bundle.includeDrafts, files: bundle.files.map((file) => file.path), warnings: bundle.warnings }),
},
});
return bundle;
};
export const getBlueprintImpactReport = async (projectIdOrSlug: string, graphId: string, nodeId: string) => {
const plan = await getBlueprintPlanById(projectIdOrSlug, graphId);
if (!plan) return undefined;
if (!plan.nodes.some((node) => node.id === nodeId)) return undefined;
return createBlueprintImpactReportDto(plan, nodeId);
};
export const syncBlueprintReviewAction = async (projectIdOrSlug: string, reviewItemId: string, action: "accept" | "reject") => {
const projectRecord = await findProjectRecord(projectIdOrSlug);
if (!projectRecord) return undefined;
const review = await prisma.reviewQueueItem.findFirst({
where: { projectId: projectRecord.id, deletedAt: null, OR: [{ id: reviewItemId }, { slug: reviewItemId }, { readableId: reviewItemId }] },
});
if (!review || review.source !== "blueprint_engine_v52") return undefined;
const payload = isRecord(review.payloadJson) ? review.payloadJson : {};
const graphId = typeof payload.graphId === "string" ? payload.graphId : undefined;
const nodeId = typeof payload.nodeId === "string" ? payload.nodeId : undefined;
if (!graphId || !nodeId) return undefined;
const graph = await blueprintDb.blueprintGraph.findFirst({
where: { projectId: projectRecord.id, deletedAt: null, OR: [{ id: graphId }, { slug: graphId }, { readableId: graphId }] },
include: { nodes: true },
}) as BlueprintGraphRecord | null;
if (!graph) return undefined;
const node = (graph.nodes ?? []).find((candidate) => candidate.templateId === nodeId || candidate.id === nodeId);
if (!node) return undefined;
const nextStatus = action === "accept" ? "accepted" : "pending";
await blueprintDb.blueprintNode.update({
where: { id: node.id },
data: { status: nextStatus, staleReason: action === "reject" ? "Latest generated draft was rejected in the review queue." : null, updatedBy: "review_queue_sync_v53" },
});
await blueprintDb.blueprintNodeRun.updateMany({
where: { projectId: projectRecord.id, nodeId: node.id, reviewItemId: review.slug ?? review.id, deletedAt: null },
data: { status: action === "accept" ? "accepted" : "failed", updatedBy: "review_queue_sync_v53" },
});
if (node.documentationItemId) {
await prisma.documentationItem.updateMany({
where: { projectId: projectRecord.id, OR: [{ id: node.documentationItemId }, { slug: node.documentationItemId }, { readableId: node.documentationItemId }] },
data: { status: action === "accept" ? "accepted" : "draft_rejected", approvalState: action === "accept" ? ApprovalState.approved : ApprovalState.rejected, updatedBy: "review_queue_sync_v53" },
});
}
let staleNodeIds: string[] = [];
if (action === "accept") {
const plan = recordToPlanDto(graph);
const impact = createBlueprintImpactReportDto(plan, node.templateId);
staleNodeIds = impact.downstreamNodeIds;
if (staleNodeIds.length) {
await blueprintDb.blueprintNode.updateMany({
where: { graphId: graph.id, templateId: { in: staleNodeIds }, status: { in: ["accepted", "needs_review", "pending", "ready"] } },
data: { status: "stale", staleReason: `${node.title} was accepted and may affect this downstream document.`, updatedBy: "staleness_engine_v53" },
});
await blueprintDb.blueprintGraph.update({ where: { id: graph.id }, data: { status: "partially_stale", updatedBy: "staleness_engine_v53" } });
}
}
await prisma.operatingLogEntry.create({
data: {
projectId: projectRecord.id,
slug: `log-${projectRecord.slug}-blueprint-review-sync-${Date.now().toString(36)}`,
readableId: `log-${projectRecord.slug}-blueprint-review-sync-${Date.now().toString(36)}`,
type: "blueprint_review_sync",
title: action === "accept" ? `Blueprint document accepted: ${node.title}` : `Blueprint draft rejected: ${node.title}`,
summary: action === "accept" ? `Accepted review item updated the blueprint node and marked ${staleNodeIds.length} downstream node(s) stale.` : "Rejected review item returned the blueprint node to pending.",
linkedEntityIdsJson: toJson([graph.slug ?? graph.id, node.templateId, review.slug ?? review.id, ...staleNodeIds]),
source: "blueprint_engine_v53",
importance: staleNodeIds.length ? "important" : "normal",
payloadJson: toJson({ graphId: graph.slug ?? graph.id, nodeId: node.templateId, reviewItemId: review.slug ?? review.id, action, staleNodeIds }),
},
});
return { graphId: graph.slug ?? graph.id, nodeId: node.templateId, action, staleNodeIds };
};
export const regenerateBlueprintNode = async (
projectIdOrSlug: string,
graphId: string,
nodeId: string,
options: { createDocumentation?: boolean; sendToReview?: boolean } = {},
) => {
const projectRecord = await findProjectRecord(projectIdOrSlug);
if (!projectRecord) return undefined;
const graph = await blueprintDb.blueprintGraph.findFirst({
where: { projectId: projectRecord.id, deletedAt: null, OR: [{ id: graphId }, { slug: graphId }, { readableId: graphId }] },
include: { project: { select: { slug: true } }, nodes: true },
}) as BlueprintGraphRecord | null;
if (!graph) return undefined;
const node = (graph.nodes ?? []).find((candidate) => candidate.templateId === nodeId || candidate.id === nodeId);
if (!node) return undefined;
const run = await runBlueprintNode(projectIdOrSlug, graph.slug ?? graph.id, node.templateId, {
createDocumentation: options.createDocumentation ?? true,
sendToReview: options.sendToReview ?? true,
});
if (!run) return undefined;
await prisma.operatingLogEntry.create({
data: {
projectId: projectRecord.id,
slug: `log-${projectRecord.slug}-blueprint-node-regenerate-${Date.now().toString(36)}`,
readableId: `log-${projectRecord.slug}-blueprint-node-regenerate-${Date.now().toString(36)}`,
type: "blueprint_node_regenerated",
title: `Blueprint node regenerated: ${node.title}`,
summary: `Regenerated ${node.title} into a review-gated draft for ${node.outputPath}.`,
linkedEntityIdsJson: toJson([graph.slug ?? graph.id, node.templateId, run.reviewItemId, run.documentationItemId].filter(Boolean)),
source: "blueprint_engine_v55",
importance: node.status === "stale" ? "important" : "normal",
payloadJson: toJson({ graphId: graph.slug ?? graph.id, nodeId: node.templateId, previousStatus: node.status, runId: run.id, reviewItemId: run.reviewItemId, documentationItemId: run.documentationItemId }),
},
});
return {
graphId: graph.slug ?? graph.id,
nodeId: node.templateId,
previousStatus: node.status,
run,
message: `Blueprint node '${node.title}' regenerated and sent through review gates.`,
};
};
export const regenerateStaleBlueprintNodes = async (
projectIdOrSlug: string,
graphId: string,
options: { limit?: number; createDocumentation?: boolean; sendToReview?: boolean } = {},
) => {
const projectRecord = await findProjectRecord(projectIdOrSlug);
if (!projectRecord) return undefined;
const graph = await blueprintDb.blueprintGraph.findFirst({
where: { projectId: projectRecord.id, deletedAt: null, OR: [{ id: graphId }, { slug: graphId }, { readableId: graphId }] },
include: { project: { select: { slug: true } }, nodes: true },
}) as BlueprintGraphRecord | null;
if (!graph) return undefined;
const staleNodes = (graph.nodes ?? [])
.filter((node) => node.status === "stale")
.sort((left, right) => left.generationOrder - right.generationOrder)
.slice(0, Math.max(1, options.limit ?? 10));
const runs = [];
for (const node of staleNodes) {
const run = await runBlueprintNode(projectIdOrSlug, graph.slug ?? graph.id, node.templateId, {
createDocumentation: options.createDocumentation ?? true,
sendToReview: options.sendToReview ?? true,
});
if (run) runs.push(run);
}
if (runs.length) {
await blueprintDb.blueprintGraph.update({
where: { id: graph.id },
data: { status: "running", updatedBy: "api" },
});
await prisma.operatingLogEntry.create({
data: {
projectId: projectRecord.id,
slug: `log-${projectRecord.slug}-blueprint-regenerate-${Date.now().toString(36)}`,
readableId: `log-${projectRecord.slug}-blueprint-regenerate-${Date.now().toString(36)}`,
type: "blueprint_stale_nodes_regenerated",
title: "Blueprint stale nodes regenerated",
summary: `${runs.length} stale blueprint node(s) were regenerated into review-gated drafts.`,
linkedEntityIdsJson: toJson([graph.slug ?? graph.id, ...runs.map((run) => run.nodeId)]),
source: "blueprint_engine_v54",
importance: "important",
payloadJson: toJson({ graphId: graph.slug ?? graph.id, regeneratedNodeIds: runs.map((run) => run.nodeId), remainingStaleCount: Math.max(0, staleNodes.length - runs.length) }),
},
});
}
return {
graphId: graph.slug ?? graph.id,
regeneratedCount: runs.length,
regeneratedNodeIds: runs.map((run) => run.nodeId),
createdReviewItemIds: runs.map((run) => run.reviewItemId).filter(Boolean),
createdDocumentationItemIds: runs.map((run) => run.documentationItemId).filter(Boolean),
skippedCount: Math.max(0, staleNodes.length - runs.length),
message: runs.length
? `${runs.length} stale blueprint node(s) regenerated and sent through review gates.`
: "No stale blueprint nodes were available for regeneration.",
};
};
@@ -0,0 +1,518 @@
import type { Prisma } from "@prisma/client";
import { ApprovalState, EntityType, EvidenceStrength, FeatureLifecycle, FeatureStatus, ReviewStatus, VerificationState } from "@prisma/client";
import { markInitiativePlannedFromReview } from "./initiativesRepository.js";
import { prisma } from "../db/prisma.js";
export type BrainstormSessionDto = {
id: string;
projectId: string;
title: string;
prompt: string;
scope: string;
mode: string;
status: string;
targetEntityId?: string;
targetEntityType?: string;
generatedBy: string;
contextSummary: string;
ideas: unknown[];
clusters: unknown[];
outcomes: unknown[];
selectedIdeaIds: string[];
nextActions: string[];
createdAt: string;
updatedAt: string;
};
export type BrainstormConversionTarget = "feature" | "documentation" | "context_pack" | "blueprint_node" | "initiative";
export type BrainstormConversionResultDto = {
success: true;
message: string;
target: BrainstormConversionTarget;
brainstorm: BrainstormSessionDto;
createdEntityId?: string;
reviewItemId?: string;
targetRoute?: string;
affectedEntities: string[];
};
export type BrainstormReviewSyncResultDto = {
applied: boolean;
action: "accept" | "reject";
reviewItemId: string;
brainstormId?: string;
acceptedTruthId?: string;
affectedEntities: string[];
message: string;
};
const toJson = (value: unknown): Prisma.InputJsonValue => JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
const toArray = (value: Prisma.JsonValue | null | undefined): unknown[] => Array.isArray(value) ? value : [];
const toStringArray = (value: Prisma.JsonValue | null | undefined): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
const slugify = (value: string) => value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 56) || "brainstorm";
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;
};
type BrainstormRecord = Prisma.BrainstormSessionGetPayload<{ include: { project: { select: { slug: true } } } }>;
const toDto = (session: BrainstormRecord): BrainstormSessionDto => ({
id: session.id,
projectId: session.project.slug,
title: session.title,
prompt: session.prompt,
scope: session.scope,
mode: session.mode,
status: session.status,
targetEntityId: session.targetEntityId ?? undefined,
targetEntityType: session.targetEntityType ?? undefined,
generatedBy: session.generatedBy,
contextSummary: session.contextSummary,
ideas: toArray(session.ideasJson),
clusters: toArray(session.clustersJson),
outcomes: toArray(session.outcomesJson),
selectedIdeaIds: toStringArray(session.selectedIdeaIdsJson),
nextActions: toStringArray(session.nextActionsJson),
createdAt: session.createdAt.toISOString(),
updatedAt: session.updatedAt.toISOString(),
});
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
const getString = (value: unknown, fallback = ""): string => typeof value === "string" ? value : fallback;
const getNumber = (value: unknown, fallback = 72): number => typeof value === "number" && Number.isFinite(value) ? value : fallback;
const getSelectedIdeas = (session: BrainstormSessionDto) => {
const selected = new Set(session.selectedIdeaIds);
const ideas = session.ideas.filter(isRecord);
const selectedIdeas = ideas.filter((idea) => selected.has(getString(idea.id)));
return selectedIdeas.length ? selectedIdeas : ideas.slice(0, 3);
};
const getTopIdea = (session: BrainstormSessionDto): Record<string, unknown> | undefined =>
[...getSelectedIdeas(session)].sort((left, right) => getNumber(right.score) - getNumber(left.score))[0];
const averageScore = (session: BrainstormSessionDto): number => {
const ideas = session.ideas.filter(isRecord);
if (!ideas.length) return 72;
return Math.round(ideas.reduce((total, idea) => total + getNumber(idea.score), 0) / ideas.length);
};
const toSelectedIdeaMarkdown = (session: BrainstormSessionDto): string => {
const selected = getSelectedIdeas(session);
if (!selected.length) return "- No selected ideas yet.";
return selected.map((idea) => `- **${getString(idea.title, "Idea")}** (${getNumber(idea.score)}/100): ${getString(idea.description, "No description.")}`).join("\n");
};
const markOutcomeConverted = (session: BrainstormSessionDto, target: BrainstormConversionTarget, targetRoute?: string) => {
const outcomeTypeByTarget: Record<BrainstormConversionTarget, string[]> = {
feature: ["feature_candidate", "implementation_slice"],
documentation: ["documentation_candidate"],
context_pack: ["context_pack_seed"],
blueprint_node: ["documentation_candidate", "implementation_slice"],
initiative: ["initiative_candidate", "implementation_slice"],
};
const targetTypes = outcomeTypeByTarget[target];
return session.outcomes.map((outcome) => {
if (!isRecord(outcome)) return outcome;
const type = getString(outcome.type);
if (!targetTypes.includes(type)) return outcome;
return { ...outcome, conversionStatus: "converted", targetRoute };
});
};
const findSession = async (projectId: string, brainstormId: string) =>
prisma.brainstormSession.findFirst({
where: { projectId, deletedAt: null, OR: [{ id: brainstormId }, { slug: brainstormId }, { readableId: brainstormId }] },
include: { project: { select: { slug: true } } },
});
export const listBrainstormsForProject = async (projectIdOrSlug: string): Promise<BrainstormSessionDto[]> => {
const project = await getProject(projectIdOrSlug);
const sessions = await prisma.brainstormSession.findMany({
where: { projectId: project.id, deletedAt: null },
include: { project: { select: { slug: true }}},
orderBy: { updatedAt: "desc" },
});
return sessions.map(toDto);
};
export const createBrainstormForProject = async (projectIdOrSlug: string, input: {
title: string;
prompt: string;
scope: string;
mode: string;
status?: string;
targetEntityId?: string;
targetEntityType?: string;
generatedBy?: string;
contextSummary?: string;
ideas?: unknown[];
clusters?: unknown[];
outcomes?: unknown[];
selectedIdeaIds?: string[];
nextActions?: string[];
}): Promise<BrainstormSessionDto> => {
const project = await getProject(projectIdOrSlug);
const slugBase = slugify(input.title);
const session = await prisma.brainstormSession.create({
data: {
projectId: project.id,
slug: `${slugBase}-${Date.now().toString(36)}`,
title: input.title,
prompt: input.prompt,
scope: input.scope,
mode: input.mode,
status: input.status ?? "exploring",
targetEntityId: input.targetEntityId,
targetEntityType: input.targetEntityType,
generatedBy: input.generatedBy ?? "api",
contextSummary: input.contextSummary ?? `${project.name} • ${input.scope}/${input.mode}`,
ideasJson: toJson(input.ideas ?? []),
clustersJson: toJson(input.clusters ?? []),
outcomesJson: toJson(input.outcomes ?? []),
selectedIdeaIdsJson: toJson(input.selectedIdeaIds ?? []),
nextActionsJson: toJson(input.nextActions ?? []),
metadataJson: toJson({ reviewBeforeTruth: true }),
},
include: { project: { select: { slug: true }}},
});
return toDto(session);
};
export const updateBrainstormForProject = async (projectIdOrSlug: string, brainstormId: string, input: Partial<{
title: string;
prompt: string;
scope: string;
mode: string;
status: string;
targetEntityId: string;
targetEntityType: string;
ideas: unknown[];
clusters: unknown[];
outcomes: unknown[];
selectedIdeaIds: string[];
nextActions: string[];
}>): Promise<BrainstormSessionDto | undefined> => {
const project = await getProject(projectIdOrSlug);
const existing = await prisma.brainstormSession.findFirst({ where: { projectId: project.id, OR: [{ id: brainstormId }, { slug: brainstormId }, { readableId: brainstormId }], deletedAt: null }});
if (!existing) return undefined;
const session = await prisma.brainstormSession.update({
where: { id: existing.id },
data: {
title: input.title,
prompt: input.prompt,
scope: input.scope,
mode: input.mode,
status: input.status,
targetEntityId: input.targetEntityId,
targetEntityType: input.targetEntityType,
ideasJson: input.ideas ? toJson(input.ideas) : undefined,
clustersJson: input.clusters ? toJson(input.clusters) : undefined,
outcomesJson: input.outcomes ? toJson(input.outcomes) : undefined,
selectedIdeaIdsJson: input.selectedIdeaIds ? toJson(input.selectedIdeaIds) : undefined,
nextActionsJson: input.nextActions ? toJson(input.nextActions) : undefined,
},
include: { project: { select: { slug: true }}},
});
return toDto(session);
};
const createReviewForConversion = async (projectId: string, projectSlug: string, session: BrainstormSessionDto, target: BrainstormConversionTarget, createdEntityId?: string, targetRoute?: string) => {
const now = new Date();
const readableTarget = target.replace("_", " ");
const slug = `rq-brainstorm-${slugify(session.title)}-${target}-${now.getTime()}`;
const record = await prisma.reviewQueueItem.create({
data: {
projectId,
slug,
readableId: slug,
type: target === "feature" ? "feature" : target === "documentation" ? "documentation_update" : target === "context_pack" ? "knowledge" : target === "initiative" ? "initiative" : "documentation_update",
title: `Review brainstorm conversion: ${session.title}`,
description: `Brainstorm CPU proposes converting '${session.title}' into a ${readableTarget}.\n\nSelected ideas:\n${toSelectedIdeaMarkdown(session)}`,
priority: target === "feature" ? "important" : "normal",
status: ReviewStatus.pending,
source: "brainstorm_cpu",
confidence: averageScore(session),
approvalState: ApprovalState.pending,
evidenceStrength: EvidenceStrength.medium,
verificationState: VerificationState.needs_evidence,
suggestedAction: `Review the brainstorm output before accepting the ${readableTarget} as project truth.`,
createdBy: "api",
updatedBy: "api",
sourceType: "brainstorm_cpu_conversion",
createdEntityType: target === "feature" ? EntityType.feature : target === "documentation" ? EntityType.documentation_item : target === "context_pack" ? EntityType.context_pack : target === "initiative" ? EntityType.initiative : undefined,
createdEntityId,
targetEntityType: undefined,
targetEntityId: session.id,
payloadJson: toJson({ brainstormId: session.id, conversionTarget: target, createdEntityId, targetRoute, selectedIdeaIds: session.selectedIdeaIds }),
provenanceJson: toJson({ createdFrom: "Brainstorm CPU conversion", projectSlug }),
},
});
return record.slug ?? record.id;
};
export const convertBrainstormForProject = async (
projectIdOrSlug: string,
brainstormId: string,
target: BrainstormConversionTarget,
): Promise<BrainstormConversionResultDto | undefined> => {
const project = await getProject(projectIdOrSlug);
const record = await findSession(project.id, brainstormId);
if (!record) return undefined;
const session = toDto(record);
const topIdea = getTopIdea(session);
const score = averageScore(session);
const suffix = `${slugify(session.title)}-${Date.now().toString(36)}`;
let createdEntityId: string | undefined;
let targetRoute: string | undefined;
if (target === "feature") {
const group = await prisma.featureGroup.upsert({
where: { projectId_slug: { projectId: project.id, slug: "brainstorm-lab" } },
create: { projectId: project.id, slug: "brainstorm-lab", readableId: "brainstorm-lab", name: "Brainstorm Lab", description: "Feature candidates converted from Brainstorm CPU sessions.", focusArea: "Exploration" },
update: {},
});
const feature = await prisma.feature.create({
data: {
projectId: project.id,
featureGroupId: group.id,
slug: `brainstorm-feature-${suffix}`,
readableId: `brainstorm-feature-${suffix}`,
name: session.title,
description: getString(topIdea?.description, session.prompt),
status: FeatureStatus.todo,
lifecycle: FeatureLifecycle.feature_candidate,
intent: `Converted from Brainstorm CPU session ${session.id}. Review before implementation.`,
confidence: score,
health: 70,
approvalState: ApprovalState.pending,
evidenceStrength: EvidenceStrength.medium,
verificationState: VerificationState.needs_evidence,
trustJson: toJson({ source: "brainstorm_cpu", confidence: score, brainstormId: session.id }),
provenanceJson: toJson({ sourceBrainstormId: session.id, selectedIdeaIds: session.selectedIdeaIds }),
sourceType: "brainstorm_cpu_conversion",
},
});
createdEntityId = feature.slug ?? feature.id;
targetRoute = `/projects/${project.slug}/features/${feature.slug ?? feature.id}`;
}
if (target === "documentation") {
const doc = await prisma.documentationItem.create({
data: {
projectId: project.id,
slug: `brainstorm-doc-${suffix}`,
readableId: `brainstorm-doc-${suffix}`,
title: `Brainstorm documentation seed: ${session.title}`,
kind: "brainstorm_seed",
status: "draft",
approvalState: ApprovalState.pending,
content: `# ${session.title}\n\n${session.prompt}\n\n## Selected ideas\n${toSelectedIdeaMarkdown(session)}\n`,
metadataJson: toJson({ brainstormId: session.id, selectedIdeaIds: session.selectedIdeaIds }),
provenanceJson: toJson({ source: "brainstorm_cpu_conversion" }),
sourceType: "brainstorm_cpu_conversion",
},
});
createdEntityId = doc.slug ?? doc.id;
targetRoute = `/projects/${project.slug}/documentation`;
}
if (target === "context_pack") {
const pack = await prisma.contextPack.create({
data: {
projectId: project.id,
slug: `brainstorm-context-${suffix}`,
readableId: `brainstorm-context-${suffix}`,
title: `Brainstorm context: ${session.title}`,
category: "brainstorm",
summary: `Reusable context generated from Brainstorm CPU session '${session.title}'.`,
generatedBy: "brainstorm_cpu",
confidence: score,
approvalState: ApprovalState.pending,
qualityJson: toJson({ coverage: score, reviewBeforeTruth: true }),
trustJson: toJson({ source: "brainstorm_cpu", brainstormId: session.id }),
provenanceJson: toJson({ selectedIdeaIds: session.selectedIdeaIds }),
sourceType: "brainstorm_cpu_conversion",
sections: {
create: [
{ projectId: project.id, slug: "selected-ideas", title: "Selected ideas", sectionType: "signals", orderIndex: 1, contentJson: toJson(getSelectedIdeas(session)) },
{ projectId: project.id, slug: "outcomes", title: "Outcome candidates", sectionType: "outcomes", orderIndex: 2, contentJson: toJson(session.outcomes) },
{ projectId: project.id, slug: "next-actions", title: "Next actions", sectionType: "actions", orderIndex: 3, contentJson: toJson(session.nextActions) },
],
},
},
});
createdEntityId = pack.slug ?? pack.id;
targetRoute = `/projects/${project.slug}/context-packs`;
}
if (target === "initiative") {
const initiative = await prisma.initiative.create({
data: {
projectId: project.id,
slug: `brainstorm-initiative-${suffix}`,
readableId: `brainstorm-initiative-${suffix}`,
title: `Brainstorm initiative: ${session.title}`,
intent: `${session.prompt}\n\nSelected ideas:\n${toSelectedIdeaMarkdown(session)}`,
status: "proposed",
priority: getSelectedIdeas(session).some((idea) => getString(idea.impact) === "high") ? "important" : "normal",
confidence: score,
sourceBrainstormId: session.id,
featureIdsJson: toJson([]),
approvalState: ApprovalState.pending,
metadataJson: toJson({ sourceBrainstormId: session.id, selectedIdeaIds: session.selectedIdeaIds, reviewBeforeTruth: true }),
provenanceJson: toJson({ source: "brainstorm_cpu_conversion" }),
},
});
createdEntityId = initiative.slug ?? initiative.id;
targetRoute = `/projects/${project.slug}/initiatives`;
}
if (target === "blueprint_node") {
createdEntityId = `blueprint-seed-${suffix}`;
targetRoute = `/projects/${project.slug}/blueprint?brainstormId=${session.id}`;
}
const reviewItemId = await createReviewForConversion(project.id, project.slug, session, target, createdEntityId, targetRoute);
const updatedSession = await prisma.brainstormSession.update({
where: { id: record.id },
data: {
status: "converted",
outcomesJson: toJson(markOutcomeConverted(session, target, targetRoute)),
metadataJson: toJson({ reviewBeforeTruth: true, lastConversionTarget: target, lastConvertedEntityId: createdEntityId, lastReviewItemId: reviewItemId }),
},
include: { project: { select: { slug: true }}},
});
await prisma.operatingLogEntry.create({
data: {
projectId: project.id,
slug: `log-brainstorm-conversion-${suffix}`,
readableId: `log-brainstorm-conversion-${suffix}`,
type: "brainstorm conversion",
title: `Brainstorm converted: ${session.title}`,
summary: `Brainstorm CPU converted selected ideas into ${target.replace("_", " ")} and queued review before truth.`,
linkedEntityIdsJson: toJson([session.id, reviewItemId, createdEntityId].filter(Boolean)),
source: "brainstorm_cpu",
importance: target === "feature" ? "important" : "normal",
payloadJson: toJson({ target, targetRoute }),
provenanceJson: toJson({ sourceBrainstormId: session.id }),
},
});
return {
success: true,
message: `Brainstorm '${session.title}' converted to ${target.replace("_", " ")} candidate and queued for review.`,
target,
brainstorm: toDto(updatedSession),
createdEntityId,
reviewItemId,
targetRoute,
affectedEntities: [session.id, reviewItemId, createdEntityId].filter((item): item is string => Boolean(item)),
};
};
export const syncBrainstormReviewAction = async (
projectIdOrSlug: string,
reviewItemId: string,
action: "accept" | "reject",
): Promise<BrainstormReviewSyncResultDto | undefined> => {
const project = await getProject(projectIdOrSlug);
const review = await prisma.reviewQueueItem.findFirst({
where: { projectId: project.id, deletedAt: null, OR: [{ id: reviewItemId }, { slug: reviewItemId }, { readableId: reviewItemId }] },
});
if (!review) return undefined;
if (review.source !== "brainstorm_cpu" && review.sourceType !== "brainstorm_cpu_conversion") return undefined;
const payload = isRecord(review.payloadJson) ? review.payloadJson : {};
const brainstormId = getString(payload.brainstormId, review.targetEntityType === EntityType.brainstorm_session ? review.targetEntityId ?? "" : "");
const affectedEntities = [review.slug ?? review.id];
if (brainstormId) {
await prisma.brainstormSession.updateMany({
where: { projectId: project.id, OR: [{ id: brainstormId }, { slug: brainstormId }, { readableId: brainstormId }] },
data: { status: action === "accept" ? "converted" : "shortlisted" },
});
affectedEntities.push(brainstormId);
}
if (action === "accept" && getString(payload.conversionTarget) === "initiative" && review.createdEntityId) {
const plannedInitiativeId = await markInitiativePlannedFromReview(project.id, review.createdEntityId, review.slug ?? review.id);
if (plannedInitiativeId) affectedEntities.push(plannedInitiativeId);
}
let acceptedTruthId: string | undefined;
if (action === "accept") {
const existing = await prisma.acceptedTruthItem.findFirst({ where: { projectId: project.id, sourceReviewItemId: review.id, deletedAt: null } });
if (existing) acceptedTruthId = existing.slug ?? existing.id;
else {
const now = new Date();
const slug = `truth-brainstorm-${slugify(review.title)}-${now.getTime()}`;
const truth = await prisma.acceptedTruthItem.create({
data: {
projectId: project.id,
slug,
readableId: slug,
sourceReviewItemId: review.id,
kind: review.type === "documentation_update" ? "documentation" : review.type,
title: review.title,
summary: review.description,
status: "accepted",
source: "brainstorm_cpu",
confidence: review.confidence,
approvalState: ApprovalState.approved,
evidenceStrength: review.evidenceStrength,
verificationState: VerificationState.verified,
payloadJson: toJson({
reviewItemId: review.id,
brainstormId,
conversionTarget: payload.conversionTarget,
createdEntityId: payload.createdEntityId,
targetRoute: payload.targetRoute,
candidateSummary: review.description,
nextAction: action === "accept" ? "Use this accepted brainstorm truth in context packs, roadmap, blueprint docs, or implementation planning." : undefined,
}),
provenanceJson: toJson({
createdFrom: "Brainstorm CPU review acceptance",
reviewSource: review.source,
reviewSourceType: review.sourceType,
reviewTargetEntityType: review.targetEntityType,
reviewTargetEntityId: review.targetEntityId,
}),
verifiedAt: now,
verifiedBy: "api",
},
});
acceptedTruthId = truth.slug ?? truth.id;
}
if (acceptedTruthId) affectedEntities.push(acceptedTruthId);
}
await prisma.operatingLogEntry.create({
data: {
projectId: project.id,
slug: `log-brainstorm-review-${slugify(review.title)}-${Date.now()}`,
readableId: `log-brainstorm-review-${slugify(review.title)}-${Date.now()}`,
type: "brainstorm review sync",
title: `Brainstorm review ${action === "accept" ? "accepted" : "rejected"}`,
summary: action === "accept" ? "Accepted brainstorm conversion was materialized as accepted truth." : "Rejected brainstorm conversion was returned to shortlist state.",
linkedEntityIdsJson: toJson(affectedEntities),
source: "brainstorm_cpu",
importance: action === "accept" ? "important" : "normal",
payloadJson: toJson({ action, brainstormId, acceptedTruthId }),
provenanceJson: toJson({ reviewItemId: review.id }),
},
});
return {
applied: true,
action,
reviewItemId: review.slug ?? review.id,
brainstormId,
acceptedTruthId,
affectedEntities,
message: action === "accept" ? "Brainstorm review accepted and materialized as accepted truth." : "Brainstorm review rejected and brainstorm returned to shortlist state.",
};
};
@@ -0,0 +1,235 @@
import type { Prisma } from "@prisma/client";
import { prisma } from "../db/prisma.js";
import type {
CaptureClassificationDto,
CaptureClassificationType,
CaptureItemDto,
CaptureItemListFilters,
CaptureItemStatus,
CaptureItemType,
CreateCaptureItemRequestDto,
} from "../types/api.js";
type CaptureRecord = Prisma.CaptureItemGetPayload<Record<string, never>>;
type ProjectRecord = { id: string; slug: string };
const toIso = (value: Date): string => value.toISOString();
const findProjectRecord = (projectIdOrSlug: string): Promise<ProjectRecord | null> =>
prisma.project.findFirst({
where: {
deletedAt: null,
OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }],
},
select: { id: true, slug: true },
});
const toCaptureDto = (
record: CaptureRecord,
projectSlug: string,
): CaptureItemDto => ({
id: record.slug ?? record.readableId ?? record.id,
projectId: projectSlug,
type: record.type as CaptureItemType,
title: record.title,
content: record.content,
source: record.source,
status: record.status as CaptureItemStatus,
...(record.classificationType
? { classificationType: record.classificationType as CaptureClassificationType }
: {}),
...(record.confidence !== null ? { confidence: record.confidence } : {}),
...(record.evidenceStrength ? { evidenceStrength: record.evidenceStrength } : {}),
...(record.suggestedAction ? { suggestedAction: record.suggestedAction } : {}),
createdAt: toIso(record.createdAt),
updatedAt: toIso(record.updatedAt),
...(record.linkedReviewItemId ? { linkedReviewItemId: record.linkedReviewItemId } : {}),
});
const splitSearch = (value: string | undefined): string | undefined =>
value?.trim() || undefined;
const toWhere = (
projectId: string,
filters: CaptureItemListFilters = {},
): Prisma.CaptureItemWhereInput => ({
projectId,
deletedAt: null,
...(filters.status?.length ? { status: { in: filters.status } } : {}),
...(filters.type?.length ? { type: { in: filters.type } } : {}),
...(filters.source?.length ? { source: { in: filters.source } } : {}),
...(filters.confidence !== undefined ? { confidence: filters.confidence } : {}),
...(filters.classificationType?.length
? { classificationType: { in: filters.classificationType } }
: {}),
});
const classifyContent = (
item: CaptureItemDto,
): Omit<CaptureClassificationDto, "captureItemId"> => {
const text = `${item.type} ${item.content}`.toLowerCase();
if (text.includes("bug") || text.includes("error")) {
return {
classificationType: "bug",
confidence: 86,
evidenceStrength: "medium",
suggestedAction: "Send to review as bug candidate.",
};
}
if (text.includes("task") || text.includes("todo")) {
return {
classificationType: "task",
confidence: 80,
evidenceStrength: "medium",
suggestedAction: "Send to review as task candidate.",
};
}
if (text.includes("decision")) {
return {
classificationType: "decision",
confidence: 82,
evidenceStrength: "medium",
suggestedAction: "Send to review as decision candidate.",
};
}
if (text.includes("rule") || text.includes("must") || text.includes("do not")) {
return {
classificationType: "memory_rule",
confidence: 90,
evidenceStrength: "strong",
suggestedAction: "Send to review as memory rule.",
};
}
if (text.includes("docs") || text.includes("documentation")) {
return {
classificationType: "documentation_update",
confidence: 78,
evidenceStrength: "weak",
suggestedAction: "Send to review as documentation update.",
};
}
return {
classificationType: "knowledge",
confidence: 72,
evidenceStrength: "weak",
suggestedAction: "Send to review as knowledge item.",
};
};
export const listCaptureItemsFromDatabase = async (
projectIdOrSlug: string,
filters: CaptureItemListFilters,
options: { page: number; pageSize: number; cursor?: string; sort?: string },
) => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const where = toWhere(project.id, filters);
const total = await prisma.captureItem.count({ where });
const records = await prisma.captureItem.findMany({
where,
orderBy: { createdAt: options.sort?.endsWith(":asc") ? "asc" : "desc" },
skip: (options.page - 1) * options.pageSize,
take: options.pageSize,
});
const pageCount = Math.ceil(total / options.pageSize);
return {
items: records.map((record) => toCaptureDto(record, project.slug)),
total,
page: options.page,
pageSize: options.pageSize,
hasNextPage: options.page < pageCount,
hasPreviousPage: options.page > 1,
...(options.sort ? { sort: options.sort } : {}),
...(options.cursor ? { nextCursor: undefined, cursor: options.cursor } : {}),
filters,
};
};
export const getCaptureItemFromDatabase = async (
projectIdOrSlug: string,
captureItemId: string,
): Promise<CaptureItemDto | undefined> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const record = await prisma.captureItem.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: captureItemId }, { slug: captureItemId }, { readableId: captureItemId }],
},
});
return record ? toCaptureDto(record, project.slug) : undefined;
};
export const createCaptureItemInDatabase = async (
projectIdOrSlug: string,
body: CreateCaptureItemRequestDto,
): Promise<CaptureItemDto | undefined> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const createdAt = new Date();
const title = body.content.trim().slice(0, 64) || "Captured item";
const slug = `cap-${project.slug}-${createdAt.getTime()}`;
const record = await prisma.captureItem.create({
data: {
projectId: project.id,
slug,
readableId: slug,
type: body.type,
title,
content: body.content.trim(),
source: body.source?.trim() || "manual",
status: "captured",
sourceType: "runtime_api",
createdBy: "api",
updatedBy: "api",
createdAt,
metadataJson: {},
provenanceJson: { createdFrom: "P5 capture API" },
},
});
return toCaptureDto(record, project.slug);
};
export const classifyCaptureItemInDatabase = async (
projectIdOrSlug: string,
captureItemId: string,
): Promise<CaptureClassificationDto | undefined | "CAPTURE_ALREADY_IN_REVIEW"> => {
const item = await getCaptureItemFromDatabase(projectIdOrSlug, captureItemId);
if (!item) return undefined;
if (item.status === "in_review") return "CAPTURE_ALREADY_IN_REVIEW";
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const classification = classifyContent(item);
const update = await prisma.captureItem.updateMany({
where: {
projectId: project.id,
deletedAt: null,
status: { in: ["captured", "classified"] },
linkedReviewItemId: null,
OR: [{ id: captureItemId }, { slug: captureItemId }, { readableId: captureItemId }],
},
data: {
status: "classified",
classificationType: classification.classificationType,
confidence: classification.confidence,
evidenceStrength: classification.evidenceStrength,
suggestedAction: classification.suggestedAction,
updatedBy: "api",
},
});
if (update.count !== 1) return "CAPTURE_ALREADY_IN_REVIEW";
return {
captureItemId,
...classification,
};
};
@@ -0,0 +1,367 @@
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,
};
};
@@ -0,0 +1,119 @@
import type { Prisma } from "@prisma/client";
import { prisma } from "../db/prisma.js";
import type { ApprovalState, EvidenceDto, EvidenceType } from "../types/api.js";
const toPublicId = (value: { slug: string | null; readableId: string | null; id: string }) =>
value.slug ?? value.readableId ?? value.id;
type EvidenceMetadata = {
linkedEntity?: EvidenceDto["linkedEntity"];
featureIds?: string[];
route?: string;
};
type DbEvidenceRecord = {
id: string;
projectId: string;
slug: string | null;
readableId: string | null;
type: string;
source: string;
confidence: number;
approvalState: string;
reason: string;
metadataJson: Prisma.JsonValue | null;
lastSeen: Date;
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;
const toEvidenceMetadata = (value: Prisma.JsonValue | null): EvidenceMetadata => {
if (!isRecord(value)) return {};
return value as EvidenceMetadata;
};
const findProjectRecordBySlugOrId = (projectIdOrSlug: string) =>
prisma.project.findFirst({
where: {
deletedAt: null,
OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }],
},
});
const toEvidenceDto = (
evidence: DbEvidenceRecord,
projectSlug: string,
): EvidenceDto => {
const metadata = toEvidenceMetadata(evidence.metadataJson);
return {
id: toPublicId(evidence),
projectId: projectSlug,
type: evidence.type as EvidenceType,
linkedEntity: metadata.linkedEntity ?? {
type: "feature",
id: "unknown",
label: "Unknown linked entity",
},
featureIds: metadata.featureIds ?? [],
source: evidence.source,
confidence: evidence.confidence,
reason: evidence.reason,
lastSeen: evidence.lastSeen.toISOString(),
approvalState: evidence.approvalState as ApprovalState,
route: metadata.route,
};
};
export const listEvidenceForProject = async (
projectIdOrSlug: string,
): Promise<EvidenceDto[] | undefined> => {
const project = await findProjectRecordBySlugOrId(projectIdOrSlug);
if (!project) return undefined;
const evidence = await prisma.evidence.findMany({
where: {
projectId: project.id,
deletedAt: null,
},
orderBy: [{ lastSeen: "desc" }],
});
return evidence.map((item) => toEvidenceDto(item, project.slug));
};
export const getEvidenceBySlugOrId = async (
projectIdOrSlug: string,
evidenceIdOrSlug: string,
): Promise<EvidenceDto | undefined> => {
const project = await findProjectRecordBySlugOrId(projectIdOrSlug);
if (!project) return undefined;
const evidence = await prisma.evidence.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: evidenceIdOrSlug }, { slug: evidenceIdOrSlug }, { readableId: evidenceIdOrSlug }],
},
});
if (!evidence) return undefined;
return toEvidenceDto(evidence, project.slug);
};
export const listEvidenceForFeature = async (
projectIdOrSlug: string,
featureIdOrSlug: string,
): Promise<EvidenceDto[] | undefined> => {
const evidence = await listEvidenceForProject(projectIdOrSlug);
if (!evidence) return undefined;
return evidence.filter((item) => item.featureIds.includes(featureIdOrSlug));
};
export const projectExists = async (projectIdOrSlug: string): Promise<boolean> => {
const project = await findProjectRecordBySlugOrId(projectIdOrSlug);
return Boolean(project);
};
@@ -0,0 +1,360 @@
import {
ApprovalState,
DeliveryChangeOperation,
DeliveryStatus,
DeliveryValidationStatus,
type Prisma,
} from "@prisma/client";
import { randomUUID } from "node:crypto";
import { prisma } from "../db/prisma.js";
import { createInitiativeForProject } from "./initiativesRepository.js";
export type FeatureDeliveryStatus =
| "draft" | "awaiting_approval" | "approved" | "executing" | "validating"
| "pull_request_open" | "merged" | "verified" | "released" | "blocked"
| "failed" | "cancelled" | "superseded";
export type FeatureDeliveryRiskLevel = "low" | "medium" | "high" | "destructive";
export type FeatureValidationResult = { command: string; status: "passed" | "failed" | "not_run"; summary?: string };
export type FeatureValidationEvidence = {
provider: "gitea_actions"; runId: number; runNumber: number; url: string; workflowPath: string;
headSha: string; conclusion: "success"; verifiedAt: string;
};
export type FeatureDeliverySpec = {
version: number; problem: string; desiredOutcome: string; users: string[]; goals: string[]; nonGoals: string[];
acceptanceCriteria: string[]; implementationPlan: string[]; risks: string[]; validationCommands: string[];
status: FeatureDeliveryStatus; branch?: string; repositoryId?: string; changedFiles: string[]; progressNotes: string[];
reviewFeedback?: string;
validationResults: FeatureValidationResult[]; validationEvidence?: FeatureValidationEvidence;
pullRequest?: { number: number; url: string; title: string; state: string; mergedAt?: string; mergeCommitSha?: string };
updatedBy: string; updatedAt: string;
};
export type FeatureDeliveryDto = {
id: string; projectId: string; title: string; intent: string; priority: "critical" | "important" | "normal" | "low";
approvalState: string; spec: FeatureDeliverySpec; createdAt: string; updatedAt: string;
};
export type FeatureDeliveryInput = {
title: string; intent: string; priority?: FeatureDeliveryDto["priority"]; problem: string; desiredOutcome: string;
users?: string[]; goals: string[]; nonGoals?: string[]; acceptanceCriteria: string[]; implementationPlan: string[];
risks?: string[]; validationCommands: string[]; riskLevel?: FeatureDeliveryRiskLevel;
};
export type DeliveryChangeInput = {
path: string; operation: "create" | "update" | "delete" | "rename"; previousPath?: string;
beforeSha?: string; afterSha?: string; commitSha?: string;
};
const deliveryTransitions: Record<FeatureDeliveryStatus, readonly FeatureDeliveryStatus[]> = {
draft: ["awaiting_approval", "approved", "cancelled"],
awaiting_approval: ["awaiting_approval", "approved", "cancelled"],
approved: ["approved", "executing", "awaiting_approval", "cancelled"],
executing: ["executing", "validating", "blocked", "failed", "cancelled"],
validating: ["validating", "pull_request_open", "blocked", "failed", "cancelled"],
pull_request_open: ["pull_request_open", "merged", "failed", "blocked", "cancelled"],
merged: ["merged", "verified", "superseded"],
verified: ["verified", "released", "superseded"],
released: ["released", "superseded"],
blocked: ["blocked", "approved", "cancelled"],
failed: ["failed", "approved", "cancelled"],
cancelled: ["cancelled"],
superseded: ["superseded"],
};
export const canTransitionFeatureDeliveryStatus = (from: FeatureDeliveryStatus, to: FeatureDeliveryStatus) => deliveryTransitions[from]?.includes(to) ?? false;
export const assertFeatureDeliveryTransition = (from: FeatureDeliveryStatus, to: FeatureDeliveryStatus) => {
if (!canTransitionFeatureDeliveryStatus(from, to)) throw new Error("FEATURE_DELIVERY_INVALID_TRANSITION");
};
const toJson = (value: unknown): Prisma.InputJsonValue => JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
const strings = (value: unknown): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
const publicId = (value: { id: string; slug?: string | null }) => value.slug ?? value.id;
const toPriority = (value: string): FeatureDeliveryDto["priority"] =>
(["critical", "important", "normal", "low"].includes(value) ? value : "normal") as FeatureDeliveryDto["priority"];
const validationStatus = (status: DeliveryValidationStatus): FeatureValidationResult["status"] =>
status === DeliveryValidationStatus.passed ? "passed" : status === DeliveryValidationStatus.failed ? "failed" : "not_run";
export const actionValidationCommand = (workflowPath: string) => `Gitea Actions: ${workflowPath}`;
export const reportedValidationResult = (entry: { command: string; status: DeliveryValidationStatus; provider: string | null; summary: string | null }): FeatureValidationResult => {
// Old syncs copied one workflow result onto every planned command, including
// native PR and live-provider checks that the workflow had never executed.
const inferredLegacyResult = entry.provider === "gitea_actions" && entry.status === DeliveryValidationStatus.passed && entry.command !== actionValidationCommand(entry.summary ?? "");
return { command: entry.command, status: inferredLegacyResult ? "not_run" : validationStatus(entry.status),
...(inferredLegacyResult ? { summary: `Legacy inferred result; verify this check independently. Observed workflow: ${entry.summary ?? "unknown"}` } : entry.summary ? { summary: entry.summary } : {}) };
};
const hasStatus = (status: DeliveryStatus, allowed: DeliveryStatus[]) => allowed.includes(status);
const normalizedRiskLevel = (value: FeatureDeliveryRiskLevel | undefined): FeatureDeliveryRiskLevel => value ?? "medium";
const requiresHumanDecision = (riskLevel: FeatureDeliveryRiskLevel) => riskLevel === "high" || riskLevel === "destructive";
const authorizationMode = (riskLevel: FeatureDeliveryRiskLevel) => riskLevel === "low" ? "auto_allowed" : "auto_allowed_with_audit";
const getProject = async (projectIdOrSlug: string) => {
const project = await prisma.project.findFirst({ where: { deletedAt: null, OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }, { readableId: projectIdOrSlug }] } });
if (!project) throw new Error("PROJECT_NOT_FOUND");
return project;
};
const planInclude = {
initiative: true,
runs: { orderBy: { createdAt: "desc" as const }, include: {
repositoryLink: true,
changes: { orderBy: { createdAt: "asc" as const } },
validations: { orderBy: { createdAt: "asc" as const } },
} },
} satisfies Prisma.DeliveryPlanInclude;
type PlanWithRuntime = Prisma.DeliveryPlanGetPayload<{ include: typeof planInclude }>;
const toDto = (projectSlug: string, plan: PlanWithRuntime): FeatureDeliveryDto => {
const run = plan.runs[0];
const actionValidation = run?.validations.find((entry) => entry.provider === "gitea_actions" && entry.status === DeliveryValidationStatus.passed && entry.headSha);
const validationEvidence = actionValidation?.externalRunId && actionValidation.externalUrl && actionValidation.headSha && actionValidation.verifiedAt
? { provider: "gitea_actions" as const, runId: Number(actionValidation.externalRunId), runNumber: Number(actionValidation.checkName?.replace(/^run-/, "") ?? actionValidation.externalRunId), url: actionValidation.externalUrl, workflowPath: actionValidation.summary ?? "gitea-actions", headSha: actionValidation.headSha, conclusion: "success" as const, verifiedAt: actionValidation.verifiedAt.toISOString() }
: undefined;
const pullRequest = run?.pullRequestNumber && run.pullRequestUrl && run.pullRequestTitle
? { number: run.pullRequestNumber, url: run.pullRequestUrl, title: run.pullRequestTitle, state: run.pullRequestState ?? "open", ...(run.mergedAt ? { mergedAt: run.mergedAt.toISOString() } : {}), ...(run.mergeCommitSha ? { mergeCommitSha: run.mergeCommitSha } : {}) }
: undefined;
return {
id: publicId(plan), projectId: projectSlug, title: plan.title, intent: plan.intent, priority: toPriority(plan.priority), approvalState: plan.approvalState,
spec: {
version: plan.version, problem: plan.problem, desiredOutcome: plan.desiredOutcome, users: strings(plan.usersJson), goals: strings(plan.goalsJson),
nonGoals: strings(plan.nonGoalsJson), acceptanceCriteria: strings(plan.acceptanceCriteriaJson), implementationPlan: strings(plan.implementationPlanJson),
risks: strings(plan.risksJson), validationCommands: strings(plan.validationCommandsJson), status: plan.status, branch: run?.branch,
repositoryId: run?.repositoryLinkId, changedFiles: Array.from(new Set(run?.changes.map((change) => change.path) ?? [])),
progressNotes: run?.changes.map((change) => `${change.operation}: ${change.previousPath ? `${change.previousPath} -> ` : ""}${change.path}`) ?? [],
validationResults: run?.validations.map(reportedValidationResult) ?? [],
...(plan.reviewFeedback ? { reviewFeedback: plan.reviewFeedback } : {}),
validationEvidence, pullRequest, updatedBy: plan.updatedBy ?? plan.createdBy, updatedAt: plan.updatedAt.toISOString(),
},
createdAt: plan.createdAt.toISOString(), updatedAt: plan.updatedAt.toISOString(),
};
};
const getDeliveryRow = async (projectIdOrSlug: string, deliveryId: string) => {
const project = await getProject(projectIdOrSlug);
const plan = await prisma.deliveryPlan.findFirst({ where: { projectId: project.id, OR: [{ id: deliveryId }, { slug: deliveryId }] }, include: planInclude });
if (!plan) throw new Error("FEATURE_DELIVERY_NOT_FOUND");
return { project, plan };
};
const emit = async (tx: Prisma.TransactionClient, projectId: string, aggregateId: string, eventType: string, payload: unknown) => {
await tx.outboxEvent.create({ data: { eventId: randomUUID(), projectId, aggregateType: "delivery", aggregateId, eventType, payloadJson: toJson(payload) } });
};
export const assertFeatureDeliveryPlanComplete = (spec: Pick<FeatureDeliverySpec, "problem" | "desiredOutcome" | "goals" | "acceptanceCriteria" | "implementationPlan" | "validationCommands">) => {
if (!spec.problem.trim() || !spec.desiredOutcome.trim() || !spec.goals.length || !spec.acceptanceCriteria.length || !spec.implementationPlan.length || !spec.validationCommands.length) throw new Error("FEATURE_DELIVERY_PLAN_INCOMPLETE");
};
export const assertFeatureDeliveryActionEvidence = (evidence: FeatureValidationEvidence | undefined, headSha: string) => {
if (!evidence || evidence.provider !== "gitea_actions" || evidence.conclusion !== "success" || evidence.headSha !== headSha) throw new Error("FEATURE_DELIVERY_CI_EVIDENCE_REQUIRED");
};
export const canSyncFeatureDeliveryValidation = (status: FeatureDeliveryStatus): boolean =>
["executing", "validating", "pull_request_open"].includes(status);
export const statusAfterFeatureDeliveryValidationSync = (status: FeatureDeliveryStatus): "validating" | "pull_request_open" =>
status === "pull_request_open" ? "pull_request_open" : "validating";
export const createFeatureDelivery = async (projectIdOrSlug: string, input: FeatureDeliveryInput, actor = "chatgpt") => {
assertFeatureDeliveryPlanComplete({ problem: input.problem, desiredOutcome: input.desiredOutcome, goals: input.goals, acceptanceCriteria: input.acceptanceCriteria, implementationPlan: input.implementationPlan, validationCommands: input.validationCommands });
const riskLevel = normalizedRiskLevel(input.riskLevel);
const humanDecisionRequired = requiresHumanDecision(riskLevel);
const initiative = await createInitiativeForProject(projectIdOrSlug, { title: input.title, intent: input.intent, priority: input.priority, sourceType: "chatgpt_feature_delivery" });
const project = await getProject(projectIdOrSlug);
const initiativeRow = await prisma.initiative.findFirstOrThrow({ where: { projectId: project.id, OR: [{ id: initiative.id }, { slug: initiative.id }] } });
const now = new Date();
const plan = await prisma.$transaction(async (tx) => {
const created = await tx.deliveryPlan.create({ data: {
projectId: project.id, initiativeId: initiativeRow.id, slug: publicId(initiativeRow), title: input.title, intent: input.intent, priority: input.priority ?? "normal",
problem: input.problem, desiredOutcome: input.desiredOutcome, usersJson: toJson(input.users ?? []), goalsJson: toJson(input.goals), nonGoalsJson: toJson(input.nonGoals ?? []),
acceptanceCriteriaJson: toJson(input.acceptanceCriteria), implementationPlanJson: toJson(input.implementationPlan), risksJson: toJson(input.risks ?? []), validationCommandsJson: toJson(input.validationCommands),
status: humanDecisionRequired ? DeliveryStatus.awaiting_approval : DeliveryStatus.approved,
approvalState: humanDecisionRequired ? ApprovalState.pending : ApprovalState.approved,
approvedAt: humanDecisionRequired ? null : now,
approvedBy: humanDecisionRequired ? null : actor,
createdBy: actor, updatedBy: actor,
}, include: planInclude });
await tx.initiative.update({ where: { id: initiativeRow.id }, data: {
status: humanDecisionRequired ? "proposed" : "planned",
approvalState: humanDecisionRequired ? ApprovalState.pending : ApprovalState.approved,
updatedBy: actor,
} });
await emit(
tx,
project.id,
created.id,
humanDecisionRequired ? "delivery.plan.user_decision_required" : "delivery.plan.auto_authorized",
humanDecisionRequired
? { actor, slug: created.slug, riskLevel, policyDecision: "USER_DECISION_REQUIRED" }
: { actor, slug: created.slug, riskLevel, policyDecision: riskLevel === "low" ? "AUTO_ALLOWED" : "AUTO_ALLOWED_WITH_AUDIT", mode: authorizationMode(riskLevel) },
);
return created;
});
return toDto(project.slug, plan);
};
export const listFeatureDeliveries = async (projectIdOrSlug: string) => {
const project = await getProject(projectIdOrSlug);
const plans = await prisma.deliveryPlan.findMany({ where: { projectId: project.id }, orderBy: { updatedAt: "desc" }, include: planInclude });
return plans.map((plan) => toDto(project.slug, plan));
};
export const getFeatureDelivery = async (projectIdOrSlug: string, deliveryId: string) => {
const { project, plan } = await getDeliveryRow(projectIdOrSlug, deliveryId);
return toDto(project.slug, plan);
};
export const updateFeatureDelivery = async (projectIdOrSlug: string, deliveryId: string, patch: Partial<FeatureDeliveryInput>, actor = "chatgpt") => {
const { project, plan } = await getDeliveryRow(projectIdOrSlug, deliveryId);
if (!hasStatus(plan.status, [DeliveryStatus.draft, DeliveryStatus.awaiting_approval, DeliveryStatus.approved])) throw new Error("FEATURE_DELIVERY_PLAN_LOCKED");
if (plan.approvalState === ApprovalState.blocked) throw new Error("FEATURE_DELIVERY_BLOCKED");
const riskLevel = normalizedRiskLevel(patch.riskLevel ?? (plan.approvalState === ApprovalState.pending ? "high" : "medium"));
const humanDecisionRequired = requiresHumanDecision(riskLevel);
assertFeatureDeliveryTransition(plan.status, humanDecisionRequired ? DeliveryStatus.awaiting_approval : DeliveryStatus.approved);
const now = new Date();
const updated = await prisma.$transaction(async (tx) => {
const row = await tx.deliveryPlan.update({ where: { id: plan.id }, data: {
title: patch.title, intent: patch.intent, priority: patch.priority, problem: patch.problem, desiredOutcome: patch.desiredOutcome,
usersJson: patch.users ? toJson(patch.users) : undefined, goalsJson: patch.goals ? toJson(patch.goals) : undefined, nonGoalsJson: patch.nonGoals ? toJson(patch.nonGoals) : undefined,
acceptanceCriteriaJson: patch.acceptanceCriteria ? toJson(patch.acceptanceCriteria) : undefined, implementationPlanJson: patch.implementationPlan ? toJson(patch.implementationPlan) : undefined,
risksJson: patch.risks ? toJson(patch.risks) : undefined, validationCommandsJson: patch.validationCommands ? toJson(patch.validationCommands) : undefined,
status: humanDecisionRequired ? DeliveryStatus.awaiting_approval : DeliveryStatus.approved,
approvalState: humanDecisionRequired ? ApprovalState.pending : ApprovalState.approved,
approvedAt: humanDecisionRequired ? null : now,
approvedBy: humanDecisionRequired ? null : actor,
reviewFeedback: null, version: { increment: 1 }, updatedBy: actor,
}, include: planInclude });
if (plan.initiativeId) await tx.initiative.update({ where: { id: plan.initiativeId }, data: {
status: humanDecisionRequired ? "proposed" : "planned",
approvalState: humanDecisionRequired ? ApprovalState.pending : ApprovalState.approved,
updatedBy: actor,
} });
await emit(
tx,
project.id,
plan.id,
humanDecisionRequired ? "delivery.plan.user_decision_required" : "delivery.plan.auto_authorized",
humanDecisionRequired
? { actor, version: row.version, riskLevel, policyDecision: "USER_DECISION_REQUIRED" }
: { actor, version: row.version, riskLevel, policyDecision: riskLevel === "low" ? "AUTO_ALLOWED" : "AUTO_ALLOWED_WITH_AUDIT", mode: authorizationMode(riskLevel) },
);
return row;
});
return toDto(project.slug, updated);
};
export const approveFeatureDelivery = async (projectIdOrSlug: string, deliveryId: string, actor = "owner") => {
const { project, plan } = await getDeliveryRow(projectIdOrSlug, deliveryId);
assertFeatureDeliveryPlanComplete({ problem: plan.problem, desiredOutcome: plan.desiredOutcome, goals: strings(plan.goalsJson), acceptanceCriteria: strings(plan.acceptanceCriteriaJson), implementationPlan: strings(plan.implementationPlanJson), validationCommands: strings(plan.validationCommandsJson) });
if (!hasStatus(plan.status, [DeliveryStatus.draft, DeliveryStatus.awaiting_approval, DeliveryStatus.approved])) throw new Error("FEATURE_DELIVERY_APPROVAL_LOCKED");
assertFeatureDeliveryTransition(plan.status, DeliveryStatus.approved);
const updated = await prisma.$transaction(async (tx) => {
const row = await tx.deliveryPlan.update({ where: { id: plan.id }, data: { status: DeliveryStatus.approved, approvalState: ApprovalState.approved, approvedAt: new Date(), approvedBy: actor, reviewFeedback: null, updatedBy: actor }, include: planInclude });
if (plan.initiativeId) await tx.initiative.update({ where: { id: plan.initiativeId }, data: { status: "planned", approvalState: ApprovalState.approved, updatedBy: actor } });
await emit(tx, project.id, plan.id, "delivery.plan.approved", { actor, version: row.version });
return row;
});
return toDto(project.slug, updated);
};
export const rejectFeatureDelivery = async (projectIdOrSlug: string, deliveryId: string, actor = "owner", reason?: string) => {
const { project, plan } = await getDeliveryRow(projectIdOrSlug, deliveryId);
if (!hasStatus(plan.status, [DeliveryStatus.draft, DeliveryStatus.awaiting_approval])) throw new Error("FEATURE_DELIVERY_REJECTION_LOCKED");
assertFeatureDeliveryTransition(plan.status, DeliveryStatus.awaiting_approval);
return prisma.$transaction(async (tx) => {
const feedback = reason?.trim() || "Herwerk dit plan voor een nieuwe uitvoeringspoging.";
const row = await tx.deliveryPlan.update({ where: { id: plan.id }, data: { status: DeliveryStatus.awaiting_approval, approvalState: ApprovalState.rejected, reviewFeedback: feedback, updatedBy: actor }, include: planInclude });
if (plan.initiativeId) await tx.initiative.update({ where: { id: plan.initiativeId }, data: { approvalState: ApprovalState.rejected, updatedBy: actor } });
await emit(tx, project.id, plan.id, "delivery.plan.revision_requested", { actor, version: row.version, reason: feedback });
return toDto(project.slug, row);
});
};
export const startFeatureDelivery = async (projectIdOrSlug: string, deliveryId: string, branch: string, actor = "chatgpt", input: { repositoryLinkId?: string; baseCommitSha?: string; headSha?: string } = {}) => {
const { project, plan } = await getDeliveryRow(projectIdOrSlug, deliveryId);
assertFeatureDeliveryPlanComplete({ problem: plan.problem, desiredOutcome: plan.desiredOutcome, goals: strings(plan.goalsJson), acceptanceCriteria: strings(plan.acceptanceCriteriaJson), implementationPlan: strings(plan.implementationPlanJson), validationCommands: strings(plan.validationCommandsJson) });
if (plan.approvalState !== ApprovalState.approved || plan.status !== DeliveryStatus.approved) throw new Error("FEATURE_DELIVERY_APPROVAL_REQUIRED");
assertFeatureDeliveryTransition(plan.status, DeliveryStatus.executing);
const repository = input.repositoryLinkId
? await prisma.giteaRepositoryLink.findFirst({ where: { id: input.repositoryLinkId, projectId: project.id } })
: await prisma.giteaRepositoryLink.findFirst({ where: { projectId: project.id }, orderBy: [{ isPrimary: "desc" }, { createdAt: "asc" }] });
if (!repository) throw new Error("GITEA_LINK_NOT_FOUND");
const updated = await prisma.$transaction(async (tx) => {
const run = await tx.deliveryRun.create({ data: { deliveryPlanId: plan.id, repositoryLinkId: repository.id, branch, baseBranch: repository.defaultBranch, baseCommitSha: input.baseCommitSha, headSha: input.headSha, status: DeliveryStatus.executing, createdBy: actor, updatedBy: actor } });
const row = await tx.deliveryPlan.update({ where: { id: plan.id }, data: { status: DeliveryStatus.executing, approvalState: ApprovalState.approved, approvedAt: plan.approvedAt ?? new Date(), approvedBy: plan.approvedBy ?? actor, updatedBy: actor }, include: planInclude });
const investigation = await tx.findingInvestigation.findUnique({ where: { deliveryPlanId: plan.id } });
if (investigation) {
await tx.findingInvestigation.update({ where: { id: investigation.id }, data: { status: "fixing" } });
await tx.repositoryFinding.update({ where: { id: investigation.findingId }, data: { status: "fixing" } });
}
if (plan.initiativeId) await tx.initiative.update({ where: { id: plan.initiativeId }, data: { status: "active", approvalState: ApprovalState.approved, updatedBy: actor } });
await emit(tx, project.id, plan.id, "delivery.run.started", { actor, runId: run.id, repositoryLinkId: repository.id, branch, authorization: "projectbrain_policy" });
return row;
});
return toDto(project.slug, updated);
};
export const recordFeatureDeliveryProgress = async (projectIdOrSlug: string, deliveryId: string, input: { changedFiles?: string[]; changes?: DeliveryChangeInput[]; note?: string; validationResults?: FeatureValidationResult[]; readyForReview?: boolean; headSha?: string }, actor = "chatgpt") => {
const { project, plan } = await getDeliveryRow(projectIdOrSlug, deliveryId);
const run = plan.runs[0];
if (!run || plan.approvalState !== ApprovalState.approved || !hasStatus(plan.status, [DeliveryStatus.executing, DeliveryStatus.validating, DeliveryStatus.pull_request_open])) throw new Error("FEATURE_DELIVERY_PROGRESS_FORBIDDEN");
const changes: DeliveryChangeInput[] = input.changes ?? (input.changedFiles ?? []).map((path) => ({ path, operation: "update" }));
const updated = await prisma.$transaction(async (tx) => {
for (const change of changes) await tx.deliveryFileChange.create({ data: { deliveryRunId: run.id, path: change.path, previousPath: change.previousPath, operation: change.operation as DeliveryChangeOperation, beforeSha: change.beforeSha, afterSha: change.afterSha, commitSha: change.commitSha, actor } });
if (changes.length || (input.headSha && input.headSha !== run.headSha)) await tx.deliveryValidation.deleteMany({ where: { deliveryRunId: run.id } });
if (input.validationResults) {
await tx.deliveryValidation.deleteMany({ where: { deliveryRunId: run.id, provider: null } });
for (const result of input.validationResults) await tx.deliveryValidation.create({ data: { deliveryRunId: run.id, command: result.command, status: result.status === "passed" ? DeliveryValidationStatus.passed : result.status === "failed" ? DeliveryValidationStatus.failed : DeliveryValidationStatus.skipped, summary: result.summary } });
}
const status = plan.status === DeliveryStatus.pull_request_open ? plan.status : input.readyForReview ? DeliveryStatus.validating : plan.status;
assertFeatureDeliveryTransition(plan.status, status);
await tx.deliveryRun.update({ where: { id: run.id }, data: { status, headSha: input.headSha, updatedBy: actor } });
const row = await tx.deliveryPlan.update({ where: { id: plan.id }, data: { status, updatedBy: actor }, include: planInclude });
await emit(tx, project.id, plan.id, "delivery.progress.recorded", { actor, runId: run.id, note: input.note, changeCount: changes.length, status });
return row;
});
return toDto(project.slug, updated);
};
export const recordFeatureDeliveryActionValidation = async (projectIdOrSlug: string, deliveryId: string, evidence: FeatureValidationEvidence, actor = "chatgpt") => {
const { project, plan } = await getDeliveryRow(projectIdOrSlug, deliveryId);
const run = plan.runs[0];
if (!run || plan.approvalState !== ApprovalState.approved || !canSyncFeatureDeliveryValidation(plan.status)) throw new Error("FEATURE_DELIVERY_PROGRESS_FORBIDDEN");
const nextStatus = statusAfterFeatureDeliveryValidationSync(plan.status) === "pull_request_open"
? DeliveryStatus.pull_request_open
: DeliveryStatus.validating;
assertFeatureDeliveryTransition(plan.status, nextStatus);
const command = actionValidationCommand(evidence.workflowPath);
const updated = await prisma.$transaction(async (tx) => {
await tx.deliveryValidation.deleteMany({ where: { deliveryRunId: run.id, provider: "gitea_actions" } });
await tx.deliveryValidation.create({ data: { deliveryRunId: run.id, command, status: DeliveryValidationStatus.passed, provider: "gitea_actions", externalRunId: String(evidence.runId), externalUrl: evidence.url, checkName: `run-${evidence.runNumber}`, headSha: evidence.headSha, summary: evidence.workflowPath, verifiedAt: new Date(evidence.verifiedAt) } });
await tx.deliveryRun.update({ where: { id: run.id }, data: { status: nextStatus, headSha: evidence.headSha, updatedBy: actor } });
const row = await tx.deliveryPlan.update({ where: { id: plan.id }, data: { status: nextStatus, updatedBy: actor }, include: planInclude });
await emit(tx, project.id, plan.id, "delivery.validation.passed", { actor, runId: run.id, headSha: evidence.headSha, externalRunId: evidence.runId });
return row;
});
return toDto(project.slug, updated);
};
export const openFeatureDeliveryPullRequest = async (projectIdOrSlug: string, deliveryId: string, pullRequest: { number: number; url: string; title: string }, actor = "chatgpt") => {
const { project, plan } = await getDeliveryRow(projectIdOrSlug, deliveryId);
const run = plan.runs[0];
if (!run || plan.approvalState !== ApprovalState.approved || plan.status !== DeliveryStatus.validating) throw new Error("FEATURE_DELIVERY_COMPLETION_FORBIDDEN");
assertFeatureDeliveryTransition(plan.status, DeliveryStatus.pull_request_open);
const updated = await prisma.$transaction(async (tx) => {
await tx.deliveryRun.update({ where: { id: run.id }, data: { status: DeliveryStatus.pull_request_open, pullRequestNumber: pullRequest.number, pullRequestUrl: pullRequest.url, pullRequestTitle: pullRequest.title, pullRequestState: "open", pullRequestOpenedAt: new Date(), updatedBy: actor } });
const row = await tx.deliveryPlan.update({ where: { id: plan.id }, data: { status: DeliveryStatus.pull_request_open, updatedBy: actor }, include: planInclude });
await emit(tx, project.id, plan.id, "delivery.pull_request.opened", { actor, runId: run.id, pullRequest });
return row;
});
return toDto(project.slug, updated);
};
/** Compatibility export: opening a PR no longer marks the delivery completed. */
export const completeFeatureDelivery = openFeatureDeliveryPullRequest;
export const reconcileFeatureDeliveryPullRequest = async (input: { owner: string; repository: string; number: number; state: "open" | "closed"; merged: boolean; mergeCommitSha?: string; headSha?: string; mergedAt?: string; actor?: string }) => {
const run = await prisma.deliveryRun.findFirst({ where: { pullRequestNumber: input.number, repositoryLink: { owner: input.owner, repository: input.repository } }, include: { deliveryPlan: true } });
if (!run) return undefined;
const actor = input.actor ?? "gitea-webhook";
const status = input.merged ? DeliveryStatus.merged : input.state === "closed" ? DeliveryStatus.failed : DeliveryStatus.pull_request_open;
assertFeatureDeliveryTransition(run.deliveryPlan.status, status);
await prisma.$transaction(async (tx) => {
await tx.deliveryRun.update({ where: { id: run.id }, data: { status, pullRequestState: input.merged ? "merged" : input.state, mergeCommitSha: input.mergeCommitSha, headSha: input.headSha, mergedAt: input.merged ? new Date(input.mergedAt ?? Date.now()) : undefined, completedAt: input.merged ? new Date(input.mergedAt ?? Date.now()) : input.state === "closed" ? new Date() : undefined, updatedBy: actor } });
await tx.deliveryPlan.update({ where: { id: run.deliveryPlanId }, data: { status, updatedBy: actor } });
if (run.deliveryPlan.initiativeId) await tx.initiative.update({ where: { id: run.deliveryPlan.initiativeId }, data: { status: input.merged ? "complete" : input.state === "closed" ? "paused" : "active", updatedBy: actor } });
await emit(tx, run.deliveryPlan.projectId, run.deliveryPlanId, input.merged ? "delivery.pull_request.merged" : input.state === "closed" ? "delivery.pull_request.closed" : "delivery.pull_request.updated", { actor, runId: run.id, ...input });
});
return getFeatureDelivery(run.deliveryPlan.projectId, run.deliveryPlanId);
};
export const markFeatureDeliveryVerified = async (projectIdOrSlug: string, deliveryId: string, actor = "system") => {
const { project, plan } = await getDeliveryRow(projectIdOrSlug, deliveryId);
const run = plan.runs[0];
if (!run || plan.status !== DeliveryStatus.merged) throw new Error("FEATURE_DELIVERY_NOT_MERGED");
assertFeatureDeliveryTransition(plan.status, DeliveryStatus.verified);
const updated = await prisma.$transaction(async (tx) => {
await tx.deliveryRun.update({ where: { id: run.id }, data: { status: DeliveryStatus.verified, verifiedAt: new Date(), completedAt: new Date(), updatedBy: actor } });
const row = await tx.deliveryPlan.update({ where: { id: plan.id }, data: { status: DeliveryStatus.verified, updatedBy: actor }, include: planInclude });
await emit(tx, project.id, plan.id, "delivery.verified", { actor, runId: run.id, mergeCommitSha: run.mergeCommitSha });
return row;
});
return toDto(project.slug, updated);
};
@@ -0,0 +1,43 @@
import { prisma } from "../db/prisma.js";
import type { FeatureGroupDto, FocusAreaPurpose } from "../types/api.js";
const toPublicId = (value: { slug: string | null; readableId: string | null; id: string }) =>
value.slug ?? value.readableId ?? value.id;
const findProjectRecordBySlugOrId = (projectIdOrSlug: string) =>
prisma.project.findFirst({
where: {
deletedAt: null,
OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }],
},
});
export const listFeatureGroupsForProject = async (
projectIdOrSlug: string,
): Promise<FeatureGroupDto[] | undefined> => {
const project = await findProjectRecordBySlugOrId(projectIdOrSlug);
if (!project) return undefined;
const groups = await prisma.featureGroup.findMany({
where: {
projectId: project.id,
deletedAt: null,
},
orderBy: [{ name: "asc" }],
include: {
features: {
where: { deletedAt: null },
orderBy: { name: "asc" },
},
},
});
return groups.map((group) => ({
id: toPublicId(group),
projectId: project.slug,
name: group.name,
description: group.description ?? undefined,
focusArea: group.focusArea as FocusAreaPurpose | undefined,
featureIds: group.features.map(toPublicId),
}));
};
@@ -0,0 +1,138 @@
import type { Prisma } from "@prisma/client";
import { prisma } from "../db/prisma.js";
import type {
CodeEvidenceDto,
EvidenceStrength,
FeatureDto,
FeatureStatus,
} from "../types/api.js";
const toPublicId = (value: { slug: string | null; readableId: string | null; id: string }) =>
value.slug ?? value.readableId ?? value.id;
type FeatureMetadata = {
bugCount?: number;
taskCount?: number;
decisionCount?: number;
codeEvidence?: CodeEvidenceDto[];
aiSuggestionCount?: number;
evidenceIds?: string[];
initiativeId?: string;
};
type DbFeatureRecord = {
id: string;
projectId: string;
featureGroupId: string;
slug: string | null;
readableId: string | null;
name: string;
description: string;
status: string;
lifecycle: FeatureDto["lifecycle"];
intent: string | null;
confidence: number;
health: number;
evidenceStrength: string | null;
trustJson: Prisma.JsonValue | null;
featureGroup?: {
id: string;
slug: string | null;
readableId: string | null;
} | null;
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;
const toFeatureMetadata = (value: Prisma.JsonValue | null): FeatureMetadata => {
if (!isRecord(value)) return {};
return value as FeatureMetadata;
};
const findProjectRecordBySlugOrId = (projectIdOrSlug: string) =>
prisma.project.findFirst({
where: {
deletedAt: null,
OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }],
},
});
const mapFeatureStatus = (status: string): FeatureStatus => {
if (status === "done") return "Done";
if (status === "in_progress") return "In Progress";
return "Todo";
};
const toFeatureDto = (feature: DbFeatureRecord, projectSlug: string): FeatureDto => {
const metadata = toFeatureMetadata(feature.trustJson);
return {
id: toPublicId(feature),
projectId: projectSlug,
groupId: feature.featureGroup ? toPublicId(feature.featureGroup) : feature.featureGroupId,
name: feature.name,
description: feature.description,
status: mapFeatureStatus(feature.status),
lifecycle: feature.lifecycle,
intent: feature.intent ?? undefined,
initiativeId: metadata.initiativeId,
evidenceStrength: feature.evidenceStrength as EvidenceStrength | undefined,
confidence: feature.confidence,
health: feature.health,
bugCount: metadata.bugCount ?? 0,
taskCount: metadata.taskCount ?? 0,
decisionCount: metadata.decisionCount ?? 0,
codeEvidence: metadata.codeEvidence ?? [],
aiSuggestionCount: metadata.aiSuggestionCount ?? 0,
evidenceIds: metadata.evidenceIds ?? [],
};
};
export const listFeaturesForProject = async (
projectIdOrSlug: string,
): Promise<FeatureDto[] | undefined> => {
const project = await findProjectRecordBySlugOrId(projectIdOrSlug);
if (!project) return undefined;
const features = await prisma.feature.findMany({
where: {
projectId: project.id,
deletedAt: null,
},
orderBy: [{ name: "asc" }],
include: {
featureGroup: true,
},
});
return features.map((feature) => toFeatureDto(feature, project.slug));
};
export const getFeatureBySlugOrId = async (
projectIdOrSlug: string,
featureIdOrSlug: string,
): Promise<FeatureDto | undefined> => {
const project = await findProjectRecordBySlugOrId(projectIdOrSlug);
if (!project) return undefined;
const feature = await prisma.feature.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: featureIdOrSlug }, { slug: featureIdOrSlug }, { readableId: featureIdOrSlug }],
},
include: {
featureGroup: true,
},
});
if (!feature) return undefined;
return toFeatureDto(feature, project.slug);
};
export const projectExists = async (projectIdOrSlug: string): Promise<boolean> => {
const project = await findProjectRecordBySlugOrId(projectIdOrSlug);
return Boolean(project);
};
@@ -0,0 +1,455 @@
import { createHash, randomUUID } from "node:crypto";
import { FindingSeverity, FindingStatus, InvestigationStatus, IssueSyncMode, type Prisma } from "@prisma/client";
import type { AppEnv } from "../config/env.js";
import { prisma } from "../db/prisma.js";
import { getGiteaIssueClient } from "../integrations/gitea/service.js";
import type { GiteaIssue, GiteaLabel } from "../integrations/gitea/types.js";
import { createFeatureDelivery, markFeatureDeliveryVerified, type FeatureDeliveryInput } from "./featureDeliveryRepository.js";
import { createPortfolioAnalysisJob } from "./analysisJobRepository.js";
export type FindingEvidence = Record<string, unknown>;
export type DetectedFinding = { severity: "info" | "warning" | "critical"; code: string; message: string; subject?: string; evidence?: FindingEvidence };
type FindingScope = {
jobId: string;
analysisJobItemId: string;
projectId?: string;
repositoryLinkId?: string;
owner: string;
repository: string;
defaultBranch: string;
findings: DetectedFinding[];
evaluatedRuleCodes?: string[];
};
const toJson = (value: unknown): Prisma.InputJsonValue => JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
const severityRank: Record<FindingSeverity, number> = { info: 1, warning: 2, critical: 3 };
const baseRiskBySeverity: Record<FindingSeverity, number> = { info: 20, warning: 60, critical: 90 };
const terminalInvestigationStatuses: InvestigationStatus[] = [InvestigationStatus.resolved, InvestigationStatus.failed, InvestigationStatus.cancelled];
const projectByIdOrSlug = async (projectIdOrSlug: string) => {
const project = await prisma.project.findFirst({ where: { deletedAt: null, OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }, { readableId: projectIdOrSlug }] } });
if (!project) throw new Error("PROJECT_NOT_FOUND");
return project;
};
const subjectFor = (finding: DetectedFinding, defaultBranch: string) => finding.subject ?? (finding.code === "DEFAULT_BRANCH_UNPROTECTED" ? defaultBranch : "repository");
export const repositoryFindingFingerprint = (owner: string, repository: string, code: string, subject: string) => createHash("sha256").update(`${owner.toLowerCase()}/${repository.toLowerCase()}:${code}:${subject}`).digest("hex");
const titleFor = (finding: DetectedFinding, owner: string, repository: string) => `[ProjectBrain] ${finding.code.replaceAll("_", " ").toLowerCase()} in ${owner}/${repository}`;
const evidenceNumber = (finding: DetectedFinding, key: string) => {
const value = finding.evidence?.[key];
return typeof value === "number" ? value : 0;
};
const evidenceString = (finding: DetectedFinding, key: string) => {
const value = finding.evidence?.[key];
return typeof value === "string" ? value : undefined;
};
export const findingRiskScore = (finding: DetectedFinding) => {
const scopeBonus = evidenceString(finding, "scope") === "default_branch" ? 8 : evidenceString(finding, "scope") === "scheduled" ? 4 : 0;
const persistenceBonus = Math.min(4, Math.max(0, evidenceNumber(finding, "consecutiveFailures") - 1));
const ruleBonus = finding.code === "DEFAULT_BRANCH_ACTION_FAILED" ? 2 : finding.code === "DEFAULT_BRANCH_IS_AGENT_BRANCH" ? 3 : 0;
return Math.min(100, baseRiskBySeverity[finding.severity as FindingSeverity] + scopeBonus + persistenceBonus + ruleBonus);
};
const legacyMigrationCandidate = (finding: DetectedFinding) => finding.severity === "critical" && new Set(["DEFAULT_BRANCH_ACTION_FAILED", "SCHEDULED_ACTION_FAILED"]).has(finding.code);
export const reconcileRepositoryFindings = async (scope: FindingScope) => {
const now = new Date();
const observedFingerprints = scope.findings.map((finding) => repositoryFindingFingerprint(scope.owner, scope.repository, finding.code, subjectFor(finding, scope.defaultBranch)));
return prisma.$transaction(async (tx) => {
const reconciled = [];
for (let index = 0; index < scope.findings.length; index += 1) {
const detected = scope.findings[index];
const severity = detected.severity as FindingSeverity;
const subject = subjectFor(detected, scope.defaultBranch);
const fingerprint = observedFingerprints[index];
let existing = await tx.repositoryFinding.findUnique({ where: { fingerprint } });
if (!existing && legacyMigrationCandidate(detected)) {
const legacy = await tx.repositoryFinding.findFirst({
where: {
owner: { equals: scope.owner, mode: "insensitive" },
repository: { equals: scope.repository, mode: "insensitive" },
ruleCode: "LATEST_ACTION_FAILED",
status: { notIn: [FindingStatus.resolved, FindingStatus.suppressed] },
},
orderBy: { lastSeenAt: "desc" },
});
if (legacy) {
existing = await tx.repositoryFinding.update({
where: { id: legacy.id },
data: {
fingerprint,
ruleCode: detected.code,
subject,
status: legacy.status === FindingStatus.issue_open ? FindingStatus.triaged : legacy.status,
},
});
}
}
const suppressionExpired = existing?.status === FindingStatus.suppressed && existing.suppressedUntil && existing.suppressedUntil <= now;
const nextStatus = existing?.status === FindingStatus.resolved || suppressionExpired ? FindingStatus.regressed : existing?.status ?? FindingStatus.new;
const finding = existing
? await tx.repositoryFinding.update({ where: { id: existing.id }, data: {
projectId: scope.projectId || existing.projectId, repositoryLinkId: scope.repositoryLinkId || existing.repositoryLinkId,
owner: scope.owner, repository: scope.repository,
title: titleFor(detected, scope.owner, scope.repository), message: detected.message, severity, riskScore: findingRiskScore(detected),
status: nextStatus, occurrenceCount: existing.lastAnalysisJobId === scope.jobId ? existing.occurrenceCount : { increment: 1 }, clearCount: 0, lastSeenAt: now, lastAnalysisJobId: scope.jobId,
resolvedAt: null, ...(suppressionExpired ? { suppressedAt: null, suppressedUntil: null, suppressionReason: null } : {}),
} })
: await tx.repositoryFinding.create({ data: {
fingerprint, projectId: scope.projectId || undefined, repositoryLinkId: scope.repositoryLinkId,
owner: scope.owner, repository: scope.repository, ruleCode: detected.code, subject,
title: titleFor(detected, scope.owner, scope.repository), message: detected.message, severity,
confidence: 95, riskScore: findingRiskScore(detected), status: FindingStatus.new, firstSeenAt: now, lastSeenAt: now, lastAnalysisJobId: scope.jobId,
} });
await tx.findingOccurrence.upsert({
where: { findingId_analysisJobItemId: { findingId: finding.id, analysisJobItemId: scope.analysisJobItemId } },
create: { findingId: finding.id, analysisJobItemId: scope.analysisJobItemId, observed: true, detailJson: toJson(detected) },
update: { observed: true, detailJson: toJson(detected) },
});
reconciled.push(finding);
}
const missing = await tx.repositoryFinding.findMany({
where: {
owner: { equals: scope.owner, mode: "insensitive" },
repository: { equals: scope.repository, mode: "insensitive" },
fingerprint: { notIn: observedFingerprints },
status: { notIn: [FindingStatus.resolved, FindingStatus.suppressed] },
},
});
for (const finding of missing) {
if (!scope.evaluatedRuleCodes?.includes(finding.ruleCode)) continue;
if (finding.lastAnalysisJobId === scope.jobId) continue;
const clearCount = finding.clearCount + 1;
await tx.repositoryFinding.update({ where: { id: finding.id }, data: {
clearCount,
...(clearCount >= 2 ? { status: FindingStatus.resolved, resolvedAt: now } : {}),
lastAnalysisJobId: scope.jobId,
} });
await tx.findingOccurrence.upsert({
where: { findingId_analysisJobItemId: { findingId: finding.id, analysisJobItemId: scope.analysisJobItemId } },
create: { findingId: finding.id, analysisJobItemId: scope.analysisJobItemId, observed: false },
update: { observed: false },
});
}
return reconciled;
});
};
const findingInclude = {
project: { select: { id: true, slug: true, name: true } },
repositoryLink: true,
issueLink: true,
investigations: { orderBy: { createdAt: "desc" as const }, take: 3, include: { deliveryPlan: { select: { id: true, slug: true, title: true, status: true, approvalState: true } } } },
occurrences: { where: { observed: true }, orderBy: [{ createdAt: "desc" as const }, { id: "desc" as const }], take: 1, select: { observed: true, detailJson: true, createdAt: true } },
} satisfies Prisma.RepositoryFindingInclude;
export const listRepositoryFindings = async (input: { projectId?: string; statuses?: FindingStatus[]; severities?: FindingSeverity[]; limit?: number; cursor?: string }) => {
const project = input.projectId ? await projectByIdOrSlug(input.projectId) : undefined;
const cursor = Number.parseInt(input.cursor ?? "0", 10);
if (!Number.isInteger(cursor) || cursor < 0) throw new Error("FINDING_CURSOR_INVALID");
const limit = Math.min(Math.max(input.limit ?? 20, 1), 50);
const rows = await prisma.repositoryFinding.findMany({
where: { ...(project ? { projectId: project.id } : {}), ...(input.statuses?.length ? { status: { in: input.statuses } } : {}), ...(input.severities?.length ? { severity: { in: input.severities } } : {}) },
orderBy: [{ riskScore: "desc" }, { lastSeenAt: "desc" }, { id: "asc" }], skip: cursor, take: limit + 1, include: findingInclude,
});
return { items: rows.slice(0, limit), nextCursor: rows.length > limit ? String(cursor + limit) : undefined };
};
export const getRepositoryFinding = async (findingId: string) => {
const finding = await prisma.repositoryFinding.findUnique({ where: { id: findingId }, include: {
...findingInclude,
occurrences: { orderBy: [{ createdAt: "desc" }, { id: "desc" }], take: 20, include: { analysisJobItem: { select: { jobId: true, status: true, summary: true, createdAt: true } } } },
} });
if (!finding) throw new Error("FINDING_NOT_FOUND");
return finding;
};
export const suppressRepositoryFinding = async (findingId: string, reason: string, until?: Date) => {
const finding = await prisma.repositoryFinding.update({ where: { id: findingId }, data: { status: FindingStatus.suppressed, suppressionReason: reason, suppressedAt: new Date(), suppressedUntil: until } });
return finding;
};
export const getFindingAutomationPolicy = async (projectIdOrSlug: string) => {
const project = await projectByIdOrSlug(projectIdOrSlug);
return prisma.findingAutomationPolicy.upsert({ where: { projectId: project.id }, create: { projectId: project.id }, update: {} });
};
export const updateFindingAutomationPolicy = async (projectIdOrSlug: string, input: {
enabled?: boolean; issueSyncMode?: IssueSyncMode; minimumSeverity?: FindingSeverity; minimumOccurrences?: number; maxIssuesPerRun?: number;
autoCloseResolved?: boolean; autoReopenRegressed?: boolean; scheduledAnalysis?: boolean; scheduleMinutes?: number; updatedBy: string;
}) => {
const project = await projectByIdOrSlug(projectIdOrSlug);
return prisma.findingAutomationPolicy.upsert({
where: { projectId: project.id },
create: { projectId: project.id, ...input },
update: input,
});
};
export const findingIssueMarker = (finding: { id: string; fingerprint: string }) => `<!-- projectbrain:finding=${finding.id} fingerprint=${finding.fingerprint} -->`;
const latestObservedDetection = (finding: Awaited<ReturnType<typeof getRepositoryFinding>>): DetectedFinding | undefined => {
const detail = finding.occurrences.find((occurrence) => occurrence.observed)?.detailJson;
return detail && typeof detail === "object" && !Array.isArray(detail) ? detail as DetectedFinding : undefined;
};
export const renderFindingEvidence = (evidence?: FindingEvidence) => {
if (!evidence) return "";
const entries = [
["Workflow", evidence.workflow],
["Branch", evidence.branch],
["Event", evidence.event],
["Conclusie", evidence.conclusion],
["Opeenvolgende failures", evidence.consecutiveFailures],
["Voltooid", evidence.completedAt],
].filter((entry): entry is [string, string | number] => typeof entry[1] === "string" || typeof entry[1] === "number");
const runUrl = typeof evidence.runUrl === "string" ? evidence.runUrl : undefined;
if (runUrl) entries.unshift(["Gitea Actions-run", `[open run](${runUrl})`]);
if (!entries.length) return "";
return `\n## Bewijs\n\n${entries.map(([label, value]) => `- **${label}:** ${value}`).join("\n")}\n`;
};
const issueBodyFor = (finding: Awaited<ReturnType<typeof getRepositoryFinding>>) => `${findingIssueMarker(finding)}
## ProjectBrain-bevinding
${finding.message}
- **Repository:** \`${finding.owner}/${finding.repository}\`
- **Regel:** \`${finding.ruleCode}\`
- **Ernst:** ${finding.severity}
- **Vertrouwen:** ${finding.confidence}%
- **Eerste detectie:** ${finding.firstSeenAt.toISOString()}
- **Laatste detectie:** ${finding.lastSeenAt.toISOString()}
- **Waarnemingen:** ${finding.occurrenceCount}
${renderFindingEvidence(latestObservedDetection(finding)?.evidence)}
Dit issue wordt idempotent beheerd door ProjectBrain. Onderzoek en codewijzigingen blijven onder de ProjectBrain-deliverygoedkeuring vallen.`;
const labelDefinitions = (finding: { severity: FindingSeverity; ruleCode: string }) => [
{ name: "projectbrain", color: "6d28d9", description: "Automatisch beheerd door ProjectBrain" },
{ name: `severity:${finding.severity}`, color: finding.severity === FindingSeverity.critical ? "dc2626" : finding.severity === FindingSeverity.warning ? "f59e0b" : "2563eb", description: "ProjectBrain finding severity" },
{ name: `finding:${finding.ruleCode.toLowerCase().replaceAll("_", "-")}`, color: "475569", description: `ProjectBrain rule ${finding.ruleCode}` },
];
const ensureLabels = async (env: AppEnv, owner: string, repository: string, finding: { severity: FindingSeverity; ruleCode: string }): Promise<GiteaLabel[]> => {
const client = getGiteaIssueClient(env);
const existing = await client.listLabels(owner, repository);
const labels: GiteaLabel[] = [];
for (const definition of labelDefinitions(finding)) {
const label = existing.find((candidate) => candidate.name === definition.name) ?? await client.createLabel(owner, repository, definition);
labels.push(label);
}
return labels;
};
const persistIssueLink = async (finding: Awaited<ReturnType<typeof getRepositoryFinding>>, issue: GiteaIssue, body: string) => prisma.$transaction(async (tx) => {
const bodyFingerprint = createHash("sha256").update(body).digest("hex");
const link = await tx.giteaIssueLink.upsert({
where: { findingId: finding.id },
create: { findingId: finding.id, owner: finding.owner, repository: finding.repository, issueNumber: issue.number, issueUrl: issue.html_url, state: issue.state, title: issue.title, bodyFingerprint, lastSyncedAt: new Date(), closedAt: issue.state === "closed" ? new Date(issue.closed_at ?? Date.now()) : undefined },
update: { issueNumber: issue.number, issueUrl: issue.html_url, state: issue.state, title: issue.title, bodyFingerprint, lastSyncedAt: new Date(), closedAt: issue.state === "closed" ? new Date(issue.closed_at ?? Date.now()) : null },
});
if (finding.projectId) await tx.outboxEvent.create({ data: { eventId: randomUUID(), projectId: finding.projectId, aggregateType: "finding", aggregateId: finding.id, eventType: "finding.issue.synced", payloadJson: toJson({ issueNumber: issue.number, issueUrl: issue.html_url, state: issue.state }) } });
await tx.repositoryFinding.update({ where: { id: finding.id }, data: { status: issue.state === "open" ? FindingStatus.issue_open : finding.status } });
return link;
});
export const syncFindingIssue = async (env: AppEnv, findingId: string, actor = "system") => {
const finding = await getRepositoryFinding(findingId);
if (!finding.repositoryLink || finding.repositoryLink.accessMode !== "read_write") throw new Error("FINDING_REPOSITORY_WRITE_LINK_REQUIRED");
if (finding.status === FindingStatus.resolved || finding.status === FindingStatus.suppressed) throw new Error("FINDING_ISSUE_CREATION_FORBIDDEN");
const client = getGiteaIssueClient(env);
const body = issueBodyFor(finding);
const marker = findingIssueMarker(finding);
const labels = await ensureLabels(env, finding.owner, finding.repository, finding);
let issue: GiteaIssue | undefined;
if (finding.issueLink) {
issue = await client.getIssue(finding.owner, finding.repository, finding.issueLink.issueNumber);
} else {
issue = (await client.listIssues(finding.owner, finding.repository, "all", 1, 100)).find((candidate) => candidate.body?.includes(marker));
}
if (!issue) issue = await client.createIssue(finding.owner, finding.repository, { title: finding.title, body, labels: labels.map((label) => label.id) });
else {
issue = await client.editIssue(finding.owner, finding.repository, issue.number, { title: finding.title, body, ...(issue.state === "closed" ? { state: "open" as const } : {}) });
await client.replaceIssueLabels(finding.owner, finding.repository, issue.number, labels.map((label) => label.id));
}
const link = await persistIssueLink(finding, issue, body);
return { finding: await getRepositoryFinding(finding.id), issue, link, actor };
};
export const closeResolvedFindingIssue = async (env: AppEnv, findingId: string, summary: string) => {
const finding = await getRepositoryFinding(findingId);
if (!finding.issueLink || finding.issueLink.state === "closed") return finding;
const client = getGiteaIssueClient(env);
await client.createIssueComment(finding.owner, finding.repository, finding.issueLink.issueNumber, `ProjectBrain-verificatie: ${summary}`);
const issue = await client.editIssue(finding.owner, finding.repository, finding.issueLink.issueNumber, { state: "closed" });
await persistIssueLink(finding, issue, issueBodyFor(finding));
return getRepositoryFinding(finding.id);
};
export const closeAutomaticallyResolvedFindingIssues = async (env: AppEnv) => {
const findings = await prisma.repositoryFinding.findMany({
where: { status: FindingStatus.resolved, projectId: { not: null }, issueLink: { state: "open" } },
include: { project: { include: { findingAutomationPolicy: true } }, issueLink: true },
orderBy: [{ resolvedAt: "asc" }, { id: "asc" }],
take: 25,
});
const closed: string[] = [];
for (const finding of findings) {
const policy = finding.project?.findingAutomationPolicy ?? (finding.projectId ? await getFindingAutomationPolicy(finding.projectId) : undefined);
if (!policy?.autoCloseResolved) continue;
try {
await closeResolvedFindingIssue(env, finding.id, "Twee opeenvolgende analyses vonden de bevinding niet meer.");
closed.push(finding.id);
} catch (error) {
console.warn(JSON.stringify({ event: "finding_issue_close_failed", findingId: finding.id, code: error instanceof Error ? error.message : "FINDING_ISSUE_CLOSE_FAILED" }));
}
}
return closed;
};
export const reconcileFindingIssueWebhook = async (input: { owner: string; repository: string; issueNumber: number; state: string; title: string; url: string; actor: string }) => {
const link = await prisma.giteaIssueLink.findUnique({ where: { owner_repository_issueNumber: { owner: input.owner, repository: input.repository, issueNumber: input.issueNumber } }, include: { finding: true } });
if (!link) return undefined;
return prisma.$transaction(async (tx) => {
await tx.giteaIssueLink.update({ where: { id: link.id }, data: { state: input.state, title: input.title, issueUrl: input.url, lastSyncedAt: new Date(), closedAt: input.state === "closed" ? new Date() : null } });
const wasResolved = link.finding.status === FindingStatus.resolved || Boolean(link.finding.resolvedAt);
const status = input.state === "open" ? FindingStatus.issue_open : wasResolved ? FindingStatus.resolved : FindingStatus.suppressed;
const finding = await tx.repositoryFinding.update({ where: { id: link.findingId }, data: {
status,
...(status === FindingStatus.resolved ? { suppressedAt: null, suppressedUntil: null, suppressionReason: null } : {}),
...(status === FindingStatus.suppressed ? { suppressedAt: new Date(), suppressionReason: `Gitea issue handmatig gesloten door ${input.actor}` } : {}),
} });
if (finding.projectId) await tx.outboxEvent.create({ data: { eventId: randomUUID(), projectId: finding.projectId, aggregateType: "finding", aggregateId: finding.id, eventType: "finding.issue.webhook", payloadJson: toJson(input) } });
return finding;
});
};
export const startFindingInvestigation = async (env: AppEnv, findingId: string, actor: string) => {
const finding = await getRepositoryFinding(findingId);
const active = finding.investigations.find((investigation) => !terminalInvestigationStatuses.includes(investigation.status));
if (active) return active;
const client = getGiteaIssueClient(env);
const [repository, branches, pullRequests, actionRuns, issue, comments, root] = await Promise.all([
client.getRepository(finding.owner, finding.repository),
client.listBranches(finding.owner, finding.repository),
client.listPullRequests(finding.owner, finding.repository, "open"),
client.listActionRuns(finding.owner, finding.repository, { limit: 10 }),
finding.issueLink ? client.getIssue(finding.owner, finding.repository, finding.issueLink.issueNumber) : Promise.resolve(undefined),
finding.issueLink ? client.listIssueComments(finding.owner, finding.repository, finding.issueLink.issueNumber) : Promise.resolve([]),
client.getContents(finding.owner, finding.repository, "", finding.repositoryLink?.defaultBranch ?? repositoryDefault(finding)).catch(() => []),
]);
const rootEntries = (Array.isArray(root) ? root : [root]).slice(0, 50).map(({ content: _content, ...entry }) => entry);
const context = {
finding: { id: finding.id, title: finding.title, message: finding.message, ruleCode: finding.ruleCode, severity: finding.severity, confidence: finding.confidence, occurrenceCount: finding.occurrenceCount, firstSeenAt: finding.firstSeenAt, lastSeenAt: finding.lastSeenAt },
repository: { fullName: `${finding.owner}/${finding.repository}`, defaultBranch: repository.default_branch, archived: repository.archived, private: repository.private },
branches: branches.slice(0, 30), pullRequests: pullRequests.slice(0, 10), actionRuns: actionRuns.workflow_runs.slice(0, 10), rootEntries,
issue: issue ? { number: issue.number, title: issue.title, body: issue.body.slice(0, 10_000), state: issue.state, url: issue.html_url, comments: comments.slice(-10).map((comment) => ({ body: comment.body.slice(0, 2_000), author: comment.user?.login, createdAt: comment.created_at })) } : undefined,
};
const investigation = await prisma.$transaction(async (tx) => {
const created = await tx.findingInvestigation.create({ data: { findingId: finding.id, projectId: finding.projectId, actor, status: InvestigationStatus.ready, contextJson: toJson(context) } });
await tx.repositoryFinding.update({ where: { id: finding.id }, data: { status: FindingStatus.investigating } });
if (finding.projectId) await tx.outboxEvent.create({ data: { eventId: randomUUID(), projectId: finding.projectId, aggregateType: "investigation", aggregateId: created.id, eventType: "finding.investigation.ready", payloadJson: toJson({ findingId: finding.id, actor }) } });
return created;
});
return investigation;
};
const repositoryDefault = (finding: Awaited<ReturnType<typeof getRepositoryFinding>>) => finding.repositoryLink?.defaultBranch ?? "main";
export const getFindingInvestigation = async (investigationId: string) => {
const investigation = await prisma.findingInvestigation.findUnique({ where: { id: investigationId }, include: { finding: { include: { issueLink: true, repositoryLink: true } }, deliveryPlan: true } });
if (!investigation) throw new Error("FINDING_INVESTIGATION_NOT_FOUND");
return investigation;
};
export const saveFindingInvestigationConclusion = async (investigationId: string, input: { diagnosis: string; rootCause: string; proposedChanges: string[]; confidence: number; conclusion: string }) => {
const investigation = await getFindingInvestigation(investigationId);
if (terminalInvestigationStatuses.includes(investigation.status)) throw new Error("FINDING_INVESTIGATION_LOCKED");
return prisma.$transaction(async (tx) => {
const updated = await tx.findingInvestigation.update({ where: { id: investigation.id }, data: { status: InvestigationStatus.concluded, diagnosis: input.diagnosis, rootCause: input.rootCause, proposedChangesJson: toJson(input.proposedChanges), confidence: input.confidence, conclusion: input.conclusion, concludedAt: new Date() } });
await tx.repositoryFinding.update({ where: { id: investigation.findingId }, data: { status: FindingStatus.triaged } });
return updated;
});
};
export const createDeliveryFromFindingInvestigation = async (investigationId: string, input: FeatureDeliveryInput, actor: string) => {
const investigation = await getFindingInvestigation(investigationId);
if (!investigation.projectId) throw new Error("FINDING_PROJECT_LINK_REQUIRED");
if (investigation.deliveryPlanId) throw new Error("FINDING_DELIVERY_ALREADY_EXISTS");
if (investigation.status !== InvestigationStatus.concluded) throw new Error("FINDING_INVESTIGATION_CONCLUSION_REQUIRED");
const delivery = await createFeatureDelivery(investigation.projectId, input, actor);
const plan = await prisma.deliveryPlan.findFirstOrThrow({ where: { projectId: investigation.projectId, OR: [{ id: delivery.id }, { slug: delivery.id }] } });
await prisma.$transaction([
prisma.findingInvestigation.update({ where: { id: investigation.id }, data: { deliveryPlanId: plan.id, status: InvestigationStatus.planned } }),
prisma.repositoryFinding.update({ where: { id: investigation.findingId }, data: { status: FindingStatus.planned } }),
]);
return { delivery, investigation: await getFindingInvestigation(investigation.id) };
};
export const queueFindingVerificationForDelivery = async (env: AppEnv, projectIdOrSlug: string, deliveryId: string) => {
const project = await projectByIdOrSlug(projectIdOrSlug);
const plan = await prisma.deliveryPlan.findFirst({ where: { projectId: project.id, OR: [{ id: deliveryId }, { slug: deliveryId }] }, include: { findingInvestigation: { include: { finding: true } } } });
const investigation = plan?.findingInvestigation;
if (!plan || !investigation?.finding.repositoryLinkId) return undefined;
if (new Set<InvestigationStatus>([InvestigationStatus.verifying, InvestigationStatus.resolved, InvestigationStatus.failed, InvestigationStatus.cancelled]).has(investigation.status)) return undefined;
const result = await createPortfolioAnalysisJob(env, `system:verification:${plan.id}`, { repositoryIds: [investigation.finding.repositoryLinkId], includeBranches: true, includePullRequests: true, includeActions: true });
await prisma.$transaction([
prisma.findingInvestigation.update({ where: { id: investigation.id }, data: { status: InvestigationStatus.verifying } }),
prisma.repositoryFinding.update({ where: { id: investigation.findingId }, data: { status: FindingStatus.verifying } }),
]);
return { job: result.job, investigationId: investigation.id, findingId: investigation.findingId };
};
export const finalizeFindingVerificationForJob = async (env: AppEnv, jobId: string) => {
const items = await prisma.analysisJobItem.findMany({ where: { jobId }, include: { findingOccurrences: true } });
const completed = [];
for (const item of items) {
if (item.status !== "succeeded") continue;
const investigations = await prisma.findingInvestigation.findMany({ where: { status: InvestigationStatus.verifying, finding: { owner: item.owner, repository: item.repository } }, include: { finding: { include: { issueLink: true } }, deliveryPlan: true } });
for (const investigation of investigations) {
const observed = item.findingOccurrences.some((occurrence) => occurrence.findingId === investigation.findingId && occurrence.observed);
if (observed) {
await prisma.$transaction([
prisma.findingInvestigation.update({ where: { id: investigation.id }, data: { status: InvestigationStatus.failed, completedAt: new Date(), conclusion: "De bevinding is na de merge opnieuw waargenomen." } }),
prisma.repositoryFinding.update({ where: { id: investigation.findingId }, data: { status: FindingStatus.regressed } }),
]);
if (investigation.finding.issueLink) await getGiteaIssueClient(env).createIssueComment(item.owner, item.repository, investigation.finding.issueLink.issueNumber, "ProjectBrain-verificatie mislukt: de bevinding is na de merge opnieuw waargenomen.").catch(() => undefined);
completed.push({ findingId: investigation.findingId, resolved: false });
} else if (item.findingOccurrences.some((occurrence) => occurrence.findingId === investigation.findingId && occurrence.observed === false)) {
await prisma.$transaction([
prisma.findingInvestigation.update({ where: { id: investigation.id }, data: { status: InvestigationStatus.resolved, completedAt: new Date(), conclusion: "Gerichte heranalyse bevestigt dat de bevinding verdwenen is." } }),
prisma.repositoryFinding.update({ where: { id: investigation.findingId }, data: { status: FindingStatus.resolved, resolvedAt: new Date(), clearCount: { increment: 1 } } }),
]);
if (investigation.deliveryPlan) await markFeatureDeliveryVerified(investigation.deliveryPlan.projectId, investigation.deliveryPlan.id, "projectbrain-verifier").catch(() => undefined);
const policy = investigation.finding.projectId ? await getFindingAutomationPolicy(investigation.finding.projectId) : undefined;
if (policy?.autoCloseResolved) await closeResolvedFindingIssue(env, investigation.findingId, "Gerichte heranalyse na de merge vond de bevinding niet meer.").catch(() => undefined);
completed.push({ findingId: investigation.findingId, resolved: true });
}
}
}
return completed;
};
export const syncAutomaticFindingIssues = async (env: AppEnv, jobId: string) => {
const findings = await prisma.repositoryFinding.findMany({ where: { lastAnalysisJobId: jobId, projectId: { not: null }, repositoryLinkId: { not: null }, status: { in: [FindingStatus.new, FindingStatus.regressed, FindingStatus.triaged] } }, include: { project: { include: { findingAutomationPolicy: true } }, issueLink: true } });
const created: string[] = [];
const counts = new Map<string, number>();
for (const finding of findings) {
const policy = finding.project?.findingAutomationPolicy ?? (finding.projectId ? await getFindingAutomationPolicy(finding.projectId) : undefined);
if (!policy?.enabled || policy.issueSyncMode !== IssueSyncMode.automatic || severityRank[finding.severity] < severityRank[policy.minimumSeverity] || finding.occurrenceCount < policy.minimumOccurrences) continue;
const projectCount = counts.get(finding.projectId!) ?? 0;
if (projectCount >= policy.maxIssuesPerRun) continue;
if (finding.status === FindingStatus.regressed && finding.issueLink?.state === "closed" && !policy.autoReopenRegressed) continue;
try {
await syncFindingIssue(env, finding.id, "system:analysis-worker");
counts.set(finding.projectId!, projectCount + 1);
created.push(finding.id);
} catch (error) {
console.warn(JSON.stringify({ event: "finding_issue_sync_failed", findingId: finding.id, code: error instanceof Error ? error.message : "FINDING_ISSUE_SYNC_FAILED" }));
}
}
return created;
};
@@ -0,0 +1,406 @@
import { ApprovalState, EntityType, EvidenceStrength, FeatureLifecycle, FeatureStatus, ReviewStatus, VerificationState } from "@prisma/client";
import { prisma } from "../db/prisma.js";
import type { Initiative, Prisma } from "@prisma/client";
export type InitiativeDto = {
id: string;
projectId: string;
title: string;
intent: string;
status: "proposed" | "planned" | "active" | "complete" | "paused";
featureIds: string[];
priority: "critical" | "important" | "normal" | "low";
confidence?: number;
approvalState?: string;
sourceBrainstormId?: string;
reviewItemId?: string;
sourceType?: string;
metadata?: unknown;
provenance?: unknown;
createdAt?: string;
updatedAt?: string;
};
const slugify = (value: string) =>
value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 56) || "initiative";
const toJson = (value: unknown): Prisma.InputJsonValue => JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
export type CreateInitiativeInput = {
title: string;
intent: string;
priority?: InitiativeDto["priority"];
status?: InitiativeDto["status"];
confidence?: number;
featureIds?: string[];
sourceType?: string;
};
export type UpdateInitiativeInput = Partial<CreateInitiativeInput> & {
approvalState?: string;
};
const toStringArray = (value: unknown): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
const toStatus = (value: string): InitiativeDto["status"] => ["proposed", "planned", "active", "complete", "paused"].includes(value) ? value as InitiativeDto["status"] : "proposed";
const toPriority = (value: string): InitiativeDto["priority"] => ["critical", "important", "normal", "low"].includes(value) ? value as InitiativeDto["priority"] : "normal";
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 toDto = (projectSlug: string, initiative: Initiative): InitiativeDto => ({
id: initiative.slug ?? initiative.id,
projectId: projectSlug,
title: initiative.title,
intent: initiative.intent,
status: toStatus(initiative.status),
featureIds: toStringArray(initiative.featureIdsJson),
priority: toPriority(initiative.priority),
confidence: initiative.confidence,
approvalState: initiative.approvalState,
sourceBrainstormId: initiative.sourceBrainstormId ?? undefined,
reviewItemId: initiative.reviewItemId ?? undefined,
sourceType: initiative.sourceType,
metadata: initiative.metadataJson ?? undefined,
provenance: initiative.provenanceJson ?? undefined,
createdAt: initiative.createdAt.toISOString(),
updatedAt: initiative.updatedAt.toISOString(),
});
export const listInitiativesForProject = async (projectIdOrSlug: string): Promise<InitiativeDto[]> => {
const project = await getProject(projectIdOrSlug);
const items = await prisma.initiative.findMany({
where: { projectId: project.id, deletedAt: null },
orderBy: [{ status: "asc" }, { updatedAt: "desc" }],
});
return items.map((item) => toDto(project.slug, item));
};
export const markInitiativePlannedFromReview = async (projectId: string, initiativeId: string, reviewItemId: string): Promise<string | undefined> => {
const existing = await prisma.initiative.findFirst({
where: { projectId, deletedAt: null, OR: [{ id: initiativeId }, { slug: initiativeId }, { readableId: initiativeId }] },
});
if (!existing) return undefined;
const updated = await prisma.initiative.update({
where: { id: existing.id },
data: {
status: "planned",
approvalState: ApprovalState.approved,
reviewItemId,
updatedBy: "review_queue",
},
});
return updated.slug ?? updated.id;
};
export const getInitiativeForProject = async (projectIdOrSlug: string, initiativeId: string): Promise<InitiativeDto | undefined> => {
const project = await getProject(projectIdOrSlug);
const item = await prisma.initiative.findFirst({
where: { projectId: project.id, deletedAt: null, OR: [{ id: initiativeId }, { slug: initiativeId }, { readableId: initiativeId }] },
});
return item ? toDto(project.slug, item) : undefined;
};
export const createInitiativeForProject = async (projectIdOrSlug: string, input: CreateInitiativeInput): Promise<InitiativeDto> => {
const project = await getProject(projectIdOrSlug);
const slug = `${slugify(input.title)}-${Date.now().toString(36)}`;
const item = await prisma.initiative.create({
data: {
projectId: project.id,
slug,
readableId: slug,
title: input.title,
intent: input.intent,
priority: input.priority ?? "normal",
status: input.status ?? "proposed",
confidence: input.confidence ?? 72,
featureIdsJson: toJson(input.featureIds ?? []),
approvalState: ApprovalState.pending,
sourceType: input.sourceType ?? "manual_initiative",
metadataJson: toJson({ reviewBeforeTruth: true, createdFrom: "initiative_endpoint" }),
provenanceJson: toJson({ source: input.sourceType ?? "manual_initiative" }),
},
});
await prisma.operatingLogEntry.create({
data: {
projectId: project.id,
slug: `log-initiative-created-${slug}`,
readableId: `log-initiative-created-${slug}`,
type: "initiative created",
title: `Initiative created: ${input.title}`,
summary: "A roadmap initiative was created as a review-gated planning object.",
linkedEntityIdsJson: toJson([item.slug ?? item.id]),
source: "initiative_center",
importance: input.priority === "critical" ? "critical" : input.priority === "important" ? "important" : "normal",
payloadJson: toJson({ initiativeId: item.slug ?? item.id, status: item.status }),
provenanceJson: toJson({ source: input.sourceType ?? "manual_initiative" }),
},
});
return toDto(project.slug, item);
};
export const updateInitiativeForProject = async (projectIdOrSlug: string, initiativeId: string, input: UpdateInitiativeInput): Promise<InitiativeDto | undefined> => {
const project = await getProject(projectIdOrSlug);
const existing = await prisma.initiative.findFirst({
where: { projectId: project.id, deletedAt: null, OR: [{ id: initiativeId }, { slug: initiativeId }, { readableId: initiativeId }] },
});
if (!existing) return undefined;
const updated = await prisma.initiative.update({
where: { id: existing.id },
data: {
title: input.title,
intent: input.intent,
priority: input.priority,
status: input.status,
confidence: input.confidence,
featureIdsJson: input.featureIds ? toJson(input.featureIds) : undefined,
approvalState: input.approvalState === "approved" ? ApprovalState.approved : input.approvalState === "rejected" ? ApprovalState.rejected : undefined,
updatedBy: "initiative_endpoint",
},
});
return toDto(project.slug, updated);
};
const suggestedFeatureTitlesFor = (initiative: InitiativeDto): string[] => {
const text = `${initiative.title} ${initiative.intent}`.toLowerCase();
if (text.includes("brainstorm")) return ["Brainstorm review detail", "Outcome conversion guardrails", "Idea graph comparison panel"];
if (text.includes("blueprint") || text.includes("documentation")) return ["Blueprint acceptance workflow", "Documentation export audit", "Stale document regeneration queue"];
if (text.includes("database") || text.includes("backend")) return ["Database smoke evidence panel", "API contract regression pack", "Persistence recovery checklist"];
if (text.includes("ux") || text.includes("usability")) return ["Guided empty-state actions", "Workflow-based navigation polish", "Mobile action rail refinement"];
return ["Initiative scope brief", "Feature candidate breakdown", "Acceptance criteria draft"];
};
export type InitiativeFeatureCandidateDto = {
id: string;
title: string;
confidence: number;
reviewItemId: string;
targetRoute: string;
materializedFeatureId?: string;
};
export type InitiativeReviewSyncResultDto = {
applied: boolean;
action: "accept" | "reject";
reviewItemId: string;
initiativeId?: string;
featureId?: string;
acceptedTruthId?: string;
affectedEntities: string[];
message: string;
};
export const createFeatureCandidatesForInitiative = async (projectIdOrSlug: string, initiativeId: string): Promise<{ initiative: InitiativeDto; candidates: InitiativeFeatureCandidateDto[] } | undefined> => {
const project = await getProject(projectIdOrSlug);
const initiative = await getInitiativeForProject(projectIdOrSlug, initiativeId);
if (!initiative) return undefined;
const titles = suggestedFeatureTitlesFor(initiative);
const candidates: InitiativeFeatureCandidateDto[] = [];
for (const [index, title] of titles.entries()) {
const slug = `rq-initiative-feature-${slugify(initiative.title)}-${index}-${Date.now().toString(36)}`;
const review = await prisma.reviewQueueItem.create({
data: {
projectId: project.id,
slug,
readableId: slug,
type: "feature",
title: `Feature candidate: ${title}`,
description: `Initiative '${initiative.title}' proposes this feature candidate.\n\nIntent: ${initiative.intent}`,
priority: initiative.priority === "critical" ? "critical" : initiative.priority === "important" ? "important" : "normal",
status: ReviewStatus.pending,
source: "initiative_center",
confidence: Math.max(62, (initiative.confidence ?? 72) - index * 6),
approvalState: ApprovalState.pending,
evidenceStrength: EvidenceStrength.medium,
verificationState: VerificationState.needs_evidence,
suggestedAction: "Review whether this feature should be promoted into the Feature Map.",
createdBy: "api",
updatedBy: "api",
sourceType: "initiative_feature_candidate",
targetEntityType: undefined,
targetEntityId: initiative.id,
payloadJson: toJson({ initiativeId: initiative.id, candidateTitle: title, candidateIndex: index }),
provenanceJson: toJson({ createdFrom: "Initiative feature generation", initiativeId: initiative.id }),
},
});
candidates.push({ id: `${initiative.id}-candidate-${index}`, title, confidence: Math.max(62, (initiative.confidence ?? 72) - index * 6), reviewItemId: review.slug ?? review.id, targetRoute: `/projects/${project.slug}/review-queue` });
}
await prisma.operatingLogEntry.create({
data: {
projectId: project.id,
slug: `log-initiative-feature-candidates-${slugify(initiative.title)}-${Date.now().toString(36)}`,
readableId: `log-initiative-feature-candidates-${slugify(initiative.title)}-${Date.now().toString(36)}`,
type: "initiative planning",
title: `Feature candidates generated for ${initiative.title}`,
summary: `${candidates.length} feature candidates were generated and queued for review.`,
linkedEntityIdsJson: toJson([initiative.id, ...candidates.map((item) => item.reviewItemId)]),
source: "initiative_center",
importance: initiative.priority === "critical" ? "critical" : initiative.priority === "important" ? "important" : "normal",
payloadJson: toJson({ initiativeId: initiative.id, candidateCount: candidates.length }),
provenanceJson: toJson({ source: "initiative_feature_generation" }),
},
});
return { initiative, candidates };
};
const safeRecord = (value: unknown): Record<string, unknown> =>
typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
const getString = (value: unknown, fallback = ""): string => typeof value === "string" && value.trim() ? value : fallback;
const acceptedTruthKindForInitiativeReview = (reviewType: string) =>
reviewType === "feature" ? "feature" : reviewType;
const ensureInitiativeFeatureGroup = async (projectId: string) =>
prisma.featureGroup.upsert({
where: { projectId_slug: { projectId, slug: "initiative-roadmap" } },
create: {
projectId,
slug: "initiative-roadmap",
readableId: "initiative-roadmap",
name: "Initiative Roadmap",
description: "Features materialized from accepted roadmap initiative candidates.",
focusArea: "Roadmap",
},
update: {},
});
export const syncInitiativeReviewAction = async (
projectIdOrSlug: string,
reviewItemId: string,
action: "accept" | "reject",
): Promise<InitiativeReviewSyncResultDto | undefined> => {
const project = await getProject(projectIdOrSlug);
const review = await prisma.reviewQueueItem.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: reviewItemId }, { slug: reviewItemId }, { readableId: reviewItemId }],
},
});
if (!review || review.sourceType !== "initiative_feature_candidate") return undefined;
const payload = safeRecord(review.payloadJson);
const initiativeId = getString(payload.initiativeId, review.targetEntityId ?? "");
const candidateTitle = getString(payload.candidateTitle, review.title.replace(/^Feature candidate:\s*/i, ""));
const affectedEntities = [review.slug ?? review.id];
let featureId: string | undefined;
let acceptedTruthId: string | undefined;
if (initiativeId) affectedEntities.push(initiativeId);
if (action === "accept") {
const group = await ensureInitiativeFeatureGroup(project.id);
const featureSlug = `initiative-feature-${slugify(candidateTitle)}-${Date.now().toString(36)}`;
const feature = await prisma.feature.create({
data: {
projectId: project.id,
featureGroupId: group.id,
slug: featureSlug,
readableId: featureSlug,
name: candidateTitle,
description: review.description,
status: FeatureStatus.todo,
lifecycle: FeatureLifecycle.planned,
intent: `Materialized from accepted roadmap initiative candidate. Initiative: ${initiativeId || "unknown"}.`,
confidence: review.confidence,
health: 65,
approvalState: ApprovalState.approved,
evidenceStrength: review.evidenceStrength,
verificationState: VerificationState.needs_evidence,
trustJson: toJson({ initiativeId, reviewItemId: review.slug ?? review.id, materializedFrom: "initiative_feature_candidate" }),
provenanceJson: toJson({ source: "initiative_review_accept", initiativeId, reviewItemId: review.id, candidateTitle }),
sourceType: "initiative_review_accept",
createdBy: "review_queue",
updatedBy: "review_queue",
},
});
featureId = feature.slug ?? feature.id;
affectedEntities.push(featureId);
if (initiativeId) {
const initiative = await prisma.initiative.findFirst({
where: { projectId: project.id, deletedAt: null, OR: [{ id: initiativeId }, { slug: initiativeId }, { readableId: initiativeId }] },
});
if (initiative) {
const existingFeatureIds = toStringArray(initiative.featureIdsJson);
await prisma.initiative.update({
where: { id: initiative.id },
data: {
status: initiative.status === "proposed" ? "planned" : initiative.status,
approvalState: ApprovalState.approved,
featureIdsJson: toJson(Array.from(new Set([...existingFeatureIds, featureId]))),
updatedBy: "review_queue",
},
});
}
}
const existingTruth = await prisma.acceptedTruthItem.findFirst({ where: { projectId: project.id, sourceReviewItemId: review.id, deletedAt: null } });
if (existingTruth) acceptedTruthId = existingTruth.slug ?? existingTruth.id;
else {
const slug = `truth-initiative-feature-${slugify(candidateTitle)}-${Date.now().toString(36)}`;
const truth = await prisma.acceptedTruthItem.create({
data: {
projectId: project.id,
slug,
readableId: slug,
sourceReviewItemId: review.id,
kind: acceptedTruthKindForInitiativeReview(review.type),
title: `Accepted initiative feature: ${candidateTitle}`,
summary: review.description,
status: "accepted",
source: "initiative_center",
confidence: review.confidence,
approvalState: ApprovalState.approved,
evidenceStrength: review.evidenceStrength,
verificationState: VerificationState.verified,
payloadJson: toJson({ initiativeId, featureId, reviewItemId: review.slug ?? review.id, candidateTitle, targetRoute: `/projects/${project.slug}/features/${featureId}` }),
provenanceJson: toJson({ createdFrom: "Initiative feature candidate review acceptance", sourceType: review.sourceType }),
verifiedAt: new Date(),
verifiedBy: "api",
},
});
acceptedTruthId = truth.slug ?? truth.id;
}
if (acceptedTruthId) affectedEntities.push(acceptedTruthId);
}
await prisma.operatingLogEntry.create({
data: {
projectId: project.id,
slug: `log-initiative-review-${slugify(candidateTitle)}-${Date.now().toString(36)}`,
readableId: `log-initiative-review-${slugify(candidateTitle)}-${Date.now().toString(36)}`,
type: "initiative review sync",
title: action === "accept" ? `Initiative feature accepted: ${candidateTitle}` : `Initiative feature rejected: ${candidateTitle}`,
summary: action === "accept" ? "Accepted initiative feature candidate was materialized into the Feature Map and Accepted Truth." : "Rejected initiative feature candidate was left out of the Feature Map.",
linkedEntityIdsJson: toJson(affectedEntities),
source: "initiative_center",
importance: action === "accept" ? "important" : "normal",
payloadJson: toJson({ action, initiativeId, featureId, acceptedTruthId }),
provenanceJson: toJson({ reviewItemId: review.id, sourceType: review.sourceType }),
},
});
return {
applied: true,
action,
reviewItemId: review.slug ?? review.id,
initiativeId,
featureId,
acceptedTruthId,
affectedEntities,
message: action === "accept"
? "Initiative feature candidate accepted and materialized into Feature Map."
: "Initiative feature candidate rejected without materializing a feature.",
};
};
@@ -0,0 +1,568 @@
import { ApprovalState, type Prisma } from "@prisma/client";
import { prisma } from "../db/prisma.js";
import { listContextPacksForProject } from "./contextPacksRepository.js";
import { selectContextPacks } from "../services/contextPackSelector.js";
import type {
CodexRunImportDto,
CodexTaskDto,
CodexTaskListFilters,
CreateCodexTaskFromHandoffRequestDto,
CreateHandoffBundleRequestDto,
CurrentStateReportDto,
CurrentStateReportListFilters,
GenerateCurrentStateReportRequestDto,
HandoffBundleDto,
HandoffBundleListFilters,
HandoffTarget,
ImportCodexOutputRequestDto,
P7MutationResponseDto,
} from "../types/api.js";
type ProjectRecord = { id: string; slug: string; activeContextPackId: string | null };
type CurrentStateRecord = Prisma.CurrentStateReportGetPayload<Record<string, never>>;
type HandoffRecord = Prisma.HandoffBundleGetPayload<Record<string, never>>;
type CodexTaskRecord = Prisma.CodexTaskGetPayload<Record<string, never>>;
type CodexImportRecord = Prisma.CodexRunImportGetPayload<Record<string, never>>;
type PageOptions = { page: number; pageSize: number; sort?: string };
const toJson = (value: unknown): Prisma.InputJsonValue => JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
const asStrings = (value: Prisma.JsonValue | null | undefined): string[] =>
Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
const asRecord = (value: Prisma.JsonValue | null | undefined): Record<string, unknown> | undefined =>
typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
const publicId = (record: { id: string; slug: string | null; readableId: string | null }) => record.slug ?? record.readableId ?? record.id;
type StoredContextSelection = {
primaryContextPackId?: string;
selectedContextPackIds?: string[];
decisionSummary?: string;
candidateCount?: number;
};
const readStoredContextSelection = (value: Prisma.JsonValue | null | undefined): StoredContextSelection | undefined => {
const record = asRecord(value);
const rawSelection = asRecord(record?.contextSelection as Prisma.JsonValue | undefined);
if (!rawSelection) return undefined;
const primaryContextPackId = typeof rawSelection.primaryContextPackId === "string" ? rawSelection.primaryContextPackId : undefined;
const selectedContextPackIds = asStrings(rawSelection.selectedContextPackIds as Prisma.JsonValue | undefined);
const decisionSummary = typeof rawSelection.decisionSummary === "string" ? rawSelection.decisionSummary : undefined;
const candidateCount = typeof rawSelection.candidateCount === "number" ? rawSelection.candidateCount : undefined;
if (!primaryContextPackId && !selectedContextPackIds.length && !decisionSummary) return undefined;
return { primaryContextPackId, selectedContextPackIds, decisionSummary, candidateCount };
};
const findProjectRecord = (projectIdOrSlug: string): Promise<ProjectRecord | null> =>
prisma.project.findFirst({
where: { deletedAt: null, OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }] },
select: { id: true, slug: true, activeContextPackId: true },
});
const findHandoffRecord = (projectId: string, idOrSlug: string) =>
prisma.handoffBundle.findFirst({
where: { projectId, deletedAt: null, OR: [{ id: idOrSlug }, { slug: idOrSlug }, { readableId: idOrSlug }] },
});
const findCodexTaskRecord = (projectId: string, idOrSlug: string) =>
prisma.codexTask.findFirst({
where: { projectId, deletedAt: null, OR: [{ id: idOrSlug }, { slug: idOrSlug }, { readableId: idOrSlug }] },
});
const paginateMeta = <T>(items: T[], total: number, page: number, pageSize: number, sort?: string, filters?: Record<string, unknown>) => ({
items,
total,
page,
pageSize,
hasNextPage: page < Math.ceil(total / pageSize),
hasPreviousPage: page > 1,
...(sort ? { sort } : {}),
...(filters ? { filters: filters as Record<string, string | number | boolean | string[]> } : {}),
});
const toCurrentStateDto = (record: CurrentStateRecord, projectSlug: string): CurrentStateReportDto => ({
id: publicId(record),
projectId: projectSlug,
generatedAt: record.generatedAt.toISOString(),
exists: asStrings(record.existsJson),
works: asStrings(record.worksJson),
uncertain: asStrings(record.uncertainJson),
broken: asStrings(record.brokenJson),
currentRisks: asStrings(record.currentRisksJson),
pendingReviewItemIds: asStrings(record.pendingReviewItemIdsJson),
...(record.activeContextPackId ? { activeContextPackId: record.activeContextPackId } : {}),
recommendedNextActions: asStrings(record.recommendedNextActionsJson),
source: record.source,
approvalState: record.approvalState,
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
});
const toHandoffDto = (record: HandoffRecord, projectSlug: string): HandoffBundleDto => {
const contextSelection = readStoredContextSelection(record.payloadJson);
return {
id: publicId(record),
projectId: projectSlug,
target: record.target as HandoffTarget,
title: record.title,
markdown: record.markdown,
qualityScore: record.qualityScore,
...(contextSelection?.primaryContextPackId ? { primaryContextPackId: contextSelection.primaryContextPackId } : {}),
...(contextSelection?.selectedContextPackIds?.length ? { selectedContextPackIds: contextSelection.selectedContextPackIds } : {}),
sourceTruthItemIds: asStrings(record.sourceTruthItemIdsJson),
sourceReviewItemIds: asStrings(record.sourceReviewItemIdsJson),
sourceCaptureItemIds: asStrings(record.sourceCaptureItemIdsJson),
sourceProjectUpdateIds: asStrings(record.sourceProjectUpdateIdsJson),
sourceOperatingLogEntryIds: asStrings(record.sourceOperatingLogEntryIdsJson),
status: record.status as HandoffBundleDto["status"],
approvalState: record.approvalState,
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
};
};
const toCodexTaskDto = (record: CodexTaskRecord, projectSlug: string): CodexTaskDto => {
const contextSelection = readStoredContextSelection(record.payloadJson);
return {
id: publicId(record),
projectId: projectSlug,
...(record.handoffBundleId ? { handoffBundleId: record.handoffBundleId } : {}),
goal: record.goal,
...(record.contextPackId ? { contextPackId: record.contextPackId } : {}),
...(contextSelection?.selectedContextPackIds?.length ? { selectedContextPackIds: contextSelection.selectedContextPackIds } : {}),
...(contextSelection?.decisionSummary ? { contextSelectionSummary: contextSelection.decisionSummary } : {}),
constraints: asStrings(record.constraintsJson),
relevantFiles: asStrings(record.relevantFilesJson),
doNotBreakRules: asStrings(record.doNotBreakRulesJson),
validationCommands: asStrings(record.validationCommandsJson),
expectedReportFormat: record.expectedReportFormat,
outputImportInstructions: record.outputImportInstructions,
approvalState: record.approvalState,
status: record.status as CodexTaskDto["status"],
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
};
};
const toCodexImportDto = (record: CodexImportRecord, projectSlug: string): CodexRunImportDto => {
const contextSelection = readStoredContextSelection(record.payloadJson);
return {
id: publicId(record),
projectId: projectSlug,
...(record.codexTaskId ? { codexTaskId: record.codexTaskId } : {}),
sourceRun: record.sourceRun,
summary: record.summary,
...(contextSelection?.primaryContextPackId ? { contextPackId: contextSelection.primaryContextPackId } : {}),
...(contextSelection?.selectedContextPackIds?.length ? { selectedContextPackIds: contextSelection.selectedContextPackIds } : {}),
...(contextSelection?.decisionSummary ? { contextSelectionSummary: contextSelection.decisionSummary } : {}),
extractedReviewItemIds: asStrings(record.extractedReviewItemIdsJson),
status: record.status as CodexRunImportDto["status"],
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
};
};
const resolveRuntimeContextSelection = async (args: {
projectSlug: string;
activeContextPackId?: string | null;
target: HandoffTarget | "codex-task";
title: string;
intent?: string;
explicitContextPackId?: string;
existingSelection?: StoredContextSelection;
}): Promise<StoredContextSelection | undefined> => {
if (args.explicitContextPackId) {
return {
primaryContextPackId: args.explicitContextPackId,
selectedContextPackIds: [args.explicitContextPackId],
decisionSummary: `Explicit context pack '${args.explicitContextPackId}' was supplied by the caller.`,
candidateCount: 1,
};
}
if (args.existingSelection?.primaryContextPackId || args.existingSelection?.selectedContextPackIds?.length) {
return args.existingSelection;
}
const contextPacks = await listContextPacksForProject(args.projectSlug);
if (!contextPacks?.length) return undefined;
const mode = args.target === "codex-task" ? "codex" : args.target === "current-state" ? "validation" : "workspace";
const selection = selectContextPacks(
args.projectSlug,
contextPacks,
{
mode,
intent: args.intent?.trim() || `${args.title}. Runtime handoff should preserve accepted truth, review discipline and current project constraints.`,
targetType: args.target === "codex-task" ? "build" : "project",
maxPacks: 3,
},
args.activeContextPackId ?? undefined,
);
return {
primaryContextPackId: selection.primaryContextPackId,
selectedContextPackIds: selection.selectedContextPackIds,
decisionSummary: selection.decisionSummary,
candidateCount: selection.candidates.length,
};
};
const mkSlug = (prefix: string, seed: string) => `${prefix}-${seed.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 42)}-${Date.now().toString(36)}`;
export const listCurrentStateReportsFromDatabase = async (projectIdOrSlug: string, filters: CurrentStateReportListFilters, options: PageOptions) => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const where: Prisma.CurrentStateReportWhereInput = {
projectId: project.id,
deletedAt: null,
...(filters.approvalState?.length ? { approvalState: { in: filters.approvalState } } : {}),
...(filters.source?.length ? { source: { in: filters.source } } : {}),
};
const total = await prisma.currentStateReport.count({ where });
const records = await prisma.currentStateReport.findMany({
where,
orderBy: { generatedAt: options.sort?.endsWith(":asc") ? "asc" : "desc" },
skip: (options.page - 1) * options.pageSize,
take: options.pageSize,
});
return paginateMeta(records.map((record) => toCurrentStateDto(record, project.slug)), total, options.page, options.pageSize, options.sort, filters);
};
export const generateCurrentStateReportInDatabase = async (
projectIdOrSlug: string,
_body: GenerateCurrentStateReportRequestDto,
): Promise<CurrentStateReportDto | undefined> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const [truth, updates, review, logs, contextPack] = await Promise.all([
prisma.acceptedTruthItem.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { createdAt: "desc" }, take: 8 }),
prisma.projectUpdate.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { createdAt: "desc" }, take: 8 }),
prisma.reviewQueueItem.findMany({ where: { projectId: project.id, deletedAt: null, status: "pending" }, orderBy: { createdAt: "desc" }, take: 12 }),
prisma.operatingLogEntry.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { timestamp: "desc" }, take: 8 }),
prisma.contextPack.findFirst({ where: { projectId: project.id, deletedAt: null }, orderBy: { updatedAt: "desc" } }),
]);
const exists = ["Project", "Workspace", "Project DNA", "Feature/Evidence DB parity", "Capture/Review DB parity", "Accepted Truth/Project Updates/Operating Log DB parity"];
const works = [
...truth.slice(0, 3).map((item) => `Accepted truth: ${item.title}`),
...updates.slice(0, 3).map((item) => `Project update ${item.status}: ${item.title}`),
...logs.slice(0, 2).map((item) => `Operating log: ${item.title}`),
];
const uncertain = review.length ? [`${review.length} pending Review Queue item(s) still require approval.`] : [];
const broken = updates.filter((item) => item.status !== "applied").length ? ["Some project updates are not applied yet."] : [];
const currentRisks = [
"Generated reports remain reviewable artifacts; they do not mutate project truth silently.",
...(review.length ? ["Pending review items may hide unaccepted build or product facts."] : []),
];
const recommendedNextActions = [
updates.some((item) => item.status !== "applied") ? "Apply ready project updates after review." : "Prepare the next context handoff from accepted truth.",
"Create a Codex handoff bundle from the latest accepted truth and current state.",
"Import Codex/build output through Capture and Review Queue, not directly into truth.",
];
const record = await prisma.currentStateReport.create({
data: {
projectId: project.id,
slug: mkSlug("current-state", project.slug),
readableId: mkSlug("csr", project.slug),
existsJson: toJson(exists),
worksJson: toJson(works.length ? works : ["Backend persistence gates up to P6 are available."]),
uncertainJson: toJson(uncertain),
brokenJson: toJson(broken),
currentRisksJson: toJson(currentRisks),
pendingReviewItemIdsJson: toJson(review.map((item) => item.slug ?? item.readableId ?? item.id)),
activeContextPackId: project.activeContextPackId ?? contextPack?.slug ?? contextPack?.readableId ?? contextPack?.id,
recommendedNextActionsJson: toJson(recommendedNextActions),
source: "database-runtime-generator",
approvalState: ApprovalState.pending,
provenanceJson: toJson({ createdFrom: "P7 current state generator" }),
},
});
await prisma.operatingLogEntry.create({
data: {
projectId: project.id,
slug: mkSlug("log-current-state", project.slug),
readableId: mkSlug("log", project.slug),
type: "build summary",
title: "Current State report generated",
summary: `Generated ${publicId(record)} from accepted truth, project updates and operating log.`,
linkedEntityIdsJson: toJson([record.id]),
source: "p7-current-state",
importance: "important",
},
});
return toCurrentStateDto(record, project.slug);
};
const markdownForHandoff = (args: {
projectSlug: string;
target: HandoffTarget;
title: string;
truth: Array<{ id: string; slug: string | null; readableId: string | null; title: string; kind: string; summary: string }>;
updates: Array<{ id: string; slug: string | null; readableId: string | null; title: string; targetArea: string; status: string; summary: string }>;
logs: Array<{ id: string; slug: string | null; readableId: string | null; title: string; type: string; summary: string }>;
contextSelection?: StoredContextSelection;
}) => [
`# ${args.title}`,
"",
`Project: ${args.projectSlug}`,
`Target: ${args.target}`,
`Generated: ${new Date().toISOString()}`,
"",
"## Automatic context pack selection",
args.contextSelection?.primaryContextPackId ? `Primary pack: ${args.contextSelection.primaryContextPackId}` : "Primary pack: none",
args.contextSelection?.selectedContextPackIds?.length ? `Selected packs: ${args.contextSelection.selectedContextPackIds.join(", ")}` : "Selected packs: none",
args.contextSelection?.decisionSummary ? `Decision: ${args.contextSelection.decisionSummary}` : "Decision: no selector result attached",
typeof args.contextSelection?.candidateCount === "number" ? `Candidates considered: ${args.contextSelection.candidateCount}` : "Candidates considered: 0",
"",
"## Accepted truth",
...(args.truth.length ? args.truth.map((item) => `- [${item.kind}] ${item.title}: ${item.summary}`) : ["- None yet."]),
"",
"## Project updates",
...(args.updates.length ? args.updates.map((item) => `- [${item.status}] [${item.targetArea}] ${item.title}: ${item.summary}`) : ["- None yet."]),
"",
"## Operating log context",
...(args.logs.length ? args.logs.map((item) => `- [${item.type}] ${item.title}: ${item.summary}`) : ["- None yet."]),
"",
"## Guardrails",
"- Do not mutate project truth directly from AI/build output.",
"- Route output back through Capture and Review Queue.",
"- Preserve existing API contracts and fixture mode unless explicitly changing a persistence gate.",
].join("\n");
export const listHandoffBundlesFromDatabase = async (projectIdOrSlug: string, filters: HandoffBundleListFilters, options: PageOptions) => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const where: Prisma.HandoffBundleWhereInput = {
projectId: project.id,
deletedAt: null,
...(filters.target?.length ? { target: { in: filters.target } } : {}),
...(filters.status?.length ? { status: { in: filters.status } } : {}),
...(filters.approvalState?.length ? { approvalState: { in: filters.approvalState } } : {}),
};
const total = await prisma.handoffBundle.count({ where });
const records = await prisma.handoffBundle.findMany({ where, orderBy: { createdAt: "desc" }, skip: (options.page - 1) * options.pageSize, take: options.pageSize });
return paginateMeta(records.map((record) => toHandoffDto(record, project.slug)), total, options.page, options.pageSize, options.sort, filters);
};
export const createHandoffBundleInDatabase = async (projectIdOrSlug: string, body: CreateHandoffBundleRequestDto): Promise<HandoffBundleDto | undefined> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const [truth, updates, review, captures, logs] = await Promise.all([
prisma.acceptedTruthItem.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { createdAt: "desc" }, take: 12 }),
prisma.projectUpdate.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { createdAt: "desc" }, take: 12 }),
prisma.reviewQueueItem.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { createdAt: "desc" }, take: 8 }),
prisma.captureItem.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { createdAt: "desc" }, take: 8 }),
prisma.operatingLogEntry.findMany({ where: { projectId: project.id, deletedAt: null }, orderBy: { timestamp: "desc" }, take: 8 }),
]);
const target = body.target ?? "codex-task";
const title = body.title?.trim() || `${target} handoff for ${project.slug}`;
const contextSelection = await resolveRuntimeContextSelection({
projectSlug: project.slug,
activeContextPackId: project.activeContextPackId,
target,
title,
intent: body.selectionIntent,
explicitContextPackId: body.contextPackId,
});
const qualityScore = Math.min(98, 45 + truth.length * 10 + updates.length * 5 + logs.length * 2 + (contextSelection?.primaryContextPackId ? 6 : 0));
const markdown = markdownForHandoff({ projectSlug: project.slug, target, title, truth, updates, logs, contextSelection });
const record = await prisma.handoffBundle.create({
data: {
projectId: project.id,
slug: mkSlug("handoff", title),
readableId: mkSlug("hb", project.slug),
target,
title,
markdown,
qualityScore,
sourceTruthItemIdsJson: toJson(truth.map(publicId)),
sourceReviewItemIdsJson: toJson(review.map(publicId)),
sourceCaptureItemIdsJson: toJson(captures.map(publicId)),
sourceProjectUpdateIdsJson: toJson(updates.map(publicId)),
sourceOperatingLogEntryIdsJson: toJson(logs.map(publicId)),
status: "ready",
approvalState: ApprovalState.pending,
payloadJson: toJson({ contextSelection }),
provenanceJson: toJson({ createdFrom: "P7 handoff bundle generator", contextSelectionSummary: contextSelection?.decisionSummary }),
},
});
await prisma.operatingLogEntry.create({
data: {
projectId: project.id,
slug: mkSlug("log-handoff", project.slug),
readableId: mkSlug("log", project.slug),
type: "project update",
title: "Handoff bundle created",
summary: `${title} created for ${target} with ${qualityScore}% quality.`,
linkedEntityIdsJson: toJson([record.id]),
source: "p7-handoff",
importance: qualityScore >= 75 ? "important" : "normal",
},
});
return toHandoffDto(record, project.slug);
};
export const listCodexTasksFromDatabase = async (projectIdOrSlug: string, filters: CodexTaskListFilters, options: PageOptions) => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const handoffFilter = filters.handoffBundleId ? await findHandoffRecord(project.id, filters.handoffBundleId) : undefined;
if (filters.handoffBundleId && !handoffFilter) {
return paginateMeta([], 0, options.page, options.pageSize, options.sort, filters);
}
const where: Prisma.CodexTaskWhereInput = {
projectId: project.id,
deletedAt: null,
...(filters.status?.length ? { status: { in: filters.status } } : {}),
...(filters.approvalState?.length ? { approvalState: { in: filters.approvalState } } : {}),
...(handoffFilter ? { handoffBundleId: handoffFilter.id } : {}),
};
const total = await prisma.codexTask.count({ where });
const records = await prisma.codexTask.findMany({ where, orderBy: { createdAt: "desc" }, skip: (options.page - 1) * options.pageSize, take: options.pageSize });
return paginateMeta(records.map((record) => toCodexTaskDto(record, project.slug)), total, options.page, options.pageSize, options.sort, filters);
};
export const createCodexTaskFromHandoffInDatabase = async (
projectIdOrSlug: string,
handoffBundleId: string,
body: CreateCodexTaskFromHandoffRequestDto,
): Promise<CodexTaskDto | undefined | "HANDOFF_NOT_FOUND"> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const handoff = await findHandoffRecord(project.id, handoffBundleId);
if (!handoff) return "HANDOFF_NOT_FOUND";
const goal = body.goal?.trim() || `Implement ${handoff.title}`;
const existingSelection = readStoredContextSelection(handoff.payloadJson);
const contextSelection = body.autoSelectContext === false
? await resolveRuntimeContextSelection({
projectSlug: project.slug,
activeContextPackId: project.activeContextPackId,
target: "codex-task",
title: goal,
explicitContextPackId: body.contextPackId ?? existingSelection?.primaryContextPackId ?? project.activeContextPackId ?? undefined,
existingSelection,
})
: await resolveRuntimeContextSelection({
projectSlug: project.slug,
activeContextPackId: project.activeContextPackId,
target: "codex-task",
title: goal,
intent: body.selectionIntent ?? `${goal}. ${handoff.title}.`,
explicitContextPackId: body.contextPackId,
existingSelection,
});
const selectedContextPackId = body.contextPackId ?? contextSelection?.primaryContextPackId ?? project.activeContextPackId ?? undefined;
const record = await prisma.codexTask.create({
data: {
projectId: project.id,
slug: mkSlug("codex-task", goal),
readableId: mkSlug("ct", project.slug),
handoffBundleId: handoff.id,
goal,
contextPackId: selectedContextPackId,
constraintsJson: toJson(["Preserve fixture mode.", "Do not write AI output directly into project truth.", "Keep review-before-truth semantics."]),
relevantFilesJson: toJson(["backend/src/routes", "backend/src/repositories", "src/features/operations", "docs"]),
doNotBreakRulesJson: toJson(["No Vite middleware expansion.", "No frontend default switch to real mode.", "No silent truth mutation."]),
validationCommandsJson: toJson(body.validationCommands?.length ? body.validationCommands : ["pnpm run validate:all", "pnpm run audit:runtime", "pnpm run audit:release"]),
expectedReportFormat: "Changed files, validation commands, remaining warnings, smoke status and next checklist.",
outputImportInstructions: "Import Codex/build output as Capture, extract Review Queue items, and only materialize approved truth after review.",
approvalState: ApprovalState.pending,
status: "ready",
payloadJson: toJson({ contextSelection }),
provenanceJson: toJson({ handoffBundleId: publicId(handoff), createdFrom: "P7 handoff", contextSelectionSummary: contextSelection?.decisionSummary }),
},
});
await prisma.handoffBundle.update({ where: { id: handoff.id }, data: { status: "used" } });
await prisma.operatingLogEntry.create({
data: {
projectId: project.id,
slug: mkSlug("log-codex-task", project.slug),
readableId: mkSlug("log", project.slug),
type: "codex run",
title: "Codex task created from handoff",
summary: `${goal} was created from ${handoff.title}.${selectedContextPackId ? ` Context pack: ${selectedContextPackId}.` : ""}`,
linkedEntityIdsJson: toJson([handoff.id, record.id]),
source: "p7-codex-task",
importance: "important",
},
});
return toCodexTaskDto(record, project.slug);
};
export const importCodexOutputInDatabase = async (
projectIdOrSlug: string,
codexTaskId: string,
body: ImportCodexOutputRequestDto,
): Promise<{ importDto: CodexRunImportDto; mutation: P7MutationResponseDto } | undefined | "CODEX_TASK_NOT_FOUND"> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const task = await findCodexTaskRecord(project.id, codexTaskId);
if (!task) return "CODEX_TASK_NOT_FOUND";
const contextSelection = readStoredContextSelection(task.payloadJson) ?? (task.contextPackId
? {
primaryContextPackId: task.contextPackId,
selectedContextPackIds: [task.contextPackId],
decisionSummary: `Imported output inherits Codex task context pack '${task.contextPackId}'.`,
candidateCount: 1,
}
: undefined);
const review = await prisma.reviewQueueItem.create({
data: {
projectId: project.id,
slug: mkSlug("review-codex-output", project.slug),
readableId: mkSlug("rq", project.slug),
type: "documentation_update",
title: `Review Codex output for ${task.goal}`,
description: body.summary,
priority: "important",
status: "pending",
source: body.sourceRun ?? task.slug ?? task.id,
confidence: 72,
approvalState: ApprovalState.pending,
evidenceStrength: "medium",
verificationState: "unverified",
suggestedAction: "Review generated changes, then accept only verified feature, documentation, memory or regression updates.",
createdBy: "api",
updatedBy: "api",
sourceType: "codex_output_import",
payloadJson: toJson({ outputText: body.outputText ?? "", codexTaskId: task.id, contextSelection }),
provenanceJson: toJson({ createdFrom: "P7 codex import", contextSelectionSummary: contextSelection?.decisionSummary }),
},
});
const record = await prisma.codexRunImport.create({
data: {
projectId: project.id,
slug: mkSlug("codex-import", project.slug),
readableId: mkSlug("cri", project.slug),
codexTaskId: task.id,
sourceRun: body.sourceRun ?? `manual-${Date.now().toString(36)}`,
summary: body.summary,
extractedReviewItemIdsJson: toJson([publicId(review)]),
status: "imported",
payloadJson: toJson({ outputText: body.outputText ?? "", contextSelection }),
provenanceJson: toJson({ createdFrom: "P7 codex output import", contextSelectionSummary: contextSelection?.decisionSummary }),
},
});
await prisma.codexTask.update({ where: { id: task.id }, data: { status: "imported" } });
await prisma.operatingLogEntry.create({
data: {
projectId: project.id,
slug: mkSlug("log-codex-import", project.slug),
readableId: mkSlug("log", project.slug),
type: "codex run",
title: "Codex output imported to Review Queue",
summary: `${body.summary}${contextSelection?.primaryContextPackId ? ` Context pack: ${contextSelection.primaryContextPackId}.` : ""}`,
linkedEntityIdsJson: toJson([task.id, record.id, review.id]),
source: "p7-codex-import",
importance: "important",
},
});
return {
importDto: toCodexImportDto(record, project.slug),
mutation: {
success: true,
message: "Codex output imported as a Review Queue item.",
createdEntityId: publicId(review),
updatedEntityId: publicId(task),
affectedEntities: [publicId(task), publicId(record), publicId(review)],
},
};
};
@@ -0,0 +1,270 @@
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<string, unknown> => 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<boolean> => {
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<string, unknown> => 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<KnowledgeItemDto[]> => {
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<KnowledgeItemDto | undefined> => {
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<KnowledgeItemDto> => {
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<KnowledgeItemDto | undefined> => {
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<KnowledgeItemDto | undefined> => {
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<MemoryRuleDto[]> => {
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<MemoryRuleDto | undefined> => {
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<MemoryRuleDto> => {
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<MemoryRuleDto | undefined> => {
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<MemoryRuleDto | undefined> => {
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<DocumentationItemDto[]> => {
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<DocumentationItemDto | undefined> => {
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<DocumentationItemDto> => {
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<DocumentationItemDto | undefined> => {
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);
};
@@ -0,0 +1,50 @@
const persistedSetupKeys = [
"activeContextPackId",
"featureGroups",
"memoryRules",
"promptTemplates",
"constraints",
"principles",
"firstRun",
"starterContextPacks",
"starterDocumentationItems",
"starterReviewItems",
"starterOperatingLog",
"brainstorms",
"codexSkills",
"disabledCodexSkillIds",
"workspaceState",
] as const;
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
export const sanitizeProjectSetupState = (value: unknown): Record<string, unknown> => {
if (!isRecord(value)) return {};
return Object.fromEntries(
persistedSetupKeys.flatMap((key) =>
Object.prototype.hasOwnProperty.call(value, key) ? [[key, value[key]]] : [],
),
);
};
export const projectSetupWorkspaceState = (value: unknown): Record<string, unknown> | undefined => {
const setup = sanitizeProjectSetupState(value);
return isRecord(setup.workspaceState) ? setup.workspaceState : undefined;
};
export const projectSetupActiveContextPackId = (value: unknown): string | undefined => {
const setup = sanitizeProjectSetupState(value);
return typeof setup.activeContextPackId === "string" && setup.activeContextPackId.trim()
? setup.activeContextPackId
: undefined;
};
export const mergeProjectSetupState = <T extends Record<string, unknown>>(
setupState: unknown,
canonical: T,
): T => ({
...sanitizeProjectSetupState(setupState),
...canonical,
}) as T;
@@ -0,0 +1,514 @@
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<string, unknown>;
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
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<ProjectWithP2Relations, "name" | "description">) =>
/\b(smoke|disposable|test project|proof project)\b/i.test(`${project.name} ${project.description}`);
const toDna = (
dna: NonNullable<ProjectWithP2Relations["dna"]>,
): 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<string, unknown>);
};
const defaultDnaForProject = (
input: CreateProjectRequestDto,
): Omit<ProjectDna, "updatedAt"> => ({
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<ProjectSummaryDto[]> => {
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<ProjectDetailDto> => {
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<ProjectDetailDto | undefined> => {
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<WorkspaceReadDto | undefined> => {
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<ProjectDetailDto | undefined> => {
const project = await findProjectRecordBySlugOrId(projectIdOrSlug);
if (!project) return undefined;
return toProjectDetail(project);
};
export const getProjectDNA = async (
projectIdOrSlug: string,
): Promise<ProjectDna | undefined> => {
const project = await findProjectRecordBySlugOrId(projectIdOrSlug);
if (!project?.dna) return undefined;
return toDna(project.dna);
};
export const getProjectWorkspace = async (
projectIdOrSlug: string,
): Promise<WorkspaceReadDto | undefined> => {
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,
},
};
};
@@ -0,0 +1,211 @@
import {
ApprovalState,
EntityType,
EvidenceStrength,
VerificationState,
type Prisma,
} from "@prisma/client";
import { prisma } from "../db/prisma.js";
import type {
ApprovalState as ApprovalStateDto,
CreateReleaseEvidenceRecordRequestDto,
EvidenceStrength as EvidenceStrengthDto,
ReleaseEvidenceKind,
ReleaseEvidenceListFilters,
ReleaseEvidenceRecordDto,
ReleaseEvidenceStatus,
VerificationState as VerificationStateDto,
} from "../types/api.js";
type ProjectRecord = { id: string; slug: string };
type ReleaseEvidenceRecord = Prisma.ReleaseEvidenceRecordGetPayload<Record<string, never>>;
const findProjectRecord = (projectIdOrSlug: string): Promise<ProjectRecord | null> =>
prisma.project.findFirst({
where: {
deletedAt: null,
OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }],
},
select: { id: true, slug: true },
});
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 toVerificationStateEnum = (
value: VerificationStateDto | string | null | undefined,
): VerificationState => {
if (value === "blocked") return VerificationState.blocked;
if (value === "needs evidence" || value === "needs_evidence") return VerificationState.needs_evidence;
if (value === "unverified") return VerificationState.unverified;
return VerificationState.verified;
};
const toVerificationStateDto = (value: VerificationState): VerificationStateDto =>
value === VerificationState.needs_evidence ? "needs evidence" : value;
const toDto = (record: ReleaseEvidenceRecord, projectSlug: string): ReleaseEvidenceRecordDto => ({
id: record.slug ?? record.readableId ?? record.id,
projectId: projectSlug,
capturedAt: record.capturedAt.toISOString(),
kind: record.kind as ReleaseEvidenceKind,
title: record.title,
summary: record.summary,
status: record.status as ReleaseEvidenceStatus,
...(record.command ? { command: record.command } : {}),
...(record.outputText ? { outputText: record.outputText } : {}),
...(record.route ? { route: record.route } : {}),
source: record.source,
confidence: record.confidence,
approvalState: record.approvalState as ApprovalStateDto,
evidenceStrength: record.evidenceStrength,
verificationState: toVerificationStateDto(record.verificationState),
createdBy: record.createdBy,
...(record.verifiedAt ? { verifiedAt: record.verifiedAt.toISOString() } : {}),
...(record.verifiedBy ? { verifiedBy: record.verifiedBy } : {}),
...(record.metadataJson && typeof record.metadataJson === "object" && !Array.isArray(record.metadataJson)
? { metadata: record.metadataJson as Record<string, unknown> }
: {}),
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
});
const paginateMeta = <T>(
items: T[],
total: number,
page: number,
pageSize: number,
sort?: string,
filters?: Record<string, unknown>,
) => ({
items,
total,
page,
pageSize,
hasNextPage: page < Math.ceil(total / pageSize),
hasPreviousPage: page > 1,
...(sort ? { sort } : {}),
...(filters ? { filters: filters as Record<string, string | number | boolean | string[]> } : {}),
});
export const listReleaseEvidenceRecordsFromDatabase = async (
projectIdOrSlug: string,
filters: ReleaseEvidenceListFilters,
options: { page: number; pageSize: number; sort?: string },
) => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const where: Prisma.ReleaseEvidenceRecordWhereInput = {
projectId: project.id,
deletedAt: null,
...(filters.kind?.length ? { kind: { in: filters.kind } } : {}),
...(filters.status?.length ? { status: { in: filters.status } } : {}),
...(filters.evidenceStrength?.length
? { evidenceStrength: { in: filters.evidenceStrength.map(toEvidenceStrengthEnum) } }
: {}),
...(filters.verificationState?.length
? { verificationState: { in: filters.verificationState.map(toVerificationStateEnum) } }
: {}),
...(filters.source?.length ? { source: { in: filters.source } } : {}),
...(filters.createdBy?.length ? { createdBy: { in: filters.createdBy } } : {}),
};
const total = await prisma.releaseEvidenceRecord.count({ where });
const records = await prisma.releaseEvidenceRecord.findMany({
where,
orderBy: { capturedAt: options.sort?.endsWith(":asc") ? "asc" : "desc" },
skip: (options.page - 1) * options.pageSize,
take: options.pageSize,
});
return paginateMeta(
records.map((record) => toDto(record, project.slug)),
total,
options.page,
options.pageSize,
options.sort,
filters,
);
};
export const getReleaseEvidenceRecordFromDatabase = async (
projectIdOrSlug: string,
releaseEvidenceId: string,
) => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const record = await prisma.releaseEvidenceRecord.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [
{ id: releaseEvidenceId },
{ slug: releaseEvidenceId },
{ readableId: releaseEvidenceId },
],
},
});
if (!record) return "RELEASE_EVIDENCE_NOT_FOUND" as const;
return toDto(record, project.slug);
};
export const createReleaseEvidenceRecordInDatabase = async (
projectIdOrSlug: string,
body: CreateReleaseEvidenceRecordRequestDto,
): Promise<ReleaseEvidenceRecordDto | undefined> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const capturedAt = new Date();
const slug = `rel-${project.slug}-${capturedAt.getTime()}-${Math.random().toString(36).slice(2, 8)}`;
const verificationState = toVerificationStateEnum(body.verificationState ?? "verified");
const evidenceStrength = toEvidenceStrengthEnum(body.evidenceStrength ?? "verified");
const verifiedAt = verificationState === VerificationState.verified ? capturedAt : undefined;
const record = await prisma.releaseEvidenceRecord.create({
data: {
projectId: project.id,
slug,
readableId: slug,
capturedAt,
kind: body.kind,
title: body.title,
summary: body.summary,
status: body.status ?? "captured",
command: body.command,
outputText: body.outputText,
route: body.route,
source: body.source ?? "runtime_readiness",
confidence: body.confidence ?? 100,
approvalState: ApprovalState.approved,
evidenceStrength,
verificationState,
metadataJson: (body.metadata ?? {}) as Prisma.InputJsonValue,
provenanceJson: {
createdFrom: "release-evidence-api",
sourceType: "runtime_readiness",
},
createdBy: body.createdBy ?? "api",
verifiedAt,
verifiedBy: body.verifiedBy ?? (verifiedAt ? "api" : undefined),
},
});
await prisma.activityEvent.create({
data: {
projectId: project.id,
entityType: EntityType.release_evidence_record,
entityId: record.id,
eventType: "release_evidence_captured",
actor: body.createdBy ?? "api",
payloadJson: {
kind: record.kind,
title: record.title,
status: record.status,
route: record.route,
},
},
});
return toDto(record, project.slug);
};
@@ -0,0 +1,22 @@
import { ReviewStatus, type Prisma, type ReviewQueueItem } from "@prisma/client";
type ReviewAttentionRecord = Pick<ReviewQueueItem, "status" | "source" | "sourceType" | "createdBy">;
const isSyntheticSource = (value: string | null | undefined) => value?.toLowerCase().includes("smoke") ?? false;
/** Pending test and seed artefacts are evidence, not owner decisions. */
export const reviewRequiresHumanAttention = (record: ReviewAttentionRecord) =>
record.status === ReviewStatus.pending &&
record.createdBy !== "seed" &&
!isSyntheticSource(record.source) &&
!isSyntheticSource(record.sourceType);
export const humanAttentionReviewWhere = {
deletedAt: null,
status: ReviewStatus.pending,
NOT: [
{ createdBy: "seed" },
{ source: { contains: "smoke", mode: "insensitive" } },
{ sourceType: { contains: "smoke", mode: "insensitive" } },
],
} satisfies Prisma.ReviewQueueItemWhereInput;
@@ -0,0 +1,448 @@
import type { Prisma } from "@prisma/client";
import { ApprovalState, EntityType, EvidenceStrength, ReviewStatus, VerificationState } from "@prisma/client";
import { prisma } from "../db/prisma.js";
import type {
CaptureItemDto,
ReviewMutationResponseDto,
ReviewQueueConvertTarget,
ReviewQueueItemDto,
ReviewQueueListFilters,
ReviewQueuePriority,
ReviewQueueStatus,
UpdateReviewQueueItemRequestDto,
} from "../types/api.js";
import { reviewRequiresHumanAttention } from "./reviewAttentionPolicy.js";
type ReviewRecord = Prisma.ReviewQueueItemGetPayload<Record<string, never>>;
type ProjectRecord = { id: string; slug: string };
const findProjectRecord = (projectIdOrSlug: string): Promise<ProjectRecord | null> =>
prisma.project.findFirst({
where: {
deletedAt: null,
OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }],
},
select: { id: true, slug: true },
});
const mapEvidenceStrength = (value: string): EvidenceStrength => {
if (value === "verified") return EvidenceStrength.verified;
if (value === "strong") return EvidenceStrength.strong;
if (value === "medium") return EvidenceStrength.medium;
return EvidenceStrength.weak;
};
const mapVerificationState = (value: string): VerificationState => {
if (value === "verified") return VerificationState.verified;
if (value === "blocked") return VerificationState.blocked;
if (value === "needs evidence") return VerificationState.needs_evidence;
return VerificationState.unverified;
};
const mapReviewStatus = (value: ReviewQueueStatus): ReviewStatus => {
if (value === "accepted") return ReviewStatus.accepted;
if (value === "rejected") return ReviewStatus.rejected;
if (value === "merged") return ReviewStatus.merged;
if (value === "converted") return ReviewStatus.converted;
return ReviewStatus.pending;
};
const toReviewStatus = (value: ReviewStatus): ReviewQueueStatus => {
if (value === ReviewStatus.accepted) return "accepted";
if (value === ReviewStatus.rejected) return "rejected";
if (value === ReviewStatus.merged) return "merged";
if (value === ReviewStatus.converted) return "converted";
return "pending";
};
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
const toStringArray = (value: unknown): string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
const toBlueprintReviewMetadata = (record: ReviewRecord): ReviewQueueItemDto["blueprint"] | undefined => {
const payload = isRecord(record.payloadJson) ? record.payloadJson : {};
const provenance = isRecord(record.provenanceJson) ? record.provenanceJson : {};
const contextSelection = isRecord(payload.contextSelection) ? payload.contextSelection : {};
const graphId = typeof payload.graphId === "string" ? payload.graphId : typeof provenance.graphId === "string" ? provenance.graphId : undefined;
const nodeId = typeof payload.nodeId === "string" ? payload.nodeId : typeof provenance.nodeId === "string" ? provenance.nodeId : undefined;
if (!graphId && !nodeId && record.source !== "blueprint_engine_v52") return undefined;
return {
graphId,
nodeId,
outputPath: typeof payload.outputPath === "string" ? payload.outputPath : undefined,
contextPackId: typeof contextSelection.primaryContextPackId === "string" ? contextSelection.primaryContextPackId : undefined,
selectedContextPackIds: toStringArray(contextSelection.selectedContextPackIds),
};
};
const toReviewDto = (record: ReviewRecord, projectSlug: string): ReviewQueueItemDto & { attentionRequired: boolean; sourceType: string } => ({
id: record.slug ?? record.readableId ?? record.id,
projectId: projectSlug,
type: record.type as ReviewQueueItemDto["type"],
title: record.title,
description: record.description,
priority: record.priority as ReviewQueuePriority,
status: toReviewStatus(record.status),
source: record.source,
confidence: record.confidence,
evidenceStrength: record.evidenceStrength,
verificationState:
record.verificationState === VerificationState.needs_evidence
? "needs evidence"
: (record.verificationState as ReviewQueueItemDto["verificationState"]),
suggestedAction: record.suggestedAction,
attentionRequired: reviewRequiresHumanAttention(record),
sourceType: record.sourceType,
...(record.notes ? { notes: record.notes } : {}),
...(record.targetEntityId ? { targetEntityId: record.targetEntityId } : {}),
...(isRecord(record.payloadJson) && typeof record.payloadJson.convertedTarget === "string"
? { convertedTarget: record.payloadJson.convertedTarget as ReviewQueueConvertTarget }
: {}),
...(record.createdEntityId ? { createdEntityId: record.createdEntityId } : {}),
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
...(toBlueprintReviewMetadata(record) ? { blueprint: toBlueprintReviewMetadata(record) } : {}),
});
const toWhere = (
projectId: string,
filters: ReviewQueueListFilters = {},
): Prisma.ReviewQueueItemWhereInput => ({
projectId,
deletedAt: null,
...(filters.priority?.length ? { priority: { in: filters.priority } } : {}),
...(filters.type?.length ? { type: { in: filters.type } } : {}),
...(filters.status?.length
? { status: { in: filters.status.map(mapReviewStatus) } }
: {}),
...(filters.source?.length ? { source: { in: filters.source } } : {}),
...(filters.confidence !== undefined ? { confidence: filters.confidence } : {}),
...(filters.evidenceStrength?.length
? { evidenceStrength: { in: filters.evidenceStrength.map(mapEvidenceStrength) } }
: {}),
...(filters.verificationState?.length
? { verificationState: { in: filters.verificationState.map(mapVerificationState) } }
: {}),
});
export const listReviewItemsFromDatabase = async (
projectIdOrSlug: string,
filters: ReviewQueueListFilters,
options: { page: number; pageSize: number; cursor?: string; sort?: string },
) => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const where = toWhere(project.id, filters);
const total = await prisma.reviewQueueItem.count({ where });
const records = await prisma.reviewQueueItem.findMany({
where,
orderBy: { createdAt: options.sort?.endsWith(":asc") ? "asc" : "desc" },
skip: (options.page - 1) * options.pageSize,
take: options.pageSize,
});
const pageCount = Math.ceil(total / options.pageSize);
return {
items: records.map((record) => toReviewDto(record, project.slug)),
total,
page: options.page,
pageSize: options.pageSize,
hasNextPage: options.page < pageCount,
hasPreviousPage: options.page > 1,
...(options.sort ? { sort: options.sort } : {}),
...(options.cursor ? { nextCursor: undefined, cursor: options.cursor } : {}),
filters,
};
};
export const getReviewItemFromDatabase = async (
projectIdOrSlug: string,
reviewItemId: string,
): Promise<ReviewQueueItemDto | undefined> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const record = await prisma.reviewQueueItem.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: reviewItemId }, { slug: reviewItemId }, { readableId: reviewItemId }],
},
});
return record ? toReviewDto(record, project.slug) : undefined;
};
const toReviewType = (
classificationType: NonNullable<CaptureItemDto["classificationType"]>,
): ReviewQueueItemDto["type"] => {
if (classificationType === "documentation_update") return "documentation_update";
if (classificationType === "open_question") return "open_question";
if (classificationType === "memory_rule") return "memory_rule";
return classificationType;
};
export const sendCaptureItemToReviewInDatabase = async (
projectIdOrSlug: string,
captureItemId: string,
): Promise<ReviewQueueItemDto | undefined | "CAPTURE_NOT_CLASSIFIED" | "CAPTURE_ALREADY_IN_REVIEW"> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const captureItem = await prisma.captureItem.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: captureItemId }, { slug: captureItemId }, { readableId: captureItemId }],
},
});
if (!captureItem) return undefined;
if (captureItem.status === "in_review" || captureItem.linkedReviewItemId) return "CAPTURE_ALREADY_IN_REVIEW";
if (captureItem.status !== "classified" || !captureItem.classificationType) return "CAPTURE_NOT_CLASSIFIED";
return prisma.$transaction(async (tx) => {
const claimed = await tx.captureItem.updateMany({
where: { id: captureItem.id, status: "classified", linkedReviewItemId: null },
data: { status: "in_review", updatedBy: "api" },
});
if (claimed.count !== 1) return "CAPTURE_ALREADY_IN_REVIEW";
const createdAt = new Date();
const slug = `rq-capture-${captureItem.id}`;
const record = await tx.reviewQueueItem.create({
data: {
projectId: project.id,
slug,
readableId: slug,
type: toReviewType(captureItem.classificationType as NonNullable<CaptureItemDto["classificationType"]>),
title: captureItem.title,
description: captureItem.content,
priority: captureItem.confidence && captureItem.confidence >= 90 ? "important" : "normal",
status: ReviewStatus.pending,
source: captureItem.source,
confidence: captureItem.confidence ?? 72,
approvalState: ApprovalState.pending,
evidenceStrength: captureItem.evidenceStrength ?? EvidenceStrength.weak,
verificationState: VerificationState.needs_evidence,
suggestedAction: captureItem.suggestedAction ?? "Review captured item before project truth changes.",
createdBy: "api",
updatedBy: "api",
sourceType: "capture_item",
createdAt,
payloadJson: { captureItemId: captureItem.id },
provenanceJson: { createdFrom: "capture_send_to_review" },
},
});
await tx.captureItem.update({
where: { id: captureItem.id },
data: { linkedReviewItemId: record.slug ?? record.id, updatedBy: "api" },
});
return toReviewDto(record, project.slug);
});
};
export const updateReviewItemInDatabase = async (
projectIdOrSlug: string,
reviewItemId: string,
body: UpdateReviewQueueItemRequestDto,
): Promise<ReviewQueueItemDto | undefined | "REVIEW_ALREADY_DECIDED"> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const existing = await prisma.reviewQueueItem.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: reviewItemId }, { slug: reviewItemId }, { readableId: reviewItemId }],
},
});
if (!existing) return undefined;
if (existing.status !== ReviewStatus.pending) return "REVIEW_ALREADY_DECIDED";
const claimed = await prisma.reviewQueueItem.updateMany({
where: { id: existing.id, status: ReviewStatus.pending },
data: {
...(body.title !== undefined ? { title: body.title.trim() } : {}),
...(body.description !== undefined ? { description: body.description.trim() } : {}),
...(body.priority !== undefined ? { priority: body.priority } : {}),
...(body.notes !== undefined ? { notes: body.notes.trim() } : {}),
updatedBy: "api",
},
});
if (claimed.count !== 1) return "REVIEW_ALREADY_DECIDED";
const record = await prisma.reviewQueueItem.findUniqueOrThrow({ where: { id: existing.id } });
return toReviewDto(record, project.slug);
};
const statusToApproval = (status: ReviewStatus): ApprovalState => {
if (status === ReviewStatus.accepted || status === ReviewStatus.converted || status === ReviewStatus.merged) {
return ApprovalState.approved;
}
if (status === ReviewStatus.rejected) return ApprovalState.rejected;
return ApprovalState.pending;
};
const mutationResult = (
reviewItemId: string,
message: string,
affectedEntities: string[],
createdEntityId?: string,
): ReviewMutationResponseDto => ({
success: true,
message,
updatedEntityId: reviewItemId,
...(createdEntityId ? { createdEntityId } : {}),
affectedEntities,
});
export const applyReviewActionInDatabase = async (
projectIdOrSlug: string,
reviewItemId: string,
action: "accept" | "reject",
notes?: string,
): Promise<ReviewMutationResponseDto | undefined | "REVIEW_ALREADY_DECIDED"> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const existing = await prisma.reviewQueueItem.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: reviewItemId }, { slug: reviewItemId }, { readableId: reviewItemId }],
},
});
if (!existing) return undefined;
const status = action === "accept" ? ReviewStatus.accepted : ReviewStatus.rejected;
const claimed = await prisma.reviewQueueItem.updateMany({
where: { id: existing.id, status: ReviewStatus.pending },
data: {
status,
approvalState: statusToApproval(status),
notes,
updatedBy: "api",
verifiedAt: action === "accept" ? new Date() : undefined,
verifiedBy: action === "accept" ? "api" : undefined,
},
});
if (claimed.count !== 1) return "REVIEW_ALREADY_DECIDED";
return mutationResult(reviewItemId, `Review item '${reviewItemId}' ${action === "accept" ? "accepted" : "rejected"}.`, [reviewItemId]);
};
export const mergeReviewItemInDatabase = async (
projectIdOrSlug: string,
reviewItemId: string,
targetEntityId: string,
notes?: string,
): Promise<ReviewMutationResponseDto | undefined | "REVIEW_ALREADY_DECIDED" | "REVIEW_TARGET_NOT_FOUND"> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const existing = await prisma.reviewQueueItem.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: reviewItemId }, { slug: reviewItemId }, { readableId: reviewItemId }],
},
});
if (!existing) return undefined;
if (existing.status !== ReviewStatus.pending) return "REVIEW_ALREADY_DECIDED";
const publicId = {
projectId: project.id,
deletedAt: null,
OR: [{ id: targetEntityId }, { slug: targetEntityId }, { readableId: targetEntityId }],
};
const targets = await Promise.all([
prisma.feature.findFirst({ where: publicId, select: { id: true } }).then((item) => item && EntityType.feature),
prisma.memoryRule.findFirst({ where: publicId, select: { id: true } }).then((item) => item && EntityType.memory_rule),
prisma.knowledgeItem.findFirst({ where: publicId, select: { id: true } }).then((item) => item && EntityType.knowledge_item),
prisma.documentationItem.findFirst({ where: publicId, select: { id: true } }).then((item) => item && EntityType.documentation_item),
prisma.acceptedTruthItem.findFirst({ where: publicId, select: { id: true } }).then((item) => item && EntityType.accepted_truth_item),
prisma.projectUpdate.findFirst({ where: publicId, select: { id: true } }).then((item) => item && EntityType.project_update),
prisma.contextPack.findFirst({ where: publicId, select: { id: true } }).then((item) => item && EntityType.context_pack),
prisma.initiative.findFirst({ where: publicId, select: { id: true } }).then((item) => item && EntityType.initiative),
prisma.reviewQueueItem.findFirst({
where: { ...publicId, id: { not: existing.id } },
select: { id: true },
}).then((item) => item && EntityType.review_queue_item),
]);
const targetEntityType = targets.find((target) => Boolean(target));
if (!targetEntityType) return "REVIEW_TARGET_NOT_FOUND";
const claimed = await prisma.reviewQueueItem.updateMany({
where: { id: existing.id, status: ReviewStatus.pending },
data: {
status: ReviewStatus.merged,
approvalState: ApprovalState.approved,
targetEntityType,
targetEntityId,
notes,
updatedBy: "api",
verifiedAt: new Date(),
verifiedBy: "api",
},
});
if (claimed.count !== 1) return "REVIEW_ALREADY_DECIDED";
return mutationResult(reviewItemId, `Review item '${reviewItemId}' merged into '${targetEntityId}'.`, [reviewItemId, targetEntityId]);
};
const toAcceptedTruthKind = (target: ReviewQueueConvertTarget) =>
target === "documentation_update" ? "documentation" : target;
export const convertReviewItemInDatabase = async (
projectIdOrSlug: string,
reviewItemId: string,
target: ReviewQueueConvertTarget,
notes?: string,
): Promise<ReviewMutationResponseDto | undefined | "REVIEW_ALREADY_DECIDED"> => {
const project = await findProjectRecord(projectIdOrSlug);
if (!project) return undefined;
const existing = await prisma.reviewQueueItem.findFirst({
where: {
projectId: project.id,
deletedAt: null,
OR: [{ id: reviewItemId }, { slug: reviewItemId }, { readableId: reviewItemId }],
},
});
if (!existing) return undefined;
if (existing.status !== ReviewStatus.pending) return "REVIEW_ALREADY_DECIDED";
const createdEntityId = `truth-converted-${target}-${existing.id}`;
const converted = await prisma.$transaction(async (tx) => {
const claimed = await tx.reviewQueueItem.updateMany({
where: { id: existing.id, status: ReviewStatus.pending },
data: {
status: ReviewStatus.converted,
approvalState: ApprovalState.approved,
createdEntityId,
createdEntityType: EntityType.accepted_truth_item,
payloadJson: {
...(isRecord(existing.payloadJson) ? existing.payloadJson : {}),
convertedTarget: target,
},
notes,
updatedBy: "api",
verifiedAt: new Date(),
verifiedBy: "api",
},
});
if (claimed.count !== 1) return false;
await tx.acceptedTruthItem.create({
data: {
projectId: project.id,
slug: createdEntityId,
readableId: createdEntityId,
sourceReviewItemId: existing.id,
kind: toAcceptedTruthKind(target),
title: existing.title,
summary: existing.description,
status: "converted",
source: existing.source,
confidence: existing.confidence,
approvalState: ApprovalState.approved,
evidenceStrength: existing.evidenceStrength,
verificationState: VerificationState.verified,
payloadJson: { reviewItemId: existing.id, convertedTarget: target },
provenanceJson: { createdFrom: "review_queue_convert" },
createdBy: "api",
verifiedAt: new Date(),
verifiedBy: "api",
},
});
return true;
});
if (!converted) return "REVIEW_ALREADY_DECIDED";
return mutationResult(reviewItemId, `Review item '${reviewItemId}' converted into durable ${target} accepted-truth artifact '${createdEntityId}'.`, [reviewItemId, createdEntityId], createdEntityId);
};
@@ -0,0 +1,424 @@
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<Record<string, never>>;
type TruthRecord = Prisma.AcceptedTruthItemGetPayload<Record<string, never>>;
type UpdateRecord = Prisma.ProjectUpdateGetPayload<Record<string, never>>;
type LogRecord = Prisma.OperatingLogEntryGetPayload<Record<string, never>>;
const findProjectRecord = (projectIdOrSlug: string): Promise<ProjectRecord | null> =>
prisma.project.findFirst({
where: {
deletedAt: null,
OR: [{ id: projectIdOrSlug }, { slug: projectIdOrSlug }],
},
select: { id: true, slug: true },
});
const findReviewRecord = async (
projectId: string,
reviewItemId: string,
): Promise<ReviewRecord | null> =>
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 = <T>(
items: T[],
total: number,
page: number,
pageSize: number,
sort?: string,
filters?: Record<string, unknown>,
) => ({
items,
total,
page,
pageSize,
hasNextPage: page < Math.ceil(total / pageSize),
hasPreviousPage: page > 1,
...(sort ? { sort } : {}),
...(filters ? { filters: filters as Record<string, string | number | boolean | string[]> } : {}),
});
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<ReviewMutationResponseDto | undefined> => {
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<ReviewMutationResponseDto | undefined> => {
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<ReviewMutationResponseDto | undefined | "PROJECT_UPDATE_ALREADY_APPLIED"> => {
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<OperatingLogEntryDto | undefined> => {
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);
};