Files
vibn-frontend/app/api/projects/[projectId]/architecture/route.ts
Mark Henderson a3aa5e4208 fix(arch+design): wire architecture and design together
- Architecture route now uses /generate endpoint (no Atlas session
  overhead, no conflicting system prompt) for clean JSON generation
- Design page fetches saved architecture on load and maps designSurfaces
  to known surface IDs via fuzzy match; AI-suggested surfaces are
  pre-selected in the picker with an "AI" badge and explanatory note

Made-with: Cursor
2026-03-03 21:11:27 -08:00

207 lines
6.6 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/authOptions";
import { query } from "@/lib/db-postgres";
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
// ---------------------------------------------------------------------------
// GET — return saved architecture (if it exists)
// ---------------------------------------------------------------------------
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { projectId } = await params;
try {
const rows = await query<{ data: any }>(
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
[projectId]
);
const data = rows[0]?.data ?? {};
return NextResponse.json({
architecture: data.architecture ?? null,
prd: data.prd ?? null,
});
} catch {
return NextResponse.json({ architecture: null, prd: null });
}
}
// ---------------------------------------------------------------------------
// POST — generate architecture recommendation from PRD using AI
// ---------------------------------------------------------------------------
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { projectId } = await params;
const body = await req.json().catch(() => ({}));
const forceRegenerate = body.forceRegenerate === true;
// Load project PRD + phases
let prd: string | null = null;
let phases: any[] = [];
try {
const rows = await query<{ data: any }>(
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
[projectId]
);
const data = rows[0]?.data ?? {};
prd = data.prd ?? null;
// Return cached architecture if it exists and not forcing regenerate
if (data.architecture && !forceRegenerate) {
return NextResponse.json({ architecture: data.architecture, cached: true });
}
} catch {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
if (!prd) {
return NextResponse.json({ error: "No PRD found — complete discovery first" }, { status: 400 });
}
try {
const phaseRows = await query<{ phase: string; title: string; summary: string; data: any }>(
`SELECT phase, title, summary, data FROM atlas_phases WHERE project_id = $1 ORDER BY saved_at ASC`,
[projectId]
);
phases = phaseRows;
} catch { /* phases optional */ }
// Build a concise context string from phases
const phaseContext = phases.map(p =>
`## ${p.title}\n${p.summary}\n${JSON.stringify(p.data, null, 2)}`
).join("\n\n");
const prompt = `You are a senior software architect. Analyse the following Product Requirements Document and recommend a technical architecture for a Turborepo monorepo.
Return ONLY a valid JSON object (no markdown, no explanation) with this exact structure:
{
"productName": "string",
"productType": "string (e.g. PWA Game, SaaS, Marketplace, Internal Tool)",
"summary": "2-3 sentence plain-English summary of the recommended architecture",
"apps": [
{
"name": "string (e.g. web, api, simulator)",
"type": "string (e.g. Next.js 15, Express API, Node.js service)",
"description": "string — what this app does",
"tech": ["string array of key technologies"],
"screens": ["string array — key screens/routes if applicable, else empty"]
}
],
"packages": [
{
"name": "string (e.g. db, types, ui)",
"description": "string — what this shared package contains"
}
],
"infrastructure": [
{
"name": "string (e.g. PostgreSQL, Redis, Background Jobs)",
"reason": "string — why this is needed based on the PRD"
}
],
"integrations": [
{
"name": "string (e.g. Ad Network SDK)",
"required": true,
"notes": "string"
}
],
"designSurfaces": ["string array — e.g. Web App, Mobile PWA, Admin"],
"riskNotes": ["string array — 1-2 key architectural risks from the PRD"]
}
Be specific to this product. Do not use generic boilerplate — base your decisions on the PRD content.
--- DISCOVERY PHASES ---
${phaseContext}
--- PRD ---
${prd}`;
try {
const res = await fetch(`${AGENT_RUNNER_URL}/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
signal: AbortSignal.timeout(120_000),
});
if (!res.ok) {
throw new Error(`Agent runner responded ${res.status}`);
}
const data = await res.json();
const raw = data.reply ?? "";
// Extract JSON from response (strip any accidental markdown)
const jsonMatch = raw.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error("No JSON in response");
const architecture = JSON.parse(jsonMatch[0]);
// Persist to project data
await query(
`UPDATE fs_projects
SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{architecture}', $2::jsonb, true),
updated_at = NOW()
WHERE id = $1`,
[projectId, JSON.stringify(architecture)]
);
return NextResponse.json({ architecture, cached: false });
} catch (err) {
console.error("[architecture] Generation failed:", err);
return NextResponse.json(
{ error: "Architecture generation failed. Please try again." },
{ status: 500 }
);
}
}
// ---------------------------------------------------------------------------
// PATCH — confirm architecture (sets architectureConfirmed flag)
// ---------------------------------------------------------------------------
export async function PATCH(
_req: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { projectId } = await params;
try {
await query(
`UPDATE fs_projects
SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{architectureConfirmed}', 'true'::jsonb, true),
updated_at = NOW()
WHERE id = $1`,
[projectId]
);
return NextResponse.json({ confirmed: true });
} catch {
return NextResponse.json({ error: "Failed to confirm" }, { status: 500 });
}
}