--- id: "03-02" phase: 03-miroflow-embutido plan: 02 type: execute wave: 1 depends_on: ["03-01"] files_modified: - server/miroflow/routes.ts - server/miroflow/engine-proxy.ts - server/routes.ts autonomous: true requirements: - REQ-3.5 - REQ-3.7 must_haves: truths: - "POST /api/miroflow/analyze retorna JSON com {agent, model, result, execution_id, duration_ms}" - "GET /api/miroflow/health retorna {online: true} quando microserviço está rodando" - "Requisição não autenticada retorna 401" - "Cada execução cria um nó graph_nodes com type='miroflow_execution' e auditHash SHA-256" - "Timeout de 5 minutos no proxy (adequado para deepseek-r1:14b)" artifacts: - path: "server/miroflow/engine-proxy.ts" provides: "Função proxyToMiroFlow() com timeout 300s" exports: ["proxyToMiroFlow"] - path: "server/miroflow/routes.ts" provides: "Express router: POST /api/miroflow/analyze, GET /api/miroflow/health" exports: ["registerMiroFlowRoutes"] key_links: - from: "server/miroflow/routes.ts" to: "server/python/miroflow_service.py" via: "fetch() para http://localhost:8006/analyze" pattern: "MIROFLOW_PORT.*8006" - from: "server/miroflow/routes.ts" to: "server/graph/service.ts" via: "createNode({type: 'miroflow_execution', ...})" pattern: "createNode.*miroflow_execution" - from: "server/routes.ts" to: "server/miroflow/routes.ts" via: "registerMiroFlowRoutes(app)" pattern: "registerMiroFlowRoutes" --- Criar o bridge Node.js → Python para o MiroFlow: proxy TypeScript (engine-proxy.ts), rotas Express (routes.ts) e registro imutável de execuções no KG via graph_nodes. Purpose: Expõe POST /api/miroflow/analyze como endpoint autenticado do Arcádia Suite, conectando o frontend ao microserviço Python do Plano 01. Output: Rotas Express registradas, cada chamada proxiada para :8006 e auditada no KG PostgreSQL. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/03-miroflow-embutido/03-01-SUMMARY.md @.planning/phases/03-miroflow-embutido/03-RESEARCH.md // TIMEOUT padrão atual: 30000ms — MiroFlow PRECISA de 300000ms (5min) async function proxyToEngine(path: string, options: RequestInit = {}): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), BI_ENGINE_TIMEOUT); const response = await fetch(`${BI_ENGINE_URL}${path}`, { ...options, signal: controller.signal }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(error.detail || `BI Engine error: ${response.status}`); } return await response.json(); } export function registerBiEngineRoutes(app: Express): void { app.post("/api/bi-engine/analyze", async (req, res) => { if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" }); ... }); } export async function createNode(data: InsertGraphNode): Promise // InsertGraphNode: { type: string, tenantId?: number, data: object } // graph_nodes tabela PostgreSQL — NÃO usar Neo4j direto import { registerBiEngineRoutes } from "./bi/engine-proxy"; // padrão a seguir registerBiEngineRoutes(app); // linha ~105 { agent: string, // "statistician" | "fiscal_auditor" | "researcher" model: string, // "deepseek-r1:14b" | "llama3.1:8b" result: string, execution_id: string, // UUID v4 duration_ms: number } Task 1: Criar server/miroflow/engine-proxy.ts e server/miroflow/routes.ts server/miroflow/engine-proxy.ts, server/miroflow/routes.ts - /opt/arcadia_merged/server/bi/engine-proxy.ts (padrão completo de proxy a seguir) - /opt/arcadia_merged/server/graph/service.ts (createNode signature, linhas 41-49) - /opt/arcadia_merged/server/graph/routes.ts (confirmar que /api/graph/nodes aceita POST) - /opt/arcadia_merged/server/auth.ts (confirmar req.isAuthenticated() e req.user shape) - proxyToMiroFlow("/health", "GET") faz fetch para MIROFLOW_URL/health com timeout 5s - proxyToMiroFlow("/analyze", "POST", body) faz fetch para MIROFLOW_URL/analyze com timeout 300000ms - POST /api/miroflow/analyze sem autenticação retorna 401 {"error": "Não autenticado"} - POST /api/miroflow/analyze autenticado encaminha body + tenant_id para :8006/analyze - Após resposta bem-sucedida, registerExecutionInKG cria nó com type="miroflow_execution" - auditHash é SHA-256 do JSON.stringify({execution_id, agent, model, input, output}) - GET /api/miroflow/health não requer autenticação, retorna {online: bool, url: string} Criar diretório server/miroflow/ se não existir. Criar server/miroflow/engine-proxy.ts: ```typescript import type { Express, Request, Response } from "express"; import crypto from "crypto"; import { createNode } from "../graph/service"; const MIROFLOW_HOST = process.env.MIROFLOW_HOST || "localhost"; const MIROFLOW_PORT = parseInt(process.env.MIROFLOW_PORT || "8006", 10); const MIROFLOW_URL = `http://${MIROFLOW_HOST}:${MIROFLOW_PORT}`; const MIROFLOW_TIMEOUT = 300_000; // 5 minutos — deepseek-r1:14b pode levar 60-120s async function proxyToMiroFlow(path: string, method: string = "GET", body?: object): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), MIROFLOW_TIMEOUT); try { const response = await fetch(`${MIROFLOW_URL}${path}`, { method, headers: { "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); clearTimeout(timeout); if (!response.ok) { const err = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(err.detail || `MiroFlow error: ${response.status}`); } return await response.json(); } catch (err: any) { clearTimeout(timeout); if (err.name === "AbortError") throw new Error("MiroFlow timeout (300s)"); throw err; } } async function registerExecutionInKG(req: Request, execData: any, inputBody: any): Promise { try { const auditHash = crypto .createHash("sha256") .update(JSON.stringify({ execution_id: execData.execution_id, agent: execData.agent, model: execData.model, input: inputBody, output: execData.result, })) .digest("hex"); await createNode({ type: "miroflow_execution", tenantId: (req.user as any)?.tenantId ?? null, data: { ...execData, input: inputBody, auditHash, immutable: true, registeredAt: new Date().toISOString(), }, }); } catch (kgErr: any) { // KG failure não deve bloquear a resposta ao cliente console.error("[MiroFlow] Falha ao registrar no KG:", kgErr.message); } } export function registerMiroFlowRoutes(app: Express): void { app.get("/api/miroflow/health", async (_req: Request, res: Response) => { try { const data = await proxyToMiroFlow("/health", "GET"); res.json({ online: true, url: MIROFLOW_URL, ...data }); } catch { res.json({ online: false, url: MIROFLOW_URL }); } }); app.post("/api/miroflow/analyze", async (req: Request, res: Response) => { if (!req.isAuthenticated()) { return res.status(401).json({ error: "Não autenticado" }); } try { const inputBody = { ...req.body, tenant_id: (req.user as any)?.tenantId ?? null, }; const data = await proxyToMiroFlow("/analyze", "POST", inputBody); // Registrar no KG de forma assíncrona (não bloqueia a resposta) registerExecutionInKG(req, data, req.body).catch(() => {}); res.json(data); } catch (err: any) { res.status(502).json({ error: err.message }); } }); console.log(`[MiroFlow Proxy] Rotas registradas -> ${MIROFLOW_URL}`); } ``` Criar server/miroflow/routes.ts como re-export limpo (manter compatibilidade com o loader de módulos): ```typescript export { registerMiroFlowRoutes } from "./engine-proxy"; ``` IMPORTANTE: - Timeout de 300_000ms (5 minutos) — NÃO usar o padrão de 30s do bi/engine-proxy.ts - registerExecutionInKG usa createNode do graph/service.ts, NÃO faz fetch para /api/graph/nodes - Falha no KG não retorna erro ao cliente (try/catch com log) - auditHash cobre: execution_id + agent + model + input + output cd /opt/arcadia_merged && npx tsc --noEmit --project tsconfig.json 2>&1 | grep -i "miroflow" | head -10 || echo "TypeScript OK (nenhum erro em miroflow)" - server/miroflow/engine-proxy.ts existe e compila sem erros TypeScript - server/miroflow/routes.ts existe (re-export ou implementação direta) - MIROFLOW_TIMEOUT = 300_000 está definido (não 30000) - Função registerExecutionInKG usa createNode importado de "../graph/service" - auditHash calculado com crypto.createHash("sha256") - POST /api/miroflow/analyze faz verificação req.isAuthenticated() antes de proxiar - grep -n "300_000\|300000" server/miroflow/engine-proxy.ts retorna resultado - grep -n "miroflow_execution" server/miroflow/engine-proxy.ts retorna resultado Arquivos TypeScript criados sem erros de compilação. Funções exportadas: registerMiroFlowRoutes. Timeout de 5min configurado. KG logging com SHA-256 implementado. Task 2: Registrar MiroFlow em server/routes.ts server/routes.ts - /opt/arcadia_merged/server/routes.ts (ARQUIVO INTEIRO — verificar imports e onde registerBiEngineRoutes é chamado para inserir MiroFlow no mesmo padrão) Adicionar import e registro das rotas MiroFlow em server/routes.ts, seguindo o padrão exato de registerBiEngineRoutes: 1. Adicionar import após a linha que importa registerBiEngineRoutes (linha ~19): ```typescript import { registerMiroFlowRoutes } from "./miroflow/engine-proxy"; ``` 2. Adicionar chamada após registerBiEngineRoutes(app) (linha ~105): ```typescript registerMiroFlowRoutes(app); ``` NÃO alterar nenhuma outra linha do arquivo. NÃO mover, reordenar ou reformatar código existente. NÃO modificar configurações do Superset ou rotas existentes. cd /opt/arcadia_merged && grep -n "registerMiroFlowRoutes" server/routes.ts && npx tsc --noEmit --project tsconfig.json 2>&1 | grep -c "error" || echo "0 erros" - `grep "registerMiroFlowRoutes" server/routes.ts` retorna 2 linhas (import + chamada) - `npx tsc --noEmit` não retorna erros relacionados a miroflow - A linha de import está próxima de registerBiEngineRoutes (mesma região do arquivo) - A chamada registerMiroFlowRoutes(app) está próxima de registerBiEngineRoutes(app) - Nenhuma linha existente do arquivo foi modificada (verificar com git diff) server/routes.ts registra as rotas MiroFlow. POST /api/miroflow/analyze e GET /api/miroflow/health estarão disponíveis quando o servidor Node iniciar. ```bash # Compilação TypeScript limpa: cd /opt/arcadia_merged && npx tsc --noEmit 2>&1 | grep -i "error" | head -5 # Confirmar registro no KG: grep -n "miroflow_execution\|auditHash\|createNode" /opt/arcadia_merged/server/miroflow/engine-proxy.ts # Confirmar timeout correto: grep -n "300" /opt/arcadia_merged/server/miroflow/engine-proxy.ts # Confirmar rotas registradas: grep -n "registerMiroFlowRoutes" /opt/arcadia_merged/server/routes.ts ``` - server/miroflow/engine-proxy.ts e server/miroflow/routes.ts existem - TypeScript compila sem erros nos novos arquivos - server/routes.ts inclui import e chamada registerMiroFlowRoutes - Timeout de 300s configurado no proxy - auditHash SHA-256 calculado e salvo via createNode - GET /api/miroflow/health acessível sem autenticação - POST /api/miroflow/analyze requer autenticação (401 sem sessão) Após conclusão, criar `.planning/phases/03-miroflow-embutido/03-02-SUMMARY.md` com: - Arquivos criados/modificados - Resultado da compilação TypeScript - Confirmação do padrão de timeout (300s) - Confirmação do KG logging com auditHash