Files
C.E.L_Slide_test2/Front/vite.config.ts
kyeongmin 90503cadd6 feat(#67): IMP-38 V4 max_rank policy formalization (u1~u3, 4 round consensus)
- u1: separate templates/phase_z2/catalog/v4_fallback_policy.yaml + load_v4_fallback_policy() loader
  (catalog pollution prevention — Codex #1 correction)
- u2: dynamic effective max_rank in lookup_v4_match_with_fallback (3-variable ceiling min,
  Codex #2 correction: min(configured, len(judgments_full32))) + 3-tier usable predicate
  (status + catalog + optional capacity) + trace 8 fields (requested/default/configured_extended/
  judgments_count/effective_extended_ceiling/effective_max_rank/usable_count/policy_applied)
- u3: 2 production call site cleanup (max_rank=3 removed, HEAD baseline) + tracked
  Front/vite.config.ts PHASE_Z_MAX_RANK env retired + 4 regression scenarios

verified: 32 passed (IMP-38 focused scope) — IMP-05 L4 dedup / L2 schema preserved,
IMP-30 allow_provisional byte-identical, caller_override backward compat (tests)

Stage cycle (#67, 7 round Claude + 5 round Codex):
- Stage 1: Claude #1 -> Codex #1 YES + 5 corrections
- Stage 2 r1+r2: Claude #2-#4 -> Codex #2 Q2 -> Codex #3 YES (4 round consensus LOCK 23195)
- Stage 3 U1+U2+U3: Claude #5-#9 -> Codex #6 NO 4to3 correction -> Codex #7 YES -> Codex #8 YES
- Stage 4: Claude #11 -> Codex #9 (anchor attribution nuance) -> Codex #10 readiness -> Codex #11

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:14:05 +09:00

543 lines
20 KiB
TypeScript

import { jsxLocPlugin } from "@builder.io/vite-plugin-jsx-loc";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { defineConfig, type Plugin, type ViteDevServer } from "vite";
import { vitePluginManusRuntime } from "vite-plugin-manus-runtime";
// =============================================================================
// Manus Debug Collector - Vite Plugin
// Writes browser logs directly to files, trimmed when exceeding size limit
// =============================================================================
const PROJECT_ROOT = import.meta.dirname;
const LOG_DIR = path.join(PROJECT_ROOT, ".manus-logs");
const MAX_LOG_SIZE_BYTES = 1 * 1024 * 1024; // 1MB per log file
const TRIM_TARGET_BYTES = Math.floor(MAX_LOG_SIZE_BYTES * 0.6); // Trim to 60% to avoid constant re-trimming
type LogSource = "browserConsole" | "networkRequests" | "sessionReplay";
function ensureLogDir() {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
}
function trimLogFile(logPath: string, maxSize: number) {
try {
if (!fs.existsSync(logPath) || fs.statSync(logPath).size <= maxSize) {
return;
}
const lines = fs.readFileSync(logPath, "utf-8").split("\n");
const keptLines: string[] = [];
let keptBytes = 0;
// Keep newest lines (from end) that fit within 60% of maxSize
const targetSize = TRIM_TARGET_BYTES;
for (let i = lines.length - 1; i >= 0; i--) {
const lineBytes = Buffer.byteLength(`${lines[i]}\n`, "utf-8");
if (keptBytes + lineBytes > targetSize) break;
keptLines.unshift(lines[i]);
keptBytes += lineBytes;
}
fs.writeFileSync(logPath, keptLines.join("\n"), "utf-8");
} catch {
/* ignore trim errors */
}
}
function writeToLogFile(source: LogSource, entries: unknown[]) {
if (entries.length === 0) return;
ensureLogDir();
const logPath = path.join(LOG_DIR, `${source}.log`);
// Format entries with timestamps
const lines = entries.map((entry) => {
const ts = new Date().toISOString();
return `[${ts}] ${JSON.stringify(entry)}`;
});
// Append to log file
fs.appendFileSync(logPath, `${lines.join("\n")}\n`, "utf-8");
// Trim if exceeds max size
trimLogFile(logPath, MAX_LOG_SIZE_BYTES);
}
/**
* Vite plugin to collect browser debug logs
* - POST /__manus__/logs: Browser sends logs, written directly to files
* - Files: browserConsole.log, networkRequests.log, sessionReplay.log
* - Auto-trimmed when exceeding 1MB (keeps newest entries)
*/
function vitePluginManusDebugCollector(): Plugin {
return {
name: "manus-debug-collector",
transformIndexHtml(html) {
if (process.env.NODE_ENV === "production") {
return html;
}
return {
html,
tags: [
{
tag: "script",
attrs: {
src: "/__manus__/debug-collector.js",
defer: true,
},
injectTo: "head",
},
],
};
},
configureServer(server: ViteDevServer) {
// POST /__manus__/logs: Browser sends logs (written directly to files)
server.middlewares.use("/__manus__/logs", (req, res, next) => {
if (req.method !== "POST") {
return next();
}
const handlePayload = (payload: any) => {
// Write logs directly to files
if (payload.consoleLogs?.length > 0) {
writeToLogFile("browserConsole", payload.consoleLogs);
}
if (payload.networkRequests?.length > 0) {
writeToLogFile("networkRequests", payload.networkRequests);
}
if (payload.sessionEvents?.length > 0) {
writeToLogFile("sessionReplay", payload.sessionEvents);
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true }));
};
const reqBody = (req as { body?: unknown }).body;
if (reqBody && typeof reqBody === "object") {
try {
handlePayload(reqBody);
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: String(e) }));
}
return;
}
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
const payload = JSON.parse(body);
handlePayload(payload);
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: String(e) }));
}
});
});
},
};
}
function vitePluginStorageProxy(): Plugin {
return {
name: "manus-storage-proxy",
configureServer(server: ViteDevServer) {
server.middlewares.use("/manus-storage", async (req, res) => {
const key = req.url?.replace(/^\//, "");
if (!key) {
res.writeHead(400, { "Content-Type": "text/plain" });
res.end("Missing storage key");
return;
}
const forgeBaseUrl = (process.env.BUILT_IN_FORGE_API_URL || "").replace(/\/+$/, "");
const forgeKey = process.env.BUILT_IN_FORGE_API_KEY;
if (!forgeBaseUrl || !forgeKey) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Storage proxy not configured");
return;
}
try {
const forgeUrl = new URL("v1/storage/presign/get", forgeBaseUrl + "/");
forgeUrl.searchParams.set("path", key);
const forgeResp = await fetch(forgeUrl, {
headers: { Authorization: `Bearer ${forgeKey}` },
});
if (!forgeResp.ok) {
res.writeHead(502, { "Content-Type": "text/plain" });
res.end("Storage backend error");
return;
}
const { url } = (await forgeResp.json()) as { url: string };
if (!url) {
res.writeHead(502, { "Content-Type": "text/plain" });
res.end("Empty signed URL");
return;
}
res.writeHead(307, { Location: url, "Cache-Control": "no-store" });
res.end();
} catch {
res.writeHead(502, { "Content-Type": "text/plain" });
res.end("Storage proxy error");
}
});
},
};
}
// =============================================================================
// Phase Z API Plugin — MDX 업로드 → 파이프라인 실행 → 결과 노출
//
// Endpoints (vite dev middleware) :
// POST /api/run multipart/JSON body {filename, content} → run_id
// GET /data/runs/{run_id}/{path} → {DESIGN_AGENT_ROOT}/data/runs/{run_id}/phase_z2/{path}
//
// 환경 변수 (선택) :
// DESIGN_AGENT_ROOT python pipeline 실행 cwd. default = D:/ad-hoc/kei/design_agent
// =============================================================================
function vitePluginPhaseZApi(): Plugin {
const DESIGN_AGENT_ROOT =
process.env.DESIGN_AGENT_ROOT || "D:\\ad-hoc\\kei\\design_agent";
const UPLOADS_DIR = path.join(DESIGN_AGENT_ROOT, "samples", "uploads");
const RUNS_DIR = path.join(DESIGN_AGENT_ROOT, "data", "runs");
return {
name: "phase-z-api",
configureServer(server: ViteDevServer) {
// ── POST /api/run — MDX 업로드 → 파이프라인 실행 (sync) ──
server.middlewares.use("/api/run", (req, res, next) => {
if (req.method !== "POST") return next();
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
let payload: {
filename?: string;
content?: string;
overrides?: {
layout?: string;
frames?: Record<string, string>; // unit_id → template_id
zoneGeometries?: Record<string, { x: number; y: number; w: number; h: number }>; // zone_id → bbox (slide-body 내부 0~1)
// IMP-08 B-3 : zone_id -> list of canonical section_id assignments
// (e.g., "top": ["03-1-sub-1"]). Forwarded as --override-section-assignment.
zoneSections?: Record<string, string[]>;
};
};
try {
payload = JSON.parse(body);
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({ success: false, error: `bad JSON: ${String(e)}` })
);
return;
}
const { filename, content, overrides } = payload;
if (!filename || typeof content !== "string") {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: false,
error: "missing filename or content",
})
);
return;
}
// Security: filename 정규화 (path 분리 / .. 차단)
const safeName = filename.replace(/[\\/]/g, "_").replace(/\.\./g, "_");
if (!safeName.match(/\.mdx?$/i)) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: false,
error: "filename must end with .mdx or .md",
})
);
return;
}
// 1. MDX 저장
if (!fs.existsSync(UPLOADS_DIR)) {
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
}
const mdxPath = path.join(UPLOADS_DIR, safeName);
fs.writeFileSync(mdxPath, content, "utf-8");
// 2. run_id 생성 (basename + timestamp)
const baseName = safeName
.replace(/\.mdx?$/i, "")
.replace(/[^a-zA-Z0-9_]/g, "_");
const ts = new Date()
.toISOString()
.replace(/[-:T]/g, "")
.replace(/\..+$/, "")
.slice(0, 14); // YYYYMMDDHHMMSS
const runId = `${baseName}_${ts}`;
// 3. python -m src.phase_z2_pipeline {mdx_path} {run_id} (cwd=DESIGN_AGENT_ROOT)
// shell: false → argv 그대로 전달 (파일명 공백/한글 안전).
// Windows 에서 .exe 확장자 자동 탐색을 위해 python.exe 명시.
// Step D : overrides 가 들어오면 --override-layout / --override-frame 인자로 forward.
const cliArgs = ["-m", "src.phase_z2_pipeline", mdxPath, runId];
if (overrides?.layout && typeof overrides.layout === "string") {
cliArgs.push("--override-layout", overrides.layout);
}
if (overrides?.frames && typeof overrides.frames === "object") {
for (const [unitId, templateId] of Object.entries(overrides.frames)) {
if (typeof templateId === "string" && templateId) {
cliArgs.push("--override-frame", `${unitId}=${templateId}`);
}
}
}
if (overrides?.zoneGeometries && typeof overrides.zoneGeometries === "object") {
for (const [zoneId, geom] of Object.entries(overrides.zoneGeometries)) {
if (geom && typeof geom === "object") {
const { x, y, w, h } = geom;
cliArgs.push(
"--override-zone-geometry",
`${zoneId}=${x},${y},${w},${h}`
);
}
}
}
// IMP-08 B-3 — zoneSections override forward to CLI.
// Each entry becomes `--override-section-assignment ZONE=sid[,sid]`.
// Empty arrays and non-string sids are filtered out so the backend
// never receives bogus assignments from a partially-built UI state.
if (overrides?.zoneSections && typeof overrides.zoneSections === "object") {
for (const [zoneId, sids] of Object.entries(overrides.zoneSections)) {
if (!Array.isArray(sids)) continue;
const cleaned = sids.filter((s) => typeof s === "string" && s.trim());
if (cleaned.length === 0) continue;
cliArgs.push(
"--override-section-assignment",
`${zoneId}=${cleaned.join(",")}`
);
}
}
console.log(
`[phase-z-api] spawn pipeline: run_id=${runId}, mdx=${mdxPath}, args=${JSON.stringify(cliArgs.slice(2))}`
);
const pythonExe = process.platform === "win32" ? "python.exe" : "python";
// 2026-05-14 — env toggle forward (보고용 일회성).
// PHASE_Z_ALLOW_RESTRUCTURE / PHASE_Z_ALLOW_REJECT : status 통과
// 2026-05-21 — IMP-38 retire PHASE_Z_MAX_RANK env (never read by backend).
// v4 fallback chain max_rank 는 templates/phase_z2/catalog/v4_fallback_policy.yaml 의
// 정식 정책 (dynamic_usable_count_based) 으로 결정 — backend src/phase_z2_pipeline.py
// 의 lookup_v4_match_with_fallback() 가 load_v4_fallback_policy() 로 적용.
const proc = spawn(pythonExe, cliArgs, {
cwd: DESIGN_AGENT_ROOT,
shell: false,
env: {
...process.env,
PHASE_Z_ALLOW_RESTRUCTURE: "1",
PHASE_Z_ALLOW_REJECT: "1",
},
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
proc.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
proc.on("error", (err) => {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: false,
error: `spawn failed: ${err.message}`,
})
);
});
proc.on("exit", (code) => {
const outDir = path.join(RUNS_DIR, runId, "phase_z2");
const finalHtmlExists = fs.existsSync(path.join(outDir, "final.html"));
const previewExists = fs.existsSync(path.join(outDir, "preview.png"));
// 2026-05-14 — success 판정 보완.
// exit_code 0 → full PASS
// exit_code != 0 → status enum 차이 (visual_regression / partial_coverage 등)
// 다만 final.html 존재 시 frontend 는 표시 가능 (clipping warning 만).
// = (exit_code 0) OR (final.html 존재) 모두 success=true.
const success = code === 0 || finalHtmlExists;
res.writeHead(success ? 200 : 500, {
"Content-Type": "application/json",
});
res.end(
JSON.stringify({
success,
run_id: runId,
exit_code: code,
final_html_exists: finalHtmlExists,
preview_exists: previewExists,
stdout: stdout.slice(-3000),
stderr: stderr.slice(-3000),
})
);
});
});
});
// ── GET /api/sample-mdx → samples/mdx/{03|04|05} (데모) ──
// 페이지 로드 시 frontend 가 자동 fetch — 상대방에게 mdx 파일 공유 안 해도 되게.
// 2026-05-14 — query param `?mdx=04` / `?mdx=05` 추가 (default = 03).
// 04 / 05 도 frontend simulation 가능하도록 frame/layout override 적용 path 유지.
const SAMPLE_MDX_MAP: Record<string, string> = {
"03": "03. DX 시행을 위한 필수 요건 및 혁신 방안.mdx",
"04": "04. DX 지연 요인.mdx",
"05": "05. 설계 방식의 왜곡.mdx",
};
server.middlewares.use("/api/sample-mdx", (req, res, next) => {
if (req.method !== "GET") return next();
const url = new URL(req.url || "/", "http://x");
const which = url.searchParams.get("mdx") || "03";
const filename = SAMPLE_MDX_MAP[which] || SAMPLE_MDX_MAP["03"];
const samplePath = path.join(
DESIGN_AGENT_ROOT,
"samples",
"mdx",
filename
);
if (!fs.existsSync(samplePath)) {
res.writeHead(404);
res.end("sample mdx not found");
return;
}
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
res.setHeader("X-Mdx-Filename", encodeURIComponent(filename));
fs.createReadStream(samplePath).pipe(res);
});
// ── GET /frame-preview/{frame_number} → data/figma_previews/{NN}.png ──
// V4 후보 카드 thumbnail. data/figma_previews/ 는 frame_number 1~32 모두 존재 (Figma original export).
// 우선 figma_to_html_agent/blocks/{frame_number}/preview.png 시도해서 더 detail 한 렌더 결과 있으면 사용 — 단 frame_id 와 frame_number 가 다른 식별자라 path 매핑이 없으므로 일단 figma_previews 만 사용.
server.middlewares.use("/frame-preview", (req, res, next) => {
if (req.method !== "GET") return next();
const url = (req.url || "").split("?")[0];
const arg = url.replace(/^\//, "").replace(/[^0-9]/g, "");
if (!arg) return next();
// 1-digit / 2-digit 모두 zero-pad 시도 (e.g., "13" → "13.png", "5" → "05.png")
const padded = arg.padStart(2, "0");
const previewPath = path.join(
DESIGN_AGENT_ROOT,
"data",
"figma_previews",
`${padded}.png`
);
if (!fs.existsSync(previewPath)) {
res.writeHead(404);
res.end("frame preview not found");
return;
}
res.setHeader("Content-Type", "image/png");
fs.createReadStream(previewPath).pipe(res);
});
// ── GET /data/runs/{run_id}/{path} → {RUNS_DIR}/{run_id}/phase_z2/{path} ──
server.middlewares.use("/data/runs", (req, res, next) => {
if (req.method !== "GET") return next();
const url = req.url || "";
if (url.includes("..") || url.includes("\\")) {
res.writeHead(400);
res.end("Bad Request");
return;
}
const segments = url.split("?")[0].split("/").filter((s) => s.length > 0);
if (segments.length < 2) return next();
const [runId, ...rest] = segments;
const filePath = path.join(RUNS_DIR, runId, "phase_z2", ...rest);
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
// Fallback to next middleware (vite default static — client/public)
return next();
}
const ext = path.extname(filePath).toLowerCase();
const mimeMap: Record<string, string> = {
".html": "text/html; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".svg": "image/svg+xml",
".md": "text/markdown; charset=utf-8",
".txt": "text/plain; charset=utf-8",
};
res.setHeader("Content-Type", mimeMap[ext] || "application/octet-stream");
fs.createReadStream(filePath).pipe(res);
});
},
};
}
const plugins = [react(), tailwindcss(), jsxLocPlugin(), vitePluginManusRuntime(), vitePluginManusDebugCollector(), vitePluginStorageProxy(), vitePluginPhaseZApi()];
export default defineConfig({
plugins,
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "client", "src"),
"@shared": path.resolve(import.meta.dirname, "shared"),
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
},
},
envDir: path.resolve(import.meta.dirname),
root: path.resolve(import.meta.dirname, "client"),
build: {
outDir: path.resolve(import.meta.dirname, "dist/public"),
emptyOutDir: true,
},
server: {
port: 3000,
strictPort: false, // Will find next available port if 3000 is busy
host: true,
allowedHosts: [
".manuspre.computer",
".manus.computer",
".manus-asia.computer",
".manuscomputer.ai",
".manusvm.computer",
"localhost",
"127.0.0.1",
],
fs: {
strict: true,
deny: ["**/.*"],
},
},
});