407 lines
18 KiB
TypeScript
407 lines
18 KiB
TypeScript
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.",
|
|
};
|
|
};
|