Tul xxx Tul
User / IP
:
216.73.216.159
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
aircan.me
/
public_html
/
progressgym
/
modules
/
roadmap
/
Viewing: actions.php
<?php /** * Backend del Módulo Super Roadmap AI Builder (Galaxy Edition) * * Modelos: * - Gemini 2.5 Flash + Google Search Grounding + Thinking High (Creador de Roadmaps por lotes) * - Gemini 2.5 Flash + Google Search Grounding + Thinking High (Instructor Operativo) * - Gemini Deep Research 2026 (Optimización de Contexto) */ session_start(); set_time_limit(0); header('Content-Type: application/json; charset=utf-8'); require_once __DIR__ . '/../../config/database.php'; require_once __DIR__ . '/../../config/openrouter.php'; require_once __DIR__ . '/../../config/time_context.php'; if (!isset($_SESSION['user_id'])) { echo json_encode(['success' => false, 'error' => 'No autorizado']); exit; } $db = getDB(); $uid = $_SESSION['user_id']; $duo = getDuoId(); $action = $_POST['action'] ?? ''; // ========================================== // Fechas de contexto // ========================================== $fecha_hoy = CURRENT_AI_DATE; $anio_actual = CURRENT_AI_YEAR; // ========================================== // Configuracion IA Roadmap // ========================================== define('ROADMAP_AI_DEFAULT_MODEL', getenv('GOOGLE_ROADMAP_MODEL') ?: (defined('GOOGLE_ROADMAP_MODEL') ? GOOGLE_ROADMAP_MODEL : (defined('GOOGLE_GEMINI_SKILLS_FAST_MODEL') ? GOOGLE_GEMINI_SKILLS_FAST_MODEL : 'gemini-2.5-flash'))); define('ROADMAP_AI_MODEL_LABEL', getenv('GOOGLE_ROADMAP_MODEL_LABEL') ?: (defined('GOOGLE_ROADMAP_MODEL_LABEL') ? GOOGLE_ROADMAP_MODEL_LABEL : 'Gemini 2.5 Flash')); define('ROADMAP_BATCH_SIZE', 3); define('ROADMAP_MIN_MODULES', 15); define('ROADMAP_MAX_MODULES', 16); define('ROADMAP_MAX_ITEMS_PER_MODULE', 7); define('ROADMAP_MAX_TOTAL_HOURS', 35); // Asegurar que la tabla del instructor existe $db->query("CREATE TABLE IF NOT EXISTS roadmap_instructor ( id INT AUTO_INCREMENT PRIMARY KEY, item_id INT NOT NULL, roadmap_title VARCHAR(200), project_name VARCHAR(200), objetivo TEXT, respuesta TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;"); // ========================================== // Helper: Call Google AI Studio — rotación de keys + reintentos inteligentes // ========================================== function extractGoogleAiText($resArr) { $content = ''; $candidates = $resArr['candidates'] ?? []; foreach ($candidates as $candidate) { $parts = $candidate['content']['parts'] ?? []; foreach ($parts as $part) { if (isset($part['text']) && empty($part['thought'])) { $content .= $part['text']; } } } if (trim($content) !== '') { return trim($content); } return trim($resArr['candidates'][0]['content']['parts'][0]['text'] ?? ''); } function getRoadmapAiOptions($maxOutputTokens = 16384, $timeout = 300) { return [ 'temperature' => 0, 'topP' => 0.1, 'thinkingLevel' => 'high', 'maxOutputTokens' => $maxOutputTokens, 'timeout' => $timeout ]; } function roadmapExtractGeminiRetryDelay($resArr, $fallback = 15) { $details = is_array($resArr['error']['details'] ?? null) ? $resArr['error']['details'] : []; foreach ($details as $detail) { if (!is_array($detail)) { continue; } if (($detail['@type'] ?? '') === 'type.googleapis.com/google.rpc.RetryInfo' && !empty($detail['retryDelay'])) { if (preg_match('/([\d.]+)s/', (string) $detail['retryDelay'], $match)) { return max(1, (int) ceil((float) $match[1])); } } } if (isset($resArr['error']['message']) && preg_match('/retry in ([\d.]+)s/i', (string) $resArr['error']['message'], $match)) { return max(1, (int) ceil((float) $match[1])); } return $fallback; } function roadmapGeminiQuotaHint($message) { $message = (string) $message; if (stripos($message, 'free_tier_requests') !== false) { return 'Límite temporal del free tier de Gemini alcanzado. Espera la ventana indicada por Google o usa una key/proyecto con billing habilitado.'; } if (stripos($message, 'input_token') !== false || stripos($message, 'token') !== false) { return 'Límite de tokens del free tier de Gemini alcanzado. Reduce contexto/salida o espera el reset de cuota.'; } return 'Rate limit/cuota de Gemini alcanzada.'; } function callGeminiRoadmap($promptSystem, $promptUser, $model = null, $grounding = false, &$db = null, $options = []) { $model = $model ?: (defined('ROADMAP_AI_DEFAULT_MODEL') ? ROADMAP_AI_DEFAULT_MODEL : 'gemini-2.5-flash'); // Pool de keys: la nueva primero, luego las demás como fallback $keys = []; $keyNames = [ 'GOOGLE_NEW_KEY', 'GOOGLE_GEMINI_API_KEY_ROADMAP', 'GOOGLE_GEMINI_API_KEY', 'GOOGLE_GEMINI_API_KEY_SCRUM', 'GOOGLE_GEMINI_API_KEY_CALORIES', 'GOOGLE_GEMINI_API_KEY_SKILLS', 'GOOGLE_GEMINI_API_KEY_OUT', ]; foreach ($keyNames as $name) { $val = getenv($name) ?: (defined($name) ? constant($name) : ''); if (!empty($val)) $keys[] = ['key' => $val, 'name' => $name]; } if (empty($keys)) { return "ERROR_API: No hay API keys de Gemini configuradas."; } // --- ROUND ROBIN LOGIC --- if (!isset($_SESSION['roadmap_key_index'])) { $_SESSION['roadmap_key_index'] = 0; } else { $_SESSION['roadmap_key_index']++; } // Asegurar que el índice esté dentro de los límites $startIndex = $_SESSION['roadmap_key_index'] % count($keys); // Reordenar el array de keys para que empiece en el índice de turno $rotatedKeys = []; for ($i = 0; $i < count($keys); $i++) { $rotatedKeys[] = $keys[($startIndex + $i) % count($keys)]; } $keys = $rotatedKeys; // ------------------------- $logDir = __DIR__ . '/logs'; if (!file_exists($logDir)) mkdir($logDir, 0777, true); $lastError = ''; $totalAttempts = 0; $generationConfig = [ "temperature" => array_key_exists('temperature', $options) ? $options['temperature'] : 0.4, "maxOutputTokens" => $options['maxOutputTokens'] ?? 16384 ]; if (array_key_exists('topP', $options) && $options['topP'] !== null) { $generationConfig["topP"] = $options['topP']; } if (!empty($options['thinkingLevel']) || array_key_exists('thinkingBudget', $options)) { $isGemini25 = stripos((string) $model, 'gemini-2.5') !== false; if ($isGemini25) { $generationConfig["thinkingConfig"] = [ "thinkingBudget" => array_key_exists('thinkingBudget', $options) ? (int) $options['thinkingBudget'] : -1 ]; } else { $generationConfig["thinkingConfig"] = [ "thinkingLevel" => $options['thinkingLevel'] ?? 'high' ]; } } $responseMimeType = $options['responseMimeType'] ?? (!$grounding ? 'application/json' : null); if (!empty($responseMimeType)) { $generationConfig["responseMimeType"] = $responseMimeType; } if (!empty($options['responseJsonSchema'])) { $generationConfig["responseJsonSchema"] = $options['responseJsonSchema']; } $payload = [ "systemInstruction" => [ "parts" => [["text" => $promptSystem]] ], "contents" => [ [ "role" => "user", "parts" => [["text" => $promptUser]] ] ], "generationConfig" => $generationConfig ]; if ($grounding) { $payload["tools"] = [ ["google_search" => new \stdClass()] ]; } $jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE); // Estrategia: respetar RetryInfo en 429 cortos antes de rotar de key. foreach ($keys as $keyInfo) { $apiKey = $keyInfo['key']; $keyName = $keyInfo['name']; $retriesThisKey = 0; $maxRetriesThisKey = 2; $retrySleepSeconds = 0; $retryReason = '500/503'; for ($attempt = 0; $attempt <= $maxRetriesThisKey; $attempt++) { $totalAttempts++; if ($attempt > 0) { $sleepSeconds = $retrySleepSeconds > 0 ? $retrySleepSeconds : 10; sleep($sleepSeconds); file_put_contents( $logDir . '/retry_log.txt', date('Y-m-d H:i:s') . " | Key: {$keyName} | Reintento #{$attempt} ({$retryReason}) | Esperando {$sleepSeconds}s\n", FILE_APPEND ); $retrySleepSeconds = 0; $retryReason = '500/503'; } $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}"; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => $jsonPayload, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => $options['timeout'] ?? 300 ]); if ($db) $db = null; $response = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($db === null) $db = getDB(true); if ($error) { $lastError = "CURL ({$keyName}): {$error}"; continue; // reintentar misma key } $resArr = json_decode($response, true); // 429 = rate limit de ESTA key → saltar inmediatamente a otra key if ($httpCode === 429) { $errorMessage = $resArr['error']['message'] ?? "HTTP 429"; $retryDelay = roadmapExtractGeminiRetryDelay($resArr, 60); $lastError = $errorMessage . " [{$keyName}]"; if ($attempt < $maxRetriesThisKey && $retryDelay <= 65) { $retrySleepSeconds = $retryDelay; $retryReason = '429 RetryInfo'; file_put_contents( $logDir . '/retry_log.txt', date('Y-m-d H:i:s') . " | Key: {$keyName} | HTTP 429 | Respetando RetryInfo {$retryDelay}s antes de rotar\n", FILE_APPEND ); continue; } file_put_contents( $logDir . '/retry_log.txt', date('Y-m-d H:i:s') . " | Key: {$keyName} | HTTP 429 (rate limit) | " . roadmapGeminiQuotaHint($errorMessage) . " | Saltando a siguiente key\n", FILE_APPEND ); break; // salir del loop de reintentos, ir a siguiente key } // 500/503 = servidor saturado → reintentar misma key una vez if (in_array($httpCode, [500, 503])) { $lastError = ($resArr['error']['message'] ?? "HTTP {$httpCode}") . " [{$keyName}]"; file_put_contents( $logDir . '/retry_log.txt', date('Y-m-d H:i:s') . " | Key: {$keyName} | HTTP {$httpCode} | Intento {$attempt}/{$maxRetriesThisKey}\n", FILE_APPEND ); continue; // reintentar misma key } // Otros errores (401, 403) → key inválida, saltar if ($httpCode >= 400) { $lastError = ($resArr['error']['message'] ?? "HTTP {$httpCode}") . " [{$keyName}]"; file_put_contents( $logDir . '/retry_log.txt', date('Y-m-d H:i:s') . " | Key: {$keyName} | HTTP {$httpCode} (no retriable) | Saltando\n", FILE_APPEND ); break; } $text = extractGoogleAiText($resArr); if ($text === null || trim($text) === '') { $lastError = "Respuesta vacía [{$keyName}]"; continue; } // Éxito if ($totalAttempts > 1) { file_put_contents( $logDir . '/retry_log.txt', date('Y-m-d H:i:s') . " | ÉXITO | Key: {$keyName} | Intento global #{$totalAttempts}\n", FILE_APPEND ); } return $text; } } file_put_contents( $logDir . '/retry_log.txt', date('Y-m-d H:i:s') . " | FALLO TOTAL | {$totalAttempts} intentos en " . count($keys) . " keys | Último: {$lastError}\n", FILE_APPEND ); return "ERROR_API: Gemini no respondió tras {$totalAttempts} intentos en " . count($keys) . " keys. Último error: {$lastError}"; } // ========================================== // Helper: Call Groq (GPT-OSS / Llama) // ========================================== function callGroqRoadmap($promptSystem, $promptUser, $model = "openai/gpt-oss-120b", &$db = null) { if (!defined('GROQ_API_KEY_TWO') || !GROQ_API_KEY_TWO) { return "ERROR_API: Faltan credenciales de GROQ en config/openrouter.php"; } $url = "https://api.groq.com/openai/v1/chat/completions"; $payload = [ "model" => $model, "messages" => [ ["role" => "system", "content" => $promptSystem], ["role" => "user", "content" => $promptUser] ], "temperature" => 0.3 ]; $isMultimodal = is_array($promptUser); if (!$isMultimodal) { $payload["response_format"] = ["type" => "json_object"]; } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Content-Type: application/json", "Authorization: Bearer " . GROQ_API_KEY_TWO ], CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE), CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 60 ]); if ($db) $db = null; $response = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($db === null) $db = getDB(true); if ($error) return "Error CURL: " . $error; $resArr = json_decode($response, true); if ($httpCode >= 400) { return "ERROR_API: " . ($resArr['error']['message'] ?? "Error Groq {$httpCode}"); } return $resArr['choices'][0]['message']['content'] ?? "Error parseando."; } // ========================================== // Función Helper: Instructor Operativo (GPT OSS 120B via Groq) // ========================================== function callGroqInstructor($systemPrompt, $userPrompt, &$db = null) { $responseText = callGeminiRoadmap( $systemPrompt, $userPrompt, ROADMAP_AI_DEFAULT_MODEL, true, $db, getRoadmapAiOptions(8192, 180) ); if (strpos($responseText, 'ERROR_API:') === 0 || strpos($responseText, 'Error CURL:') === 0) { return ['error' => $responseText]; } $content = trim($responseText); if ($content === '') { return ['error' => ROADMAP_AI_MODEL_LABEL . ' no respondió. Respuesta vacía.']; } return ['content' => $content]; } // ========================================== // Helper: Clean JSON // ========================================== function getCleanJsonRoadmap($str, &$db = null) { if (preg_match('/```json(.*?)```/is', $str, $matches)) { $str = $matches[1]; } else { $start = strpos($str, '{'); $end = strrpos($str, '}'); if ($start !== false && $end !== false) { $str = substr($str, $start, $end - $start + 1); } } $str = str_replace(["\n", "\r", "\t"], " ", $str); $str = preg_replace('/[[:cntrl:]]/', '', $str); $parsed = json_decode(trim($str), true); if (!$parsed && defined('GROQ_API_KEY')) { $repairSystem = "Eres un analizador de sintaxis JSON. Recibirás un JSON masivo que se ha cortado/truncado por límite de red. TU ÚNICA MISIÓN es devolver el MISMO JSON pero agregando los corchetes ] y llaves } necesarios al puro final para que vuelva a ser VÁLIDO. No agregues datos nuevos, ciérralo tal como quedó. Devuelve EXCLUSIVAMENTE el JSON resultante entero, sin preámbulos."; $repairedStr = callGroqRoadmap($repairSystem, $str, "meta-llama/llama-4-scout-17b-16e-instruct", $db); $repairedClean = $repairedStr; if (preg_match('/```json(.*?)```/is', $repairedClean, $m)) { $repairedClean = $m[1]; } else { $s = strpos($repairedClean, '{'); $e = strrpos($repairedClean, '}'); if ($s !== false && $e !== false) { $repairedClean = substr($repairedClean, $s, $e - $s + 1); } } $repairedClean = str_replace(["\n", "\r", "\t"], " ", $repairedClean); $repairedClean = preg_replace('/[[:cntrl:]]/', '', $repairedClean); $parsed = json_decode(trim($repairedClean), true); if (!$parsed) { if (!file_exists(__DIR__ . '/logs')) mkdir(__DIR__ . '/logs', 0777, true); file_put_contents(__DIR__ . '/logs/error_payload.txt', "JSON ROADMAP FALLÓ:\n\n" . $str . "\n\nJSON_LAST_ERROR: " . json_last_error_msg()); } } return $parsed; } // ========================================== // Helpers: Generacion de roadmaps por lotes // ========================================== function roadmapLog($filename, $message) { $logDir = __DIR__ . '/logs'; if (!file_exists($logDir)) { mkdir($logDir, 0777, true); } file_put_contents($logDir . '/' . $filename, date('Y-m-d H:i:s') . " | " . $message . "\n", FILE_APPEND); } function roadmapText($value, $fallback = '') { if (is_array($value) || is_object($value)) { $value = json_encode($value, JSON_UNESCAPED_UNICODE); } $text = trim((string) $value); return $text !== '' ? $text : $fallback; } function roadmapTextList($value, $limit = 5) { if (!is_array($value)) { return []; } $items = []; foreach ($value as $item) { if (is_array($item)) { $item = $item['description'] ?? $item['task'] ?? $item['title'] ?? json_encode($item, JSON_UNESCAPED_UNICODE); } $text = roadmapText($item); if ($text !== '') { $items[] = $text; } if (count($items) >= $limit) { break; } } return $items; } function roadmapSlugForYoutube($value, $anio_actual) { $value = strtolower(roadmapText($value, 'roadmap')); $value = preg_replace('/[^a-z0-9]+/i', '+', $value); $value = trim($value, '+'); if ($value === '') { $value = 'roadmap'; } return 'https://www.youtube.com/results?search_query=' . $value . '+' . $anio_actual; } function chooseRoadmapProjectModules($target, $cantidad_proyectos) { $projectModules = []; $target = max(ROADMAP_MIN_MODULES, (int) $target); $cantidad_proyectos = max(1, min((int) $cantidad_proyectos, $target - 1)); for ($i = 1; $i <= $cantidad_proyectos; $i++) { $candidate = (int) round(($i * ($target - 1)) / ($cantidad_proyectos + 1)) + 1; $candidate = max(2, min($target, $candidate)); if (in_array($candidate, $projectModules, true)) { $up = $candidate; $down = $candidate; while ($up <= $target || $down >= 2) { if ($up <= $target && !in_array($up, $projectModules, true)) { $candidate = $up; break; } if ($down >= 2 && !in_array($down, $projectModules, true)) { $candidate = $down; break; } $up++; $down--; } } if (!in_array($candidate, $projectModules, true)) { $projectModules[] = $candidate; } } for ($moduleNumber = 2; count($projectModules) < $cantidad_proyectos && $moduleNumber <= $target; $moduleNumber++) { if (!in_array($moduleNumber, $projectModules, true)) { $projectModules[] = $moduleNumber; } } sort($projectModules); return $projectModules; } function roadmapVersionNumberScore($version) { $version = preg_replace('/^[vV]/', '', trim((string) $version)); return $version !== '' ? $version : '0'; } function roadmapVersionTierKey($suffix) { $suffix = strtolower(trim(preg_replace('/\s+/', ' ', (string) $suffix))); if ($suffix === '') { return ''; } if (preg_match('/\b(flash[\s-]lite|flash|pro|lite|ultra|nano|mini|turbo|opus|sonnet|haiku)\b/i', $suffix, $match)) { return str_replace('-', ' ', strtolower($match[1])); } return ''; } function roadmapTechnologyBasePattern($base) { $parts = preg_split('/\s+/', trim((string) $base)); $quoted = []; foreach ($parts as $part) { if ($part !== '') { $pattern = preg_quote($part, '/'); $pattern = str_replace('\.js', '\.?\s*js', $pattern); $quoted[] = $pattern; } } return implode('\s+', $quoted); } function roadmapVersionTierPattern($tier) { $parts = preg_split('/[\s-]+/', trim((string) $tier)); $quoted = []; foreach ($parts as $part) { if ($part !== '') { $quoted[] = preg_quote($part, '/'); } } return implode('[\s_-]+', $quoted); } function roadmapCleanTechnologyBase($base) { $base = trim(preg_replace('/\s+/', ' ', (string) $base)); $base = preg_replace('/^[#\-–—:\s]+/', '', $base); $base = preg_replace('/^(modulo|módulo|clase|seccion|sección)\s+\d+\s*[:\-–—]?\s*/iu', '', $base); $prefixPattern = '/^(fundamentos|introduccion|introducción|creacion|creación|configuracion|configuración|desarrollo|integracion|integración|optimizacion|optimización|primeros pasos|bases|conceptos|arquitectura|testing|pruebas|seguridad|auditoria|auditoría)\s+(de|del|en|con|para|a|al|sobre)?\s*/iu'; for ($i = 0; $i < 2; $i++) { $cleaned = preg_replace($prefixPattern, '', $base); if ($cleaned === $base) { break; } $base = trim($cleaned); } if (preg_match('/\b(?:con|usando|sobre|para|en)\s+([A-Za-z][A-Za-z0-9.+#-]*(?:\s+[A-Za-z][A-Za-z0-9.+#-]*){0,3})$/iu', $base, $match)) { $candidate = trim($match[1]); if ($candidate !== '') { $base = $candidate; } } return $base; } function roadmapAddCanonicalVersionEntry(&$registry, $base, $version, $suffix, $canonical = '') { $base = roadmapCleanTechnologyBase($base); $version = trim((string) $version); $suffix = trim(preg_replace('/[\s_-]+/', ' ', (string) $suffix)); if ($base === '' || $version === '') { return; } $tier = roadmapVersionTierKey($suffix); $key = strtolower(trim(preg_replace('/\s+/', ' ', $base . ($tier !== '' ? ' ' . $tier : '')))); $canonical = roadmapText($canonical, trim($base . ' ' . $version . ($suffix !== '' ? ' ' . $suffix : ''))); $score = roadmapVersionNumberScore($version); if (!isset($registry[$key]) || version_compare($score, $registry[$key]['score'], '>') || (version_compare($score, $registry[$key]['score'], '=') && strlen($canonical) > strlen($registry[$key]['canonical']))) { $registry[$key] = [ 'base' => $base, 'tier' => $tier, 'canonical' => $canonical, 'score' => $score ]; } } function roadmapMergeVersionRegistries($primary, $secondary) { $merged = is_array($primary) ? $primary : []; $secondary = is_array($secondary) ? $secondary : []; foreach ($secondary as $entry) { if (!is_array($entry)) { continue; } roadmapAddCanonicalVersionEntry( $merged, $entry['base'] ?? '', $entry['score'] ?? '', $entry['tier'] ?? '', $entry['canonical'] ?? '' ); } uasort($merged, function ($a, $b) { return strlen($b['base'] . $b['tier']) <=> strlen($a['base'] . $a['tier']); }); return $merged; } function roadmapExtractCanonicalVersionRegistry($blueprint) { $registry = []; $blueprint = is_array($blueprint) ? $blueprint : []; $versionRegex = '/(?<![A-Za-z0-9])([A-Za-z][A-Za-z0-9.+#-]*(?:\s+[A-Za-z][A-Za-z0-9.+#-]*){0,5})\s+(v?\d+(?:\.\d+){0,3})(?:\s+((?:Pro|Flash(?:[\s-]Lite)?|Lite|Ultra|Nano|Mini|Turbo|Opus|Sonnet|Haiku)(?:\s+(?:Preview|Experimental|Beta|Alpha|Stable))?|Preview|Experimental|Beta|Alpha|LTS|Current|Stable|RC|Public\s+Beta))?/u'; $codeVersionRegex = '/(?<![A-Za-z0-9])([A-Za-z][A-Za-z0-9.+#]*?)[_-](v?\d+(?:\.\d+){0,3})(?:[_-]((?:pro|flash(?:[_-]lite)?|lite|ultra|nano|mini|turbo|opus|sonnet|haiku)(?:[_-](?:preview|experimental|beta|alpha|stable))?|preview|experimental|beta|alpha|lts|current|stable|rc))?\b/iu'; foreach ($blueprint as $module) { if (!is_array($module)) { continue; } $text = roadmapText($module['focus_technology'] ?? ''); $segments = preg_split('/[,;()]+/', $text); foreach ($segments as $segment) { if (preg_match_all($versionRegex, $segment, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { roadmapAddCanonicalVersionEntry($registry, $match[1] ?? '', $match[2] ?? '', $match[3] ?? ''); } } if (preg_match_all($codeVersionRegex, $segment, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { roadmapAddCanonicalVersionEntry($registry, $match[1] ?? '', $match[2] ?? '', $match[3] ?? '', $match[0] ?? ''); } } } } uasort($registry, function ($a, $b) { return strlen($b['base'] . $b['tier']) <=> strlen($a['base'] . $a['tier']); }); return $registry; } function roadmapExtractCanonicalVersionRegistryFromPlanner($entries) { if (!is_array($entries) || empty($entries)) { return []; } $blueprint = []; foreach ($entries as $entry) { if (is_array($entry)) { $technology = roadmapText($entry['technology'] ?? $entry['name'] ?? ''); $canonical = roadmapText($entry['canonical_name'] ?? $entry['canonical'] ?? $entry['official_name'] ?? ''); $version = roadmapText($entry['version'] ?? ''); if ($canonical === '' && $technology !== '') { $canonical = trim($technology . ' ' . $version); } elseif ($version !== '' && stripos($canonical, $version) === false) { $canonical = trim($canonical . ' ' . $version); } } else { $canonical = roadmapText($entry); } if ($canonical !== '') { $blueprint[] = ['focus_technology' => $canonical]; } } return roadmapExtractCanonicalVersionRegistry($blueprint); } function roadmapCanonicalizeVersionText($text, $registry) { $text = roadmapText($text); if ($text === '' || empty($registry)) { return $text; } $channelPattern = '(?:\s+(?:Preview|Experimental|Beta|Alpha|LTS|Current|Stable|RC|Public\s+Beta))*'; $codeChannelPattern = '(?:[_-](?:preview|experimental|beta|alpha|lts|current|stable|rc))*'; foreach ($registry as $entry) { $basePattern = roadmapTechnologyBasePattern($entry['base']); if ($basePattern === '') { continue; } if (!empty($entry['tier'])) { $tierPattern = roadmapVersionTierPattern($entry['tier']); $pattern = '/(?<![A-Za-z0-9])' . $basePattern . '\s+v?\d+(?:\.\d+){0,3}\s+' . $tierPattern . $channelPattern . '\b/iu'; } else { $pattern = '/(?<![A-Za-z0-9])' . $basePattern . '\s+v?\d+(?:\.\d+){0,3}' . $channelPattern . '\b/iu'; } $text = preg_replace($pattern, $entry['canonical'], $text); if (strpos($entry['base'], ' ') === false) { $codeBasePattern = preg_quote($entry['base'], '/'); if (!empty($entry['tier'])) { $tierPattern = roadmapVersionTierPattern($entry['tier']); $codePattern = '/(?<![A-Za-z0-9])' . $codeBasePattern . '[_-]v?\d+(?:\.\d+){0,3}[_-]' . $tierPattern . $codeChannelPattern . '\b/iu'; } else { $codePattern = '/(?<![A-Za-z0-9])' . $codeBasePattern . '[_-]v?\d+(?:\.\d+){0,3}' . $codeChannelPattern . '\b/iu'; } $text = preg_replace($codePattern, $entry['canonical'], $text); } } return $text; } function roadmapNeutralizeOutOfScopeAiModelNames($text, $moduleScope) { $text = roadmapText($text); $moduleScope = strtolower(roadmapText($moduleScope)); if (!preg_match('/\b(gemini|google\s+ai|google\s+genai)\b/i', $moduleScope)) { $text = preg_replace('/\bGemini\s+\d+(?:\.\d+)?(?:\s+(?:Pro|Flash(?:\s+Lite)?|Flash-Lite|Ultra|Nano|Lite|Preview|Experimental|Beta|Stable))*\b/iu', 'modelo Gemini verificado en el registro canonico', $text); } if (!preg_match('/\b(openai|chatgpt|gpt)\b/i', $moduleScope)) { $text = preg_replace('/\b(?:GPT[-\s]?\d+(?:\.\d+)?[A-Za-z-]*|ChatGPT[-\s]?\d+(?:\.\d+)?)\b/iu', 'modelo OpenAI verificado en el registro canonico', $text); } if (!preg_match('/\b(claude|anthropic)\b/i', $moduleScope)) { $text = preg_replace('/\bClaude\s+(?:Opus|Sonnet|Haiku)\s+\d+(?:\.\d+)?\b/iu', 'modelo Claude verificado en el registro canonico', $text); } return $text; } function roadmapApplyVersionRegistryToModules(&$modules, $registry) { if (empty($registry) || !is_array($modules)) { return; } foreach ($modules as &$module) { if (!is_array($module)) { continue; } foreach (['module_title', 'focus_technology', 'planned_project_name'] as $field) { if (isset($module[$field])) { $module[$field] = roadmapCanonicalizeVersionText($module[$field], $registry); } } $moduleScope = roadmapText($module['module_title'] ?? '') . ' ' . roadmapText($module['focus_technology'] ?? ''); if (!empty($module['module_research']) && is_array($module['module_research'])) { foreach (['version_status', 'continuity_note'] as $field) { if (isset($module['module_research'][$field])) { $module['module_research'][$field] = roadmapCanonicalizeVersionText($module['module_research'][$field], $registry); } } if (!empty($module['module_research']['evidence']) && is_array($module['module_research']['evidence'])) { foreach ($module['module_research']['evidence'] as &$source) { if (!is_array($source)) { continue; } foreach (['source_title', 'finding'] as $field) { if (isset($source[$field])) { $source[$field] = roadmapCanonicalizeVersionText($source[$field], $registry); } } } unset($source); } } if (!empty($module['execution_items']) && is_array($module['execution_items'])) { foreach ($module['execution_items'] as &$item) { $item = roadmapCanonicalizeVersionText($item, $registry); $item = roadmapNeutralizeOutOfScopeAiModelNames($item, $moduleScope); } unset($item); } if (!empty($module['consolidation_projects']) && is_array($module['consolidation_projects'])) { foreach ($module['consolidation_projects'] as &$project) { if (!is_array($project)) { continue; } foreach (['project_name', 'description'] as $field) { if (isset($project[$field])) { $project[$field] = roadmapCanonicalizeVersionText($project[$field], $registry); $project[$field] = roadmapNeutralizeOutOfScopeAiModelNames($project[$field], $moduleScope); } } } unset($project); } if (!empty($module['resources']) && is_array($module['resources'])) { foreach ($module['resources'] as &$resource) { if (is_array($resource) && isset($resource['title'])) { $resource['title'] = roadmapCanonicalizeVersionText($resource['title'], $registry); } } unset($resource); } } unset($module); } function roadmapApplyCanonicalVersionContinuity(&$modules, $blueprint) { $registry = roadmapExtractCanonicalVersionRegistry($blueprint); roadmapApplyVersionRegistryToModules($modules, $registry); return $registry; } function roadmapVersionRegistryForPrompt($registry) { $items = []; $registry = is_array($registry) ? $registry : []; foreach ($registry as $entry) { if (!is_array($entry)) { continue; } $label = trim(($entry['base'] ?? '') . (!empty($entry['tier']) ? ' ' . $entry['tier'] : '')); $items[] = [ 'technology' => $label, 'canonical' => $entry['canonical'] ?? '', 'version' => $entry['score'] ?? '' ]; } return $items; } function roadmapOfficialSourceDomainsForText($text) { $text = strtolower(roadmapText($text)); $requirements = []; if (preg_match('/\bGemini\s+\d|\bgemini[-_\s]?\d/i', $text)) { $requirements['Google Gemini'] = ['ai.google.dev', 'cloud.google.com', 'docs.cloud.google.com']; } if (preg_match('/\b(ChatGPT[-\s]?\d|GPT[-\s]?\d|o\d)\b/i', $text)) { $requirements['OpenAI/ChatGPT'] = ['openai.com', 'platform.openai.com', 'developers.openai.com', 'help.openai.com']; } if (preg_match('/\bClaude\s+(?:Opus|Sonnet|Haiku)?\s*\d/i', $text)) { $requirements['Anthropic/Claude'] = ['anthropic.com', 'docs.anthropic.com']; } return $requirements; } function roadmapUrlMatchesOfficialDomains($url, $domains) { $host = strtolower(parse_url(roadmapText($url), PHP_URL_HOST) ?: ''); if ($host === '' || strpos($host, 'vertexaisearch.cloud.google.com') !== false) { return false; } foreach ($domains as $domain) { $domain = strtolower($domain); $suffix = '.' . $domain; if ($host === $domain || substr($host, -strlen($suffix)) === $suffix) { return true; } } return false; } function roadmapFetchOfficialSourceText($url) { static $cache = []; $url = roadmapText($url); if ($url === '') { return null; } if (isset($cache[$url])) { return $cache[$url]; } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 12, CURLOPT_USERAGENT => 'ProgressGymRoadmapAudit/1.0' ]); $html = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($html === false || $httpCode >= 400) { $cache[$url] = null; return null; } $text = strtolower(html_entity_decode(strip_tags((string) $html), ENT_QUOTES | ENT_HTML5, 'UTF-8')); $text = preg_replace('/\s+/', ' ', $text); $cache[$url] = $text; return $text; } function roadmapAiModelClaimFragments($claimText) { $claimText = roadmapText($claimText); $fragments = []; $patterns = [ '/\bGPT[-\s]?\d+(?:\.\d+)?[A-Za-z-]*/i', '/\bGemini\s+\d+(?:\.\d+)?(?:\s+(?:Pro|Flash(?:\s+Lite)?|Flash-Lite|Ultra|Nano|Lite|Preview|Experimental|Beta|Stable))*\b/i', '/\bgemini[-_]\d+(?:\.\d+)?[-_](?:pro|flash(?:[-_]lite)?|lite|nano|ultra)\b/i', '/\bClaude\s+(?:Opus|Sonnet|Haiku)\s+\d+(?:\.\d+)?/i' ]; foreach ($patterns as $pattern) { if (preg_match_all($pattern, $claimText, $matches)) { foreach ($matches[0] as $match) { $fragments[] = trim($match); } } } if (empty($fragments) && !empty(roadmapOfficialSourceDomainsForText($claimText))) { $fragments[] = $claimText; } return array_values(array_unique(array_filter($fragments))); } function roadmapOfficialSourceContainsModelClaim($url, $claimText) { $fragments = roadmapAiModelClaimFragments($claimText); if (empty($fragments)) { return true; } $text = roadmapFetchOfficialSourceText($url); if ($text === null) { return true; } foreach ($fragments as $fragment) { $fragment = strtolower($fragment); $candidates = array_values(array_unique([ $fragment, str_replace(' flash lite', ' flash-lite', $fragment), str_replace('-', ' ', $fragment), str_replace('_', ' ', $fragment), str_replace(' ', '-', $fragment), str_replace([' ', '.'], ['-', '-'], $fragment), str_replace([' ', '-'], ['', ''], $fragment) ])); $found = false; foreach ($candidates as $candidate) { if ($candidate !== '' && strpos($text, $candidate) !== false) { $found = true; break; } } if (!$found) { return false; } } return true; } function roadmapProviderSourceIssues($batchData) { $issues = []; $batchData = is_array($batchData) ? $batchData : []; $registry = is_array($batchData['batch_version_registry'] ?? null) ? $batchData['batch_version_registry'] : []; foreach ($registry as $entry) { if (!is_array($entry)) { continue; } $claimText = trim( roadmapText($entry['technology'] ?? '') . ' ' . roadmapText($entry['canonical_name'] ?? $entry['canonical'] ?? '') . ' ' . roadmapText($entry['finding'] ?? '') ); foreach (roadmapOfficialSourceDomainsForText($claimText) as $provider => $domains) { if (!roadmapUrlMatchesOfficialDomains($entry['source_url'] ?? '', $domains)) { $issues[] = "{$provider}: batch_version_registry sin URL oficial directa para {$claimText}"; } elseif (!roadmapOfficialSourceContainsModelClaim($entry['source_url'] ?? '', roadmapText($entry['canonical_name'] ?? $entry['canonical'] ?? $claimText))) { $issues[] = "{$provider}: la fuente oficial no contiene literalmente el modelo canonico {$claimText}"; } } } $modules = is_array($batchData['modules'] ?? null) ? $batchData['modules'] : []; foreach ($modules as $module) { if (!is_array($module)) { continue; } $moduleNumber = $module['module_number'] ?? '?'; $claimText = trim( roadmapText($module['module_title'] ?? '') . ' ' . roadmapText($module['focus_technology'] ?? '') ); $requirements = roadmapOfficialSourceDomainsForText($claimText); if (empty($requirements)) { continue; } $evidence = is_array($module['module_research']['evidence'] ?? null) ? $module['module_research']['evidence'] : []; $fragments = roadmapAiModelClaimFragments($claimText); foreach ($requirements as $provider => $domains) { $hasOfficial = false; foreach ($evidence as $source) { if (is_array($source) && roadmapUrlMatchesOfficialDomains($source['source_url'] ?? '', $domains)) { $hasOfficial = true; break; } } if (!$hasOfficial) { $issues[] = "{$provider}: modulo {$moduleNumber} menciona proveedor/modelo sin evidencia oficial directa"; } } foreach ($fragments as $fragment) { foreach (roadmapOfficialSourceDomainsForText($fragment) as $provider => $domains) { $fragmentVerified = false; foreach ($evidence as $source) { if (!is_array($source) || !roadmapUrlMatchesOfficialDomains($source['source_url'] ?? '', $domains)) { continue; } if (roadmapOfficialSourceContainsModelClaim($source['source_url'] ?? '', $fragment)) { $fragmentVerified = true; break; } } if (!$fragmentVerified) { $issues[] = "{$provider}: modulo {$moduleNumber} no verifica literalmente el modelo {$fragment} en fuente oficial"; } } } } return array_values(array_unique($issues)); } function roadmapFilterVersionRegistryToModuleScope($registry, $batchBlueprint, $rawModules) { $registry = is_array($registry) ? $registry : []; $scopeParts = []; foreach ([$batchBlueprint, $rawModules] as $collection) { if (!is_array($collection)) { continue; } foreach ($collection as $module) { if (!is_array($module)) { continue; } $scopeParts[] = roadmapText($module['module_title'] ?? $module['title'] ?? ''); $scopeParts[] = roadmapText($module['focus_technology'] ?? $module['technology'] ?? ''); $scopeParts[] = roadmapText($module['planned_project_name'] ?? $module['project_name'] ?? ''); } } $scope = strtolower(implode(' ', $scopeParts)); $filtered = []; foreach ($registry as $entry) { if (!is_array($entry)) { continue; } $claim = strtolower( roadmapText($entry['technology'] ?? '') . ' ' . roadmapText($entry['canonical_name'] ?? $entry['canonical'] ?? '') ); if (preg_match('/\b(gemini|google\s+ai|google\s+genai)\b/i', $claim) && !preg_match('/\b(gemini|google\s+ai|google\s+genai)\b/i', $scope)) { continue; } if (preg_match('/\b(openai|chatgpt|gpt[-\s]?\d|o\d)\b/i', $claim) && !preg_match('/\b(openai|chatgpt|gpt)\b/i', $scope)) { continue; } if (preg_match('/\b(claude|anthropic)\b/i', $claim) && !preg_match('/\b(claude|anthropic)\b/i', $scope)) { continue; } $base = strtolower(roadmapCleanTechnologyBase($entry['technology'] ?? $entry['canonical_name'] ?? '')); $base = preg_replace('/\bv?\d+(?:\.\d+){0,3}\b/i', '', $base); $tokens = preg_split('/[^a-z0-9.+#]+/i', $base); $meaningfulToken = ''; foreach ($tokens as $token) { $token = strtolower(trim($token)); if ($token !== '' && strlen($token) >= 3 && !in_array($token, ['api', 'sdk', 'pro', 'lite', 'flash', 'model', 'current', 'stable'], true)) { $meaningfulToken = $token; break; } } if ($meaningfulToken !== '' && strpos($scope, $meaningfulToken) === false) { continue; } $filtered[] = $entry; } return $filtered; } function roadmapCanonicalizeRoadmapData(&$data, $registry) { if (empty($registry) || !is_array($data)) { return; } if (!empty($data['roadmap_metadata']) && is_array($data['roadmap_metadata'])) { foreach (['topic', 'vision'] as $field) { if (isset($data['roadmap_metadata'][$field])) { $data['roadmap_metadata'][$field] = roadmapCanonicalizeVersionText($data['roadmap_metadata'][$field], $registry); } } } foreach (['daily_drill', 'honest_level', 'final_boss_audit'] as $field) { if (isset($data[$field])) { $data[$field] = roadmapCanonicalizeVersionText($data[$field], $registry); } } foreach (['can_do_after', 'cannot_do_yet', 'professional_gap'] as $field) { if (!empty($data[$field]) && is_array($data[$field])) { foreach ($data[$field] as &$item) { $item = roadmapCanonicalizeVersionText($item, $registry); } unset($item); } } if (!empty($data['next_roadmap']) && is_array($data['next_roadmap'])) { foreach (['title', 'reason'] as $field) { if (isset($data['next_roadmap'][$field])) { $data['next_roadmap'][$field] = roadmapCanonicalizeVersionText($data['next_roadmap'][$field], $registry); } } } } function getRoadmapPromptTemplateSource() { return <<<'ROADMAP_PROMPT_EMBEDDED_2026' $PROMPT_SUPER_ROADMAP = "Eres ProgressGym Super Roadmap AI, un Tech Lead de Élite, Arquitecto de Aprendizaje Implacable y Analista del Mercado Tecnológico con acceso a Google Search en tiempo real. El día de hoy es {$fecha_hoy}. Tu misión es diseñar un plan de estudios intensivo, moderno y profesional, optimizado por Pareto, que maximice resultados prácticos sin sacrificar fundamentos, criterio técnico ni viabilidad. [INPUTS DEL USUARIO] - DISCIPLINA A DOMINAR: {$objetivo} - VOLUMEN ESPERADO: Exactamente {$cantidad_proyectos} Proyectos de consolidación distribuidos. - CONTEXTO / FILTRO DE RUIDO: {$contexto} - NIVEL DE PRESIÓN: {$nivel} - FECHA DE CORTE OBLIGATORIA PARA GROUNDING Y VERSIONES: {$fecha_hoy}. -REGLA DE HERRAMIENTA NOMBRADA: Si el usuario nombra una herramienta, plataforma o producto específico en el objetivo o contexto, ese producto debe ser el eje del roadmap; alternativas del ecosistema solo pueden aparecer como compatibilidad, comparación o integración secundaria. [FASE OBLIGATORIA DE DESCUBRIMIENTO Y GROUNDING EN TIEMPO REAL] PROHIBIDO EL HARDCODING: Está terminantemente prohibido asumir, inventar o utilizar nombres de herramientas o números de versión obsoletos o preconfigurados. Antes de estructurar el JSON, debes realizar búsquedas web explícitas para: 1. Rastrear los repositorios oficiales, changelogs y tags de lanzamiento de la disciplina solicitada para identificar la versión menor y el parche exacto más reciente a la fecha de hoy (Ejemplo formativo: 'Tecnología v90.9'). REGLA DE CORTE TEMPORAL DE VERSIONES: Usa {$fecha_hoy} como fecha máxima de corte. Apóyate totalmente en Google Search para identificar la versión oficial vigente, evitando usar tu conocimiento previo. Si encuentras varias, gana la de publicación más nueva (Ejemplo: tecnologia v10 no, tecnologia v10.8 sí). No sugieras versiones obsoletas. REGLA DE MODELOS IA: Busca en la web los modelos más recientes lanzados hasta {$fecha_hoy} en lugar de asumir modelos antiguos o inventar versiones futuras. No uses familias genéricas si existe un modelo oficial más específico. REGLA DE CATÁLOGO OFICIAL PARA MODELOS IA: Si mencionas modelos concretos de cualquier proveedor (Google Gemini, OpenAI, Anthropic, etc.), verifica el catálogo oficial vigente del proveedor y usa el nombre comercial y/o código de modelo exacto publicado allí. No elijas por memoria ni por ejemplos del prompt. Si existen canales stable, preview o experimental, elige el canal que encaje con el contexto y etiqueta el canal explícitamente. Si no puedes verificar el nombre exacto, usa una formulación genérica segura y marca el dato como pendiente de verificación. REGLA ESPECIAL CHATGPT / OPENAI: Si el roadmap menciona ChatGPT, OpenAI, GPT o modelos disponibles dentro de ChatGPT, debes contrastar fuentes oficiales de OpenAI/ChatGPT en tiempo real. No conviertas automáticamente modelos API en nombres de producto ChatGPT ni inventes numeraciones comerciales; usa exactamente el nombre que aparezca publicado oficialmente para el producto o modelo solicitado. REGLA DE PRODUCTO VS MODELO: No mezcles nombres de modelos con productos. Ejemplo abstracto: “Modelo X vN” no equivale a “Producto Y con agente Z”; cada uno debe verificarse y nombrarse por separado. REGLA DE REGISTRO CANÓNICO DE VERSIONES: Cuando verifiques una herramienta, runtime, framework, IDE, SDK o modelo con versión concreta, registra una única forma canónica y reutilízala literalmente en todo el roadmap. No alternes versiones minor/patch para la misma tecnología entre módulos, proyectos, recursos o resumen final salvo que el roadmap compare explícitamente dos canales distintos. REGLA DE INVESTIGACIÓN POR LOTE: Cada lote de módulos debe re-ejecutar grounding profundo para las tecnologías concretas que aparezcan en ese lote, comprobar documentación oficial, changelogs, releases o catálogos oficiales, y reconciliar el resultado con el registro canónico global. Si aparece una versión más reciente y oficial que la registrada, actualiza el registro de ese lote con la nueva forma canónica y úsala de forma consistente en todos los módulos ya generados y futuros. 2. LÍMITE DE TIEMPO Y VIABILIDAD REAL: El roadmap no debe superar 35 horas. Si el alcance solicitado es demasiado amplio, prioriza profundidad sobre cantidad. No aumentes módulos para aparentar completitud. Reduce, fusiona o marca explícitamente como "fuera de alcance" los temas que no puedan enseñarse con práctica realista dentro del límite. Cada módulo debe tener una duración suficiente para producir una habilidad verificable, no solo exposición superficial. 3. Realizar una verificación previa en tu ventana de razonamiento (High Thinking) listando las URLs de los changelogs encontrados antes de construir la estructura final. REGLAS ESTRICTAS DE ARQUITECTURA Y ASIMILACIÓN: 1. FORMATO ÚNICO: Responde EXCLUSIVAMENTE con JSON válido. Ningún texto fuera del JSON. 2. LÍMITE DE TIEMPO (35 HORAS) Y ANTI-COMPRESIÓN: Tienes 35 horas totales. Si el alcance es amplio, reduce o fusiona módulos antes que comprimir temas complejos; prioriza habilidades verificables sobre cantidad de secciones. 3. LA REGLA DEL CINTURÓN BLANCO: Si la disciplina es un lenguaje, los primeros módulos DEBEN aislar los fundamentos lógicos (variables, control de flujo) antes de introducir frameworks o abstracciones complejas. 4. LEY DE PARETO EXTREMA (80/20): Filtra la basura académica. Encontrando el 20% del stack actual que produce el 80% de los resultados comerciales en el mercado actual. 5. DESACOPLE MODULAR Y SECUENCIACIÓN: Separa tecnologías solo cuando sea necesario para comprensión y práctica real. A mayor ecosistema, mayor debe ser el cuidado de secuencia y priorización, no necesariamente el número de módulos. 6. CERO CONSUMO PASIVO: Todas las subtareas (items) deben ser órdenes de ejecución directas (ej. 'Implementar', 'Refactorizar', 'Configurar'). Máximo 5 tareas por módulo. 7. NUEVA LEY DEL ECOSISTEMA DE SUPERVIVENCIA 10x (DEMANDA METRIC-DRIVEN): Investiga activamente en la web cuáles son las herramientas accesorias de última generación, IDEs agénticos, entornos de desarrollo o plataformas de prototipado que están siendo masivamente demandadas por las empresas y acelerando la contratación en la disciplina solicitada (ej. si es frontend, busca herramientas de vanguardia comercial vigentes en el mercado; si es backend, entornos agénticos o runtimes de alto impacto). Integra estas herramientas de supervivencia de forma nativa en el roadmap sin mutar el núcleo de la disciplina principal. 8. REGLA DE SEGURIDAD, ÉTICA Y LABORATORIO CONTROLADO: Todo contenido de ciberseguridad, agentes autónomos, ejecución de código, scraping, automatización, explotación, credenciales, red team o análisis de vulnerabilidades debe formularse desde un enfoque defensivo, autorizado y reproducible en laboratorio. Incluye límites explícitos: alcance autorizado, entorno local/sandbox, no persistencia real, no evasión operativa, no extracción de credenciales reales y no uso contra terceros. Prioriza detección, mitigación, auditoría, hardening, reporting y validación. Todo roadmap de ciberseguridad debe incluir metodología profesional: alcance autorizado, reglas de engagement, clasificación de severidad, evidencia reproducible, mitigación, reporte técnico/ejecutivo y límites legales. 9. LEY DE RECURSOS VERIFICABLES: Cada módulo debe incluir exactamente 2 recursos: - Recurso oficial raíz o documentación oficial estable. - Consulta de búsqueda segura para material complementario. No inventes páginas profundas. Si no existe documentación oficial clara, usa una fuente primaria reconocida o marca el recurso como "pendiente de verificación". Si un recurso no es oficial o primario, no lo etiquetes como documentación oficial; úsalo solo como fuente secundaria o búsqueda complementaria. 10. REGLA DE AISLAMIENTO TECNOLÓGICO: PROHIBIDO agrupar tecnologías distintas en un solo módulo. Crea módulos independientes y cortos para dominar cada sintaxis o herramienta por separado antes de plantear su integración. 11. REGLA DE NIVEL REAL Y PRERREQUISITOS: Debes inferir el nivel real del roadmap según la dificultad de los temas: Principiante, Intermedio, Avanzado o Mixto. Incluye prerrequisitos mínimos obligatorios. Si el usuario pide "desde cero" pero el alcance incluye temas avanzados, divide el roadmap en fase base y fase aplicada, o declara que no es viable cubrirlo todo con profundidad en 35 horas. 12. REGLA DE ACTUALIDAD ESTABLE: Prioriza tecnologías actuales, pero distingue entre: - Núcleo estable: conceptos, patrones, fundamentos y herramientas consolidadas. - Capa cambiante: modelos, IDEs, librerías, versiones y plataformas recientes. No construyas el roadmap alrededor de nombres de modelos concretos salvo que el usuario lo pida. Enseña criterios de selección, benchmarking y adaptación. 13. REGLA ANTI-HYPE Y ANTI-PROMESAS ABSOLUTAS: Evita promesas como "dominar", "crear cualquier proyecto", "sin programar", "en minutos", "90% automático" o equivalentes, salvo que estén acotadas como prototipado o laboratorio. Toda promesa debe ser verificable con proyectos, tests, entregables o criterios de aceptación. 14. REGLA DE PROFUNDIDAD MÍNIMA: Si un módulo cubre un tema avanzado —arquitectura, seguridad, agentes, RAG, MCP, CI/CD, testing, explotación ética, despliegue o performance— debe incluir al menos: - una práctica verificable, - una validación o test, - un criterio de éxito, - y una advertencia de límites. Si no cabe en el tiempo, reduce alcance. [FASE OBLIGATORIA DE CONTROL DE CALIDAD] Antes de emitir el JSON final, valida internamente el roadmap contra estos checks: 1. ¿El alcance cabe realmente en 35 horas? 2. ¿Cada módulo produce una habilidad verificable? 3. ¿Hay prerrequisitos claros? 4. ¿Se evita hype, promesas absolutas y nombres de herramientas innecesarios? 5. ¿Los temas avanzados tienen práctica, validación y límites? 6. ¿Hay equilibrio entre fundamentos, herramientas, proyectos y seguridad? 7. ¿La actualidad está basada en fuentes, no en moda? Si falla algún punto, ajusta el roadmap antes de responder. No incluyas esta auditoría en la respuesta final salvo en campos JSON resumidos. REGLA DE CAMPOS OBLIGATORIOS INFERIDOS: Está prohibido usar "No especificado", "N/A", "por definir", null o valores vacíos en campos de nivel, viabilidad, prerrequisitos, riesgos, fuentes, versión o alcance. Si el usuario no proporciona el dato, debes inferirlo desde el objetivo, contexto, dificultad técnica y estructura del roadmap. ESTRUCTURA JSON OBLIGATORIA:" ROADMAP_PROMPT_EMBEDDED_2026; } function getRoadmapPromptTemplateSourceCompact() { return <<<'ROADMAP_PROMPT_COMPACT_2026' $PROMPT_SUPER_ROADMAP = "Eres ProgressGym Super Roadmap AI: arquitecto de aprendizaje tecnico, investigador con Google Search en tiempo real y auditor de versiones. Hoy es {$fecha_hoy}. Responde solo JSON valido cuando se te pida JSON. Inputs: - Disciplina: {$objetivo} - Proyectos de consolidacion: {$cantidad_proyectos} - Contexto del usuario: {$contexto} - Nivel de presion: {$nivel} - Corte temporal obligatorio: {$fecha_hoy} Contrato operativo: 1. Grounding primero: antes de proponer versiones, consulta fuentes oficiales actuales: documentacion raiz, releases/changelog, tags oficiales o catalogos de modelos. No uses memoria ni ejemplos del prompt como fuente. 2. Herramienta nombrada: si el usuario nombra un producto/plataforma, ese producto es el eje. Alternativas solo entran como compatibilidad, comparacion o integracion secundaria. 3. Registro canonico: cada tecnologia versionada, runtime, framework, SDK, IDE, producto IA o modelo debe tener una unica forma canonica. Reutilizala literalmente en todos los modulos, proyectos, recursos y resumen. No alternes major/minor/patch ni canales salvo comparacion explicita. 4. Investigacion por modulo: cada modulo debe incluir module_research con fecha {$fecha_hoy}, fuente oficial, hallazgo concreto, canal/version y nota de continuidad. Si un modulo menciona tecnologia sin version, marca sin_version y explica el criterio. 5. IA y productos: si mencionas Gemini, OpenAI, ChatGPT, Claude u otro proveedor, consulta el catalogo o pagina oficial vigente del proveedor. Diferencia producto, modelo API y modelo disponible dentro del producto. No inventes numeraciones comerciales; usa exactamente el nombre oficial publicado. 5.1. Evidencia de modelos IA: los nombres canonicos de modelos solo pueden salir de fuentes oficiales del proveedor (Gemini: ai.google.dev o cloud.google.com; OpenAI/ChatGPT: openai.com, platform/developers/help.openai.com; Claude: anthropic.com o docs.anthropic.com). No uses comparativas, benchmarks de terceros, blogs ajenos ni URLs de redireccion de grounding como prueba canonica. 5.2. No nombres modelos concretos en execution_items de modulos que no sean integraciones directas de ese proveedor. En modulos generales de Cursor usa 'modelo actual verificado en el registro canonico' o 'modelo configurado en Cursor' para evitar contaminar el roadmap con ejemplos obsoletos. 6. Alcance: genera 15 o 16 modulos como maximo, dentro de 35 horas. Prioriza menos modulos con mas investigacion y practica verificable, no amplitud superficial. 7. Secuencia: empieza por fundamentos si la disciplina lo requiere. Aisla tecnologias que tengan sintaxis, instalacion o mental model propio antes de integrarlas. 8. Items: cada execution_item debe ser accionable (Implementar, Configurar, Refactorizar, Validar, Auditar). Usa los items necesarios sin relleno, maximo 7 por modulo. 9. Recursos: cada modulo debe tener exactamente 2 recursos: una fuente oficial/primaria y una busqueda YouTube segura {$anio_actual}. No inventes paginas profundas. 10. Seguridad: cualquier contenido de seguridad, agentes, scraping, automatizacion o ejecucion de codigo debe ser defensivo, autorizado y reproducible en sandbox, con limites legales claros. 11. Anti-hype: evita promesas absolutas. Toda capacidad prometida debe tener practica, entregable, test, auditoria o criterio de aceptacion. 12. Profundidad minima: temas avanzados (arquitectura, RAG, MCP, CI/CD, seguridad, performance, agentes, despliegue) requieren practica verificable, validacion/test, criterio de exito y limite. 13. Calidad final: antes de responder valida internamente que el alcance cabe, cada modulo produce habilidad verificable, hay continuidad de versiones, recursos verificables y cero campos vacios tipo N/A/null/por definir. ESTRUCTURA JSON OBLIGATORIA:" ROADMAP_PROMPT_COMPACT_2026; } function loadRoadmapPromptTemplate($objetivo, $cantidad_proyectos, $nivel, $contexto, $fecha_hoy, $anio_actual) { $template = trim(getRoadmapPromptTemplateSourceCompact()); $template = preg_replace('/^\s*\$PROMPT_SUPER_ROADMAP\s*=\s*"/', '', $template); $template = preg_replace('/"\s*;?\s*$/', '', $template); return str_replace( ['{$fecha_hoy}', '{$anio_actual}', '{$objetivo}', '{$cantidad_proyectos}', '{$contexto}', '{$nivel}'], [$fecha_hoy, $anio_actual, $objetivo, $cantidad_proyectos, $contexto, $nivel], $template ); } function getRoadmapJsonContractPrompt($cantidad_proyectos, $anio_actual) { $model = ROADMAP_AI_DEFAULT_MODEL; $modelLabel = ROADMAP_AI_MODEL_LABEL; $batchSize = ROADMAP_BATCH_SIZE; $minModules = ROADMAP_MIN_MODULES; $maxModules = ROADMAP_MAX_MODULES; $maxItems = ROADMAP_MAX_ITEMS_PER_MODULE; return <<<PROMPT ESTRUCTURA JSON FINAL OBLIGATORIA: { "_thinking": "Resumen breve del criterio de arquitectura usado. No incluyas cadena de pensamiento privada.", "_generator": { "provider": "google_ai_studio", "model": "{$model}", "label": "{$modelLabel}", "thinking_level": "high", "temperature": 0, "top_p": 0.1, "grounding": true, "batch_size": {$batchSize} }, "roadmap_metadata": { "topic": "Disciplina exacta", "vision": "Vision concreta y profesional", "total_modules_created": {$minModules}, "total_projects_distributed": {$cantidad_proyectos}, "estimated_total_hours": 35, "model_used": "{$model}", "model_label": "{$modelLabel}" }, "modules": [ { "module_number": 1, "module_title": "Nombre del modulo", "focus_technology": "Tecnologia central", "module_research": { "verified_at": "fecha actual usada para investigar", "version_status": "vigente|lts|current|preview|sin_version", "evidence": [ { "source_title": "Fuente oficial consultada", "source_url": "https://...", "finding": "version/nombre/canal confirmado" } ], "continuity_note": "como conserva continuidad con el registro canonico" }, "estimated_module_hours": 1.25, "execution_items": [ "Orden ejecutable 1", "Orden ejecutable 2 hasta maximo {$maxItems}" ], "consolidation_projects": [ { "project_name": "Nombre", "description": "Que se construye aplicando solo lo aprendido hasta este modulo" } ], "resources": [ { "title": "Doc Raiz Oficial", "url": "https://..." }, { "title": "Busqueda YouTube Segura {$anio_actual}", "url": "https://www.youtube.com/results?search_query=PALABRA+CLAVE+{$anio_actual}" } ] } ], "daily_drill": "Practica diaria concreta", "can_do_after": ["Capacidad verificable 1"], "cannot_do_yet": ["Limite honesto 1"], "professional_gap": ["Siguiente brecha real 1"], "honest_level": "Nivel real al terminar", "next_roadmap": { "title": "Continuacion recomendada", "reason": "Por que sigue" }, "final_boss_audit": "Criterio final verificable" } PROMPT; } function callRoadmapJson($systemPrompt, $userPrompt, &$db, $maxOutputTokens = 16384) { $lastText = ''; for ($attempt = 1; $attempt <= 2; $attempt++) { $responseText = callGeminiRoadmap( $systemPrompt, $userPrompt, ROADMAP_AI_DEFAULT_MODEL, true, $db, getRoadmapAiOptions($maxOutputTokens, 300) ); if (strpos($responseText, 'ERROR_API:') === 0 || strpos($responseText, 'Error CURL:') === 0) { return ['error' => $responseText]; } $lastText = $responseText; $data = getCleanJsonRoadmap($responseText, $db); if (is_array($data)) { return ['data' => $data, 'raw' => $responseText]; } roadmapLog('error_payload.txt', "JSON invalido en intento {$attempt}: " . substr($responseText, 0, 1000)); $userPrompt .= "\n\nREINTENTO OBLIGATORIO: Tu respuesta anterior no fue JSON valido. Devuelve solo un objeto JSON parseable, sin markdown, sin comentarios y sin texto externo."; } return ['error' => 'La IA fallo creando JSON valido tras 2 intentos. RAW: ' . htmlspecialchars(substr($lastText, 0, 300))]; } function normalizeRoadmapBlueprint($planner, $cantidad_proyectos, $objetivo) { $rawBlueprint = $planner['module_blueprint'] ?? $planner['modules_blueprint'] ?? $planner['modules'] ?? []; if (!is_array($rawBlueprint) || empty($rawBlueprint)) { return ['error' => 'El blueprint no incluyo module_blueprint.']; } $rawBlueprint = array_values($rawBlueprint); $target = (int) ($planner['target_total_modules'] ?? ($planner['roadmap_metadata']['total_modules_created'] ?? count($rawBlueprint))); if ($target < ROADMAP_MIN_MODULES) { $target = ROADMAP_MIN_MODULES; } if ($target > ROADMAP_MAX_MODULES) { $target = ROADMAP_MAX_MODULES; } if (count($rawBlueprint) < ROADMAP_MIN_MODULES) { return ['error' => 'El blueprint no respeto el minimo de ' . ROADMAP_MIN_MODULES . ' modulos. Reintenta la generacion para mantener el estandar por modulo.']; } if (count($rawBlueprint) < $target) { $target = count($rawBlueprint); } if ($target < ROADMAP_MIN_MODULES) { return ['error' => 'El blueprint genero muy pocos modulos para trabajar por modulos investigados.']; } $modules = []; for ($i = 0; $i < $target; $i++) { $module = is_array($rawBlueprint[$i]) ? $rawBlueprint[$i] : []; $num = $i + 1; $hours = (float) ($module['estimated_module_hours'] ?? round(ROADMAP_MAX_TOTAL_HOURS / $target, 2)); if ($hours <= 0) { $hours = round(ROADMAP_MAX_TOTAL_HOURS / $target, 2); } $modules[] = [ 'module_number' => $num, 'module_title' => roadmapText($module['module_title'] ?? $module['title'] ?? '', "Modulo {$num}: {$objetivo}"), 'focus_technology' => roadmapText($module['focus_technology'] ?? $module['technology'] ?? $module['focus'] ?? '', $objetivo), 'estimated_module_hours' => $hours, 'project_slot' => !empty($module['project_slot']) || !empty($module['has_project']), 'planned_project_name' => roadmapText($module['planned_project_name'] ?? $module['project_name'] ?? '') ]; } $projectNamesByModule = []; $projectModules = []; $rawProjectDistribution = $planner['project_distribution'] ?? []; if (is_array($rawProjectDistribution)) { foreach ($rawProjectDistribution as $projectInfo) { if (!is_array($projectInfo)) { continue; } $moduleNumber = (int) ($projectInfo['module_number'] ?? 0); if ($moduleNumber >= 1 && $moduleNumber <= $target) { $projectModules[] = $moduleNumber; $projectNamesByModule[$moduleNumber] = roadmapText($projectInfo['project_name'] ?? $projectInfo['name'] ?? ''); } } } foreach ($modules as $module) { if (!empty($module['project_slot'])) { $projectModules[] = $module['module_number']; } } $projectModules = array_values(array_unique(array_filter($projectModules))); if (count($projectModules) !== $cantidad_proyectos) { $projectModules = chooseRoadmapProjectModules($target, $cantidad_proyectos); } foreach ($modules as &$module) { $num = $module['module_number']; $module['project_slot'] = in_array($num, $projectModules, true); if ($module['project_slot'] && $module['planned_project_name'] === '') { $projectIndex = array_search($num, $projectModules, true); $module['planned_project_name'] = $projectNamesByModule[$num] ?? ("Proyecto de consolidacion " . ($projectIndex + 1)); } } unset($module); $plannerRegistry = roadmapExtractCanonicalVersionRegistryFromPlanner($planner['technology_version_registry'] ?? []); if (!empty($plannerRegistry)) { foreach ($modules as &$module) { foreach (['module_title', 'focus_technology', 'planned_project_name'] as $field) { if (isset($module[$field])) { $module[$field] = roadmapCanonicalizeVersionText($module[$field], $plannerRegistry); } } } unset($module); } return [ 'target_total_modules' => $target, 'module_blueprint' => $modules, 'metadata' => $planner['roadmap_metadata'] ?? $planner['metadata'] ?? [], 'raw' => $planner ]; } function normalizeRoadmapBatchModules($rawModules, $batchBlueprint, $anio_actual) { $rawModules = is_array($rawModules) ? array_values($rawModules) : []; $normalized = []; $rawByNumber = []; foreach ($rawModules as $raw) { if (!is_array($raw)) { continue; } $num = (int) ($raw['module_number'] ?? $raw['project_number'] ?? 0); if ($num > 0) { $rawByNumber[$num] = $raw; } } foreach ($batchBlueprint as $idx => $blueprint) { $num = (int) $blueprint['module_number']; $raw = $rawByNumber[$num] ?? ($rawModules[$idx] ?? []); if (!is_array($raw)) { $raw = []; } $items = roadmapTextList($raw['execution_items'] ?? $raw['tasks'] ?? [], ROADMAP_MAX_ITEMS_PER_MODULE); if (empty($items)) { $items[] = 'Implementar una practica guiada sobre ' . $blueprint['focus_technology']; $items[] = 'Validar el aprendizaje con una entrega observable'; } $resources = []; $rawResources = $raw['resources'] ?? []; if (is_array($rawResources)) { foreach ($rawResources as $resource) { if (!is_array($resource)) { continue; } $title = roadmapText($resource['title'] ?? '', 'Recurso oficial'); $url = roadmapText($resource['url'] ?? ''); if ($url !== '') { $resources[] = ['title' => $title, 'url' => $url]; } if (count($resources) >= 2) { break; } } } if (count($resources) < 1) { $resources[] = ['title' => 'Doc Raiz Oficial', 'url' => 'https://cloud.google.com/docs']; } if (count($resources) < 2) { $resources[] = [ 'title' => 'Busqueda YouTube Segura', 'url' => roadmapSlugForYoutube($blueprint['focus_technology'], $anio_actual) ]; } $projects = []; if (!empty($blueprint['project_slot'])) { $rawProjects = $raw['consolidation_projects'] ?? []; if (is_array($rawProjects)) { foreach ($rawProjects as $project) { if (!is_array($project)) { continue; } $projects[] = [ 'project_name' => roadmapText($project['project_name'] ?? $project['name'] ?? '', $blueprint['planned_project_name']), 'description' => roadmapText($project['description'] ?? $project['desc'] ?? '', 'Construir una entrega verificable con lo aprendido hasta este modulo.') ]; break; } } if (empty($projects)) { $projects[] = [ 'project_name' => $blueprint['planned_project_name'], 'description' => 'Construir una entrega verificable usando solo lo aprendido hasta el modulo ' . $num . '.' ]; } } $rawResearch = is_array($raw['module_research'] ?? null) ? $raw['module_research'] : []; $evidence = []; $rawEvidence = $rawResearch['evidence'] ?? $rawResearch['sources'] ?? []; if (is_array($rawEvidence)) { foreach ($rawEvidence as $source) { if (!is_array($source)) { continue; } $sourceUrl = roadmapText($source['source_url'] ?? $source['url'] ?? ''); if ($sourceUrl === '') { continue; } $evidence[] = [ 'source_title' => roadmapText($source['source_title'] ?? $source['title'] ?? '', 'Fuente oficial verificada'), 'source_url' => $sourceUrl, 'finding' => roadmapText($source['finding'] ?? $source['summary'] ?? '', 'Version o estado verificado en fuente oficial.') ]; if (count($evidence) >= 4) { break; } } } if (empty($evidence)) { $evidence[] = [ 'source_title' => $resources[0]['title'] ?? 'Fuente oficial verificada', 'source_url' => $resources[0]['url'] ?? 'https://cloud.google.com/docs', 'finding' => 'Fuente primaria usada para verificar el modulo.' ]; } $normalized[] = [ 'module_number' => $num, 'module_title' => roadmapText($raw['module_title'] ?? $raw['title'] ?? '', $blueprint['module_title']), 'focus_technology' => roadmapText($blueprint['focus_technology'] ?? '', roadmapText($raw['focus_technology'] ?? $raw['technology'] ?? '', '')), 'module_research' => [ 'verified_at' => roadmapText($rawResearch['verified_at'] ?? $rawResearch['date'] ?? '', defined('CURRENT_AI_DATE') ? CURRENT_AI_DATE : date('Y-m-d')), 'version_status' => roadmapText($rawResearch['version_status'] ?? $rawResearch['status'] ?? '', 'vigente'), 'evidence' => $evidence, 'continuity_note' => roadmapText($rawResearch['continuity_note'] ?? $rawResearch['continuity'] ?? '', 'Mantiene la forma canonica definida para este roadmap.') ], 'estimated_module_hours' => (float) ($raw['estimated_module_hours'] ?? $blueprint['estimated_module_hours']), 'execution_items' => $items, 'consolidation_projects' => $projects, 'resources' => array_slice($resources, 0, 2) ]; } return $normalized; } function capRoadmapHours(&$modules) { $total = 0; foreach ($modules as $module) { $total += (float) ($module['estimated_module_hours'] ?? 0); } if ($total <= ROADMAP_MAX_TOTAL_HOURS || $total <= 0) { return round($total, 2); } $factor = ROADMAP_MAX_TOTAL_HOURS / $total; $newTotal = 0; foreach ($modules as &$module) { $module['estimated_module_hours'] = round(max(0.5, ((float) $module['estimated_module_hours']) * $factor), 2); $newTotal += $module['estimated_module_hours']; } unset($module); while ($newTotal > ROADMAP_MAX_TOTAL_HOURS && !empty($modules)) { for ($i = count($modules) - 1; $i >= 0 && $newTotal > ROADMAP_MAX_TOTAL_HOURS; $i--) { if ($modules[$i]['estimated_module_hours'] > 0.5) { $modules[$i]['estimated_module_hours'] = round($modules[$i]['estimated_module_hours'] - 0.01, 2); $newTotal = round($newTotal - 0.01, 2); } } break; } return round($newTotal, 2); } function roadmapPreviousSummary($modules) { $summary = []; foreach (array_slice($modules, -8) as $module) { $summary[] = [ 'module_number' => $module['module_number'] ?? null, 'module_title' => $module['module_title'] ?? '', 'focus_technology' => $module['focus_technology'] ?? '' ]; } return json_encode($summary, JSON_UNESCAPED_UNICODE); } function buildRoadmapWithBatches($objetivo, $cantidad_proyectos, $nivel, $contexto, $fecha_hoy, $anio_actual, &$db) { $basePrompt = loadRoadmapPromptTemplate($objetivo, $cantidad_proyectos, $nivel, $contexto, $fecha_hoy, $anio_actual); $contract = getRoadmapJsonContractPrompt($cantidad_proyectos, $anio_actual); $minModules = ROADMAP_MIN_MODULES; $maxModules = ROADMAP_MAX_MODULES; $maxItems = ROADMAP_MAX_ITEMS_PER_MODULE; $plannerSystem = $basePrompt . "\n\nMODO BLUEPRINT COMPACTO: genera solo arquitectura de {$minModules}-{$maxModules} modulos. Usa grounding web profundo, registra versiones oficiales y no inventes versiones."; $plannerUser = <<<PROMPT Genera el BLUEPRINT del roadmap para: {$objetivo} Parametros: - Proyectos de consolidacion exactos: {$cantidad_proyectos} - Nivel de presion: {$nivel} - Contexto del usuario: {$contexto} - Fecha actual: {$fecha_hoy} Devuelve EXCLUSIVAMENTE JSON valido con esta estructura: { "roadmap_metadata": { "topic": "{$objetivo}", "vision": "vision concreta", "total_modules_created": {$minModules}, "total_projects_distributed": {$cantidad_proyectos}, "estimated_total_hours": 35 }, "target_total_modules": "numero entero entre {$minModules} y {$maxModules} segun amplitud real de la tecnologia", "technology_version_registry": [ { "technology": "nombre exacto de herramienta, runtime, SDK, IDE o modelo", "canonical_name": "forma canonica exacta verificada para reutilizar literalmente", "version": "version exacta si aplica", "channel": "stable|lts|current|preview|experimental|official", "source_type": "official_docs|release_notes|model_catalog|changelog", "source_url": "URL oficial usada para verificar" } ], "module_blueprint": [ { "module_number": 1, "module_title": "nombre corto", "focus_technology": "tecnologia/concepto central aislado", "estimated_module_hours": 1.25, "project_slot": false, "planned_project_name": "" } ], "project_distribution": [ { "module_number": 5, "project_name": "nombre del proyecto" } ], "daily_drill": "practica diaria", "can_do_after": ["capacidad verificable"], "cannot_do_yet": ["limite honesto"], "professional_gap": ["brecha profesional"], "honest_level": "nivel real al terminar", "next_roadmap": { "title": "continuacion", "reason": "razon" }, "final_boss_audit": "criterio final" } Reglas: - No generes execution_items todavia. - module_blueprint debe tener exactamente target_total_modules entradas. - target_total_modules debe estar entre {$minModules} y {$maxModules}. - La suma de estimated_module_hours debe ser <= 35. - project_distribution debe contener exactamente {$cantidad_proyectos} proyectos. - Aisla tecnologias: no agrupes herramientas distintas en un modulo si necesitan sintaxis propia. - technology_version_registry debe declarar una sola forma canonica por tecnologia versionada, proveedor de IA, runtime, framework, SDK o IDE. - module_blueprint.focus_technology debe reutilizar literalmente esas formas canonicas; no cambies minor/patch entre modulos ni entre lotes. - Para modelos IA, investiga el catalogo oficial vigente del proveedor y registra el nombre exacto publicado; no uses nombres sugeridos por memoria ni ejemplos del prompt como fuente de verdad. - Para ChatGPT/OpenAI/GPT, verifica fuentes oficiales de OpenAI o ChatGPT y diferencia producto, modelo API y modelo disponible en ChatGPT. Si no existe una numeracion oficial exacta, no la inventes. - Para Gemini, OpenAI/ChatGPT y Claude, source_url debe ser una URL oficial directa del proveedor; no uses URLs de redireccion de grounding ni comparativas de otro proveedor como evidencia canonica. - No nombres modelos concretos de Gemini/OpenAI/Claude en execution_items si el modulo no trata directamente ese proveedor. Usa una referencia generica al modelo actual verificado. - Para Node.js, npm, frameworks, SDKs, IDEs y herramientas con canal LTS/current/stable/preview, registra el canal elegido y no alternes entre canales dentro del mismo roadmap. - Antes de cerrar el blueprint, revisa internamente que ninguna tecnologia aparezca con dos versiones distintas. Si detectas duplicidad, conserva solo la version oficial mas reciente o el canal explicitamente elegido. - El roadmap final tendra hasta {$maxItems} execution_items por modulo, asi que diseña menos modulos y mas densidad practica. PROMPT; $plannerResult = callRoadmapJson($plannerSystem, $plannerUser, $db, 12000); if (!empty($plannerResult['error'])) { return ['error' => $plannerResult['error']]; } $blueprintResult = normalizeRoadmapBlueprint($plannerResult['data'], $cantidad_proyectos, $objetivo); if (!empty($blueprintResult['error'])) { $plannerUser .= "\n\nREINTENTO DE CALIDAD: El blueprint anterior no respeto el rango {$minModules}-{$maxModules} o no incluyo suficientes modulos. Devuelve exactamente entre {$minModules} y {$maxModules} entradas en module_blueprint y exactamente {$cantidad_proyectos} proyectos en project_distribution."; $plannerResult = callRoadmapJson($plannerSystem, $plannerUser, $db, 12000); if (!empty($plannerResult['error'])) { return ['error' => $plannerResult['error']]; } $blueprintResult = normalizeRoadmapBlueprint($plannerResult['data'], $cantidad_proyectos, $objetivo); if (!empty($blueprintResult['error'])) { return ['error' => $blueprintResult['error']]; } } $blueprint = $blueprintResult['module_blueprint']; $target = $blueprintResult['target_total_modules']; $lockedVersionRegistry = roadmapMergeVersionRegistries( roadmapExtractCanonicalVersionRegistry($blueprint), roadmapExtractCanonicalVersionRegistryFromPlanner($blueprintResult['raw']['technology_version_registry'] ?? []) ); roadmapApplyVersionRegistryToModules($blueprint, $lockedVersionRegistry); $allModules = []; $batchNumber = 0; for ($start = 1; $start <= $target; $start += ROADMAP_BATCH_SIZE) { $batchNumber++; $end = min($target, $start + ROADMAP_BATCH_SIZE - 1); $batchBlueprint = array_values(array_filter($blueprint, function ($module) use ($start, $end) { return $module['module_number'] >= $start && $module['module_number'] <= $end; })); $lockedRegistryJson = json_encode(roadmapVersionRegistryForPrompt($lockedVersionRegistry), JSON_UNESCAPED_UNICODE); $batchSystem = $basePrompt . "\n\n" . $contract . "\n\nMODO MODULO CON GROUNDING PROFUNDO: genera solo el modulo solicitado. Investiga sus versiones desde cero con Google Search, registra evidencia oficial y conserva continuidad global. No regeneres modulos previos."; $batchUser = "Genera EXCLUSIVAMENTE los modulos {$start}-{$end} de {$target}.\n\n" . "REGISTRO CANONICO GLOBAL BLOQUEADO HASTA AHORA:\n" . $lockedRegistryJson . "\n\n" . "BLUEPRINT DEL MODULO:\n" . json_encode($batchBlueprint, JSON_UNESCAPED_UNICODE) . "\n\n" . "RESUMEN DE MODULOS YA GENERADOS:\n" . roadmapPreviousSummary($allModules) . "\n\n" . "Devuelve solo JSON con esta forma:\n" . "{ \"batch_version_registry\": [ { \"technology\": \"nombre\", \"canonical_name\": \"forma oficial verificada\", \"version\": \"version exacta\", \"channel\": \"stable|lts|current|preview|official|sin_version\", \"source_type\": \"official_docs|release_notes|model_catalog|changelog|official_product_page\", \"source_url\": \"URL oficial\" } ], \"modules\": [ { \"module_number\": 1, \"module_title\": \"...\", \"focus_technology\": \"...\", \"module_research\": { \"verified_at\": \"{$fecha_hoy}\", \"version_status\": \"vigente|lts|current|preview|sin_version\", \"evidence\": [ { \"source_title\": \"Fuente oficial\", \"source_url\": \"https://...\", \"finding\": \"hallazgo concreto\" } ], \"continuity_note\": \"continuidad aplicada\" }, \"estimated_module_hours\": 1.25, \"execution_items\": [\"...\"], \"consolidation_projects\": [], \"resources\": [] } ], \"batch_audit\": \"control de grounding y continuidad\" }\n\n" . "Reglas del modulo:\n" . "- Usa exactamente los module_number del blueprint.\n" . "- Cada modulo debe tener entre 4 y {$maxItems} execution_items, todos como ordenes accionables y sin relleno.\n" . "- resources debe tener exactamente 2 enlaces: doc raiz oficial y busqueda YouTube segura {$anio_actual}.\n" . "- consolidation_projects debe estar vacio salvo si project_slot=true en el blueprint; ahi incluye exactamente 1 proyecto.\n" . "- Ejecuta grounding profundo para la tecnologia concreta de este modulo: documentacion oficial, releases/changelog, catalogo de modelos o pagina oficial del producto. No uses memoria.\n" . "- module_research es obligatorio en cada modulo y debe indicar fecha {$fecha_hoy}, fuente oficial y continuidad con el registro canonico.\n" . "- Si la investigacion del modulo confirma una version oficial mas reciente que el registro global, declarala en batch_version_registry y usa esa forma canonica en todo el modulo.\n" . "- Si no confirma una version mas reciente, conserva exactamente la forma canonica del registro global o del blueprint. No cambies major/minor/patch entre modulos.\n" . "- Para ChatGPT/OpenAI/GPT, usa fuentes oficiales de OpenAI/ChatGPT y no conviertas nombres de modelos API en nombres de producto si la fuente oficial no lo dice literalmente.\n" . "- Para Gemini, OpenAI/ChatGPT y Claude, batch_version_registry.source_url y module_research.evidence.source_url deben ser URLs oficiales directas del proveedor. No uses vertexaisearch.cloud.google.com ni comparativas de otros proveedores como evidencia canonica.\n" . "- No pongas nombres concretos de modelos IA en execution_items si el modulo no trata directamente ese proveedor. En modulos generales usa 'modelo actual verificado' o 'modelo configurado en Cursor'.\n" . "- No repitas temas de modulos anteriores y no adelantes contenido no habilitado por el orden."; $batchResult = callRoadmapJson($batchSystem, $batchUser, $db, 12000); if (!empty($batchResult['error'])) { return ['error' => "Fallo generando lote {$start}-{$end}: " . $batchResult['error']]; } $batchResult['data']['batch_version_registry'] = roadmapFilterVersionRegistryToModuleScope( $batchResult['data']['batch_version_registry'] ?? [], $batchBlueprint, $batchResult['data']['modules'] ?? [] ); $rawModulesForResearchCheck = $batchResult['data']['modules'] ?? []; $needsResearchRetry = false; $providerSourceIssues = roadmapProviderSourceIssues($batchResult['data'] ?? []); if (!empty($providerSourceIssues)) { $needsResearchRetry = true; } if (is_array($rawModulesForResearchCheck)) { foreach ($rawModulesForResearchCheck as $rawModuleForResearchCheck) { $research = is_array($rawModuleForResearchCheck['module_research'] ?? null) ? $rawModuleForResearchCheck['module_research'] : []; $evidence = $research['evidence'] ?? $research['sources'] ?? []; if (empty($research['verified_at']) || empty($evidence) || !is_array($evidence)) { $needsResearchRetry = true; break; } } } else { $needsResearchRetry = true; } if ($needsResearchRetry) { $retryIssues = !empty($providerSourceIssues) ? ("\nProblemas de fuente detectados:\n- " . implode("\n- ", $providerSourceIssues)) : ''; $retryBatchUser = $batchUser . "\n\nREINTENTO ESTRICTO DE GROUNDING: El modulo debe incluir batch_version_registry y cada modulo debe incluir module_research con verified_at={$fecha_hoy}, evidence con fuente oficial directa y continuity_note. Para modelos IA no aceptes redirects de grounding ni comparativas de otros proveedores como fuente canonica. Si el modulo no trata directamente ese proveedor, elimina nombres concretos de modelos IA de execution_items y usa referencia generica al modelo actual verificado. Re-investiga con grounding oficial la tecnologia del modulo y devuelve esos campos completos antes de modules." . $retryIssues; $batchResult = callRoadmapJson($batchSystem, $retryBatchUser, $db, 12000); if (!empty($batchResult['error'])) { return ['error' => "Fallo generando lote {$start}-{$end} tras reintento de grounding: " . $batchResult['error']]; } $batchResult['data']['batch_version_registry'] = roadmapFilterVersionRegistryToModuleScope( $batchResult['data']['batch_version_registry'] ?? [], $batchBlueprint, $batchResult['data']['modules'] ?? [] ); if (empty($batchResult['data']['modules'])) { return ['error' => "Fallo generando modulo {$start}-{$end}: la IA no cumplio el contrato de investigacion por modulo."]; } $providerSourceIssuesAfterRetry = roadmapProviderSourceIssues($batchResult['data'] ?? []); if (!empty($providerSourceIssuesAfterRetry)) { $secondRetryUser = $batchUser . "\n\nSEGUNDO REINTENTO ESTRICTO: El intento anterior sigue citando modelos IA no verificados oficialmente. Problemas:\n- " . implode("\n- ", $providerSourceIssuesAfterRetry) . "\n\nRehaz el modulo sin esos modelos. Si no puedes verificar un modelo concreto en una fuente oficial directa donde aparezca literalmente, elimina el nombre concreto y usa 'modelo actual verificado en el catalogo oficial'."; $batchResult = callRoadmapJson($batchSystem, $secondRetryUser, $db, 12000); if (!empty($batchResult['error'])) { return ['error' => "Fallo generando lote {$start}-{$end} tras segundo reintento de grounding: " . $batchResult['error']]; } $batchResult['data']['batch_version_registry'] = roadmapFilterVersionRegistryToModuleScope( $batchResult['data']['batch_version_registry'] ?? [], $batchBlueprint, $batchResult['data']['modules'] ?? [] ); if (empty($batchResult['data']['modules'])) { return ['error' => "Fallo generando modulo {$start}-{$end}: la IA no cumplio el contrato de investigacion por modulo."]; } $providerSourceIssuesAfterSecondRetry = roadmapProviderSourceIssues($batchResult['data'] ?? []); if (!empty($providerSourceIssuesAfterSecondRetry)) { return ['error' => "Fallo generando modulo {$start}-{$end}: fuentes oficiales invalidas para modelos IA. " . implode(' | ', $providerSourceIssuesAfterSecondRetry)]; } } } $rawModules = $batchResult['data']['modules'] ?? []; if (!is_array($rawModules) || empty($rawModules)) { return ['error' => "Fallo generando lote {$start}-{$end}: la IA no devolvio modules."]; } $batchRegistry = roadmapExtractCanonicalVersionRegistryFromPlanner($batchResult['data']['batch_version_registry'] ?? []); if (!empty($batchRegistry)) { $lockedVersionRegistry = roadmapMergeVersionRegistries($lockedVersionRegistry, $batchRegistry); roadmapApplyVersionRegistryToModules($blueprint, $lockedVersionRegistry); roadmapApplyVersionRegistryToModules($allModules, $lockedVersionRegistry); } $normalizedBatch = normalizeRoadmapBatchModules($rawModules, $batchBlueprint, $anio_actual); roadmapApplyVersionRegistryToModules($normalizedBatch, $lockedVersionRegistry); $allModules = array_merge($allModules, $normalizedBatch); roadmapApplyVersionRegistryToModules($allModules, $lockedVersionRegistry); roadmapLog('quality_gate_log.txt', "Modulo batch {$batchNumber} generado: modulos {$start}-{$end}"); } usort($allModules, function ($a, $b) { return ($a['module_number'] ?? 0) <=> ($b['module_number'] ?? 0); }); $versionRegistry = roadmapMergeVersionRegistries($lockedVersionRegistry, roadmapApplyCanonicalVersionContinuity($allModules, $blueprint)); roadmapApplyVersionRegistryToModules($allModules, $versionRegistry); $estimatedTotal = capRoadmapHours($allModules); $metadata = $blueprintResult['metadata']; $projectCount = 0; foreach ($allModules as $module) { $projectCount += count($module['consolidation_projects'] ?? []); } $data = [ '_thinking' => 'Roadmap construido por blueprint compacto y generacion modulo a modulo con grounding profundo para reducir perdida de continuidad y versiones obsoletas.', '_generator' => [ 'provider' => 'google_ai_studio', 'model' => ROADMAP_AI_DEFAULT_MODEL, 'label' => ROADMAP_AI_MODEL_LABEL, 'thinking_level' => 'high', 'temperature' => 0, 'top_p' => 0.1, 'grounding' => true, 'batch_size' => ROADMAP_BATCH_SIZE, 'batch_count' => (int) ceil($target / ROADMAP_BATCH_SIZE), 'generated_at' => defined('CURRENT_AI_ISO_DATE') ? CURRENT_AI_ISO_DATE : date('Y-m-d') ], 'roadmap_metadata' => [ 'topic' => roadmapText($metadata['topic'] ?? '', $objetivo), 'vision' => roadmapText($metadata['vision'] ?? '', 'Dominar ' . $objetivo . ' con una ruta intensiva, actual y verificable.'), 'total_modules_created' => count($allModules), 'total_projects_distributed' => $projectCount, 'estimated_total_hours' => $estimatedTotal, 'model_used' => ROADMAP_AI_DEFAULT_MODEL, 'model_label' => ROADMAP_AI_MODEL_LABEL, 'batch_size' => ROADMAP_BATCH_SIZE ], 'modules' => $allModules, 'daily_drill' => roadmapText($blueprintResult['raw']['daily_drill'] ?? '', 'Practicar 45 minutos diarios y cerrar cada sesion con una entrega verificable.'), 'can_do_after' => roadmapTextList($blueprintResult['raw']['can_do_after'] ?? [], 7), 'cannot_do_yet' => roadmapTextList($blueprintResult['raw']['cannot_do_yet'] ?? [], 7), 'professional_gap' => roadmapTextList($blueprintResult['raw']['professional_gap'] ?? [], 7), 'honest_level' => roadmapText($blueprintResult['raw']['honest_level'] ?? '', 'Base profesional inicial con proyectos verificables.'), 'next_roadmap' => is_array($blueprintResult['raw']['next_roadmap'] ?? null) ? $blueprintResult['raw']['next_roadmap'] : [ 'title' => 'Continuacion avanzada', 'reason' => 'Profundizar despues de completar entregas reales sin repetir fundamentos.' ], 'final_boss_audit' => roadmapText($blueprintResult['raw']['final_boss_audit'] ?? '', 'Construir y defender un proyecto final funcional, documentado y verificable.') ]; roadmapCanonicalizeRoadmapData($data, $versionRegistry); roadmapLog('quality_gate_log.txt', "Roadmap final: " . count($allModules) . " modulos, {$projectCount} proyectos, {$estimatedTotal} horas, modelo " . ROADMAP_AI_DEFAULT_MODEL); return ['data' => $data]; } // ========================================== // Ruta: Generar Roadmap // ========================================== if ($action === 'generar_roadmap') { $objetivo = trim($_POST['objetivo'] ?? ''); $cantidad_proyectos = (int) ($_POST['cantidad_proyectos'] ?? 3); $nivel = trim($_POST['nivel'] ?? 'Normal'); $contexto = trim($_POST['contexto'] ?? ''); if (empty($objetivo)) { echo json_encode(['success' => false, 'error' => 'La disciplina es requerida.']); exit; } // Limitar input para no reventar cuota de tokens if (mb_strlen($objetivo) > 500) { $objetivo = mb_substr($objetivo, 0, 500); } if (mb_strlen($contexto) > 500) { $contexto = mb_substr($contexto, 0, 500); } if ($cantidad_proyectos < 1) $cantidad_proyectos = 1; if ($cantidad_proyectos > 10) $cantidad_proyectos = 10; $generationResult = buildRoadmapWithBatches($objetivo, $cantidad_proyectos, $nivel, $contexto, $fecha_hoy, $anio_actual, $db); if (!empty($generationResult['error'])) { echo json_encode(['success' => false, 'error' => $generationResult['error']]); exit; } $data = $generationResult['data']; $titulo = $data['metadata']['topic'] ?? $data['roadmap_metadata']['topic'] ?? $objetivo; $vision = $data['metadata']['vision'] ?? $data['roadmap_metadata']['vision'] ?? ''; try { $db->beginTransaction(); $stmt = $db->prepare("INSERT INTO roadmaps (usuario_id, duo_id, titulo, disciplina, cantidad_proyectos, nivel_presion, contexto, vision, json_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([ $uid, $duo, $titulo, $objetivo, $cantidad_proyectos, $nivel, $contexto, $vision, json_encode($data, JSON_UNESCAPED_UNICODE) ]); $roadmap_id = $db->lastInsertId(); $stmtItem = $db->prepare("INSERT INTO roadmap_items (roadmap_id, project_number, project_name, descripcion) VALUES (?, ?, ?, ?)"); $iterable = $data['modules'] ?? $data['projects'] ?? []; foreach ($iterable as $module) { $mNum = $module['module_number'] ?? $module['project_number'] ?? 1; $mName = $module['title'] ?? $module['module_title'] ?? $module['project_name'] ?? 'Módulo ' . $mNum; // Nueva estructura: tasks (array de strings) if (!empty($module['tasks'])) { foreach ($module['tasks'] as $task) { $stmtItem->execute([$roadmap_id, $mNum, $mName, $task]); } } // Vieja estructura: execution_items if (!empty($module['execution_items'])) { foreach ($module['execution_items'] as $item) { $stmtItem->execute([$roadmap_id, $mNum, $mName, $item]); } } // Nueva estructura: project (objeto singular) if (!empty($module['project']) && !empty($module['project']['included']) && !empty($module['project']['name'])) { $cpName = $module['project']['name']; $cpDesc = $module['project']['desc'] ?? ''; $stmtItem->execute([$roadmap_id, $mNum, $mName, "PROYECTO: {$cpName} - {$cpDesc}"]); } // Vieja estructura: consolidation_projects (array) if (!empty($module['consolidation_projects'])) { foreach ($module['consolidation_projects'] as $cp) { $cpName = $cp['project_name'] ?? 'Proyecto'; $cpDesc = $cp['description'] ?? 'Aplicar el conocimiento'; $stmtItem->execute([$roadmap_id, $mNum, $mName, "PROYECTO: {$cpName} - {$cpDesc}"]); } } } $db->commit(); echo json_encode([ 'success' => true, 'roadmap_id' => $roadmap_id, 'model_label' => ROADMAP_AI_MODEL_LABEL, 'modules_count' => count($data['modules'] ?? []), 'batch_size' => ROADMAP_BATCH_SIZE ]); } catch (Exception $e) { $db->rollBack(); echo json_encode(['success' => false, 'error' => $e->getMessage()]); } exit; } // ========================================== // Ruta: Obtener todos los Roadmaps (Grid) // ========================================== if ($action === 'obtener_roadmaps') { $stmt = $db->prepare(" SELECT r.*, (SELECT COUNT(*) FROM roadmap_items WHERE roadmap_id = r.id) as total_items, (SELECT COUNT(*) FROM roadmap_items WHERE roadmap_id = r.id AND estado = 'completed') as items_completados FROM roadmaps r WHERE r.usuario_id = ? AND r.duo_id = ? ORDER BY r.id DESC "); $stmt->execute([$uid, $duo]); $roadmaps = $stmt->fetchAll(PDO::FETCH_ASSOC); echo json_encode(['success' => true, 'roadmaps' => $roadmaps]); exit; } // ========================================== // Ruta: Obtener un Roadmap con Items // ========================================== if ($action === 'obtener_roadmap') { $roadmap_id = (int) ($_POST['roadmap_id'] ?? 0); $stmt = $db->prepare("SELECT * FROM roadmaps WHERE id = ? AND usuario_id = ? AND duo_id = ?"); $stmt->execute([$roadmap_id, $uid, $duo]); $roadmap = $stmt->fetch(PDO::FETCH_ASSOC); if (!$roadmap) { echo json_encode(['success' => false, 'error' => 'Roadmap no encontrado.']); exit; } $stmtItems = $db->prepare(" SELECT ri.*, (SELECT rir.id FROM roadmap_item_research rir WHERE rir.item_id = ri.id AND rir.usuario_id = ? LIMIT 1) as has_research, (SELECT COUNT(*) FROM roadmap_instructor i WHERE i.item_id = ri.id) as has_instructor FROM roadmap_items ri WHERE ri.roadmap_id = ? ORDER BY ri.project_number ASC, ri.id ASC "); $stmtItems->execute([$uid, $roadmap_id]); $items = $stmtItems->fetchAll(PDO::FETCH_ASSOC); echo json_encode(['success' => true, 'roadmap' => $roadmap, 'items' => $items]); exit; } // ========================================== // Ruta: Toggle Item // ========================================== if ($action === 'toggle_item') { $item_id = (int) ($_POST['item_id'] ?? 0); $status = $_POST['status'] ?? 'pending'; $stmtCheck = $db->prepare("SELECT i.id FROM roadmap_items i JOIN roadmaps r ON i.roadmap_id = r.id WHERE i.id = ? AND r.usuario_id = ?"); $stmtCheck->execute([$item_id, $uid]); if (!$stmtCheck->fetch()) { echo json_encode(['success' => false, 'error' => 'No autorizado']); exit; } $completedAt = ($status === 'completed') ? date('Y-m-d H:i:s') : null; $stmt = $db->prepare("UPDATE roadmap_items SET estado = ?, completed_at = ? WHERE id = ?"); $stmt->execute([$status, $completedAt, $item_id]); echo json_encode(['success' => true]); exit; } // ========================================== // Ruta: Eliminar Roadmap // ========================================== if ($action === 'eliminar_roadmap') { $roadmap_id = (int) ($_POST['roadmap_id'] ?? 0); $db->beginTransaction(); try { $db->prepare("DELETE FROM roadmap_items WHERE roadmap_id = ?")->execute([$roadmap_id]); $db->prepare("DELETE FROM roadmaps WHERE id = ? AND usuario_id = ? AND duo_id = ?")->execute([$roadmap_id, $uid, $duo]); $db->commit(); echo json_encode(['success' => true]); } catch (Exception $e) { $db->rollBack(); echo json_encode(['success' => false, 'error' => $e->getMessage()]); } exit; } // ========================================== // Ruta: Auditar Item (Auditor Despiadado - GPT-OSS 120B via Groq) // ========================================== if ($action === 'auditar_item') { $item_id = (int) ($_POST['item_id'] ?? 0); $evidencia = trim($_POST['evidencia'] ?? ''); if (!$item_id || empty($evidencia)) { echo json_encode(['success' => false, 'error' => 'Faltan parámetros: item_id y evidencia son requeridos.']); exit; } $stmt = $db->prepare("SELECT i.descripcion, i.estado, i.project_name FROM roadmap_items i JOIN roadmaps r ON i.roadmap_id = r.id WHERE i.id = ? AND r.usuario_id = ?"); $stmt->execute([$item_id, $uid]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { echo json_encode(['success' => false, 'error' => 'Item no encontrado o no autorizado.']); exit; } if ($row['estado'] === 'completed') { echo json_encode(['success' => false, 'error' => 'Este item ya está completado.']); exit; } $task_description = $row['descripcion']; $PROMPT_AUDITOR = "Eres el 'Auditor Despiadado', un Tech Lead hiper-exigente. El usuario debe cumplir esta tarea: '{$task_description}'. A continuación, evaluará su código o evidencia. Revisa vulnerabilidades, malas prácticas o si usó tecnologías obsoletas. Eres estricto pero justo. DEBES responder EXCLUSIVAMENTE con un JSON válido con esta estructura: { \"status\": \"APROBADO\" | \"RECHAZADO\", \"feedback\": \"Comentario directo y brutal de máximo 2 líneas. Si apruebas, felicita brevemente. Si rechazas, di exactamente qué línea o concepto refactorizar.\" } REGLAS VISUALES: Si recibes capturas de pantalla, verifica si demuestran la tarea completada empíricamente. Si es algo irrelevante, rechaza."; $imagenes = []; if (!empty($_POST['imagen1'])) $imagenes[] = $_POST['imagen1']; if (!empty($_POST['imagen2'])) $imagenes[] = $_POST['imagen2']; if (empty($imagenes)) { $PROMPT_USER = "EVIDENCIA DEL USUARIO:\n{$evidencia}"; $modelo_ai = "openai/gpt-oss-120b"; } else { $PROMPT_USER = [ [ "type" => "text", "text" => "EVIDENCIA DEL USUARIO:\n{$evidencia}\n[NOTA AL AUDITOR: Analiza las imágenes adjuntas junto con este texto]." ] ]; foreach ($imagenes as $img) { $PROMPT_USER[] = [ "type" => "image_url", "image_url" => ["url" => $img] ]; } $modelo_ai = "meta-llama/llama-4-scout-17b-16e-instruct"; // Modelo con soporte de vision } $responseText = callGroqRoadmap($PROMPT_AUDITOR, $PROMPT_USER, $modelo_ai, $db); if (strpos($responseText, 'ERROR_API:') === 0) { echo json_encode(['success' => false, 'error' => $responseText]); exit; } $data = getCleanJsonRoadmap($responseText, $db); if (!$data || !isset($data['status'])) { echo json_encode(['success' => false, 'error' => 'Error del Auditor (JSON no válido): ' . json_last_error_msg()]); exit; } if (strtoupper($data['status']) === 'APROBADO') { $updateStmt = $db->prepare("UPDATE roadmap_items SET estado = 'completed', completed_at = NOW() WHERE id = ?"); $updateStmt->execute([$item_id]); echo json_encode(['success' => true, 'status' => 'APROBADO', 'feedback' => $data['feedback']]); } else { echo json_encode(['success' => true, 'status' => 'RECHAZADO', 'feedback' => $data['feedback']]); } exit; } // ========================================== // Ruta: Investigar Item (Gemini 2.5 Flash + Google Search Grounding) // ========================================== if ($action === 'investigar_item') { $task_description = trim($_POST['task_description'] ?? ''); $user_context = trim($_POST['user_context'] ?? ''); $itemId = (int) ($_POST['item_id'] ?? 0); if (empty($task_description)) { echo json_encode(['success' => false, 'error' => 'La descripción de la tarea es requerida.']); exit; } $researchSystem = "Eres un Investigador Técnico de Élite de ProgressGym. Fecha: {$fecha_hoy}. Tu misión es generar un informe práctico, actualizado y verificado sobre la tarea técnica indicada. ESTRUCTURA DEL INFORME: ## Estado del Arte ({$anio_actual}) - Qué ha cambiado, versiones actuales, enfoque moderno. ## Implementación Práctica - Explicación clara con bloques de código funcionales. ## Ejercicios - 2-3 ejercicios prácticos progresivos. ## Errores Comunes - Los 3 errores más frecuentes y cómo evitarlos. FORMATO OBLIGATORIO: - Markdown limpio: ## secciones, ### subsecciones, **negrita**, listas con -. - PROHIBIDO tablas markdown. Usa listas. - Bloques de código con lenguaje especificado (```js, ```python, etc). - Sé directo y denso. Sin relleno."; $researchUser = "Investiga a fondo: {$task_description}"; if (!empty($user_context)) { $researchUser .= "\n\nContexto adicional del usuario: {$user_context}"; } $responseText = callGeminiRoadmap( $researchSystem, $researchUser, ROADMAP_AI_DEFAULT_MODEL, true, $db, getRoadmapAiOptions(8192, 300) ); if (is_string($responseText) && strpos($responseText, 'ERROR_API:') === 0) { echo json_encode(['success' => false, 'error' => $responseText]); exit; } $cleanResult = trim($responseText); if (empty($cleanResult)) { echo json_encode(['success' => false, 'error' => 'Respuesta vacía de la IA.']); exit; } // Guardar en DB if ($itemId > 0) { try { $stmtDel = $db->prepare("DELETE FROM roadmap_item_research WHERE item_id = ? AND usuario_id = ?"); $stmtDel->execute([$itemId, $uid]); $stmtIns = $db->prepare("INSERT INTO roadmap_item_research (item_id, usuario_id, contenido) VALUES (?, ?, ?)"); $stmtIns->execute([$itemId, $uid, $cleanResult]); } catch (Exception $e) { } } echo json_encode([ 'success' => true, 'resultado' => $cleanResult, 'item_id' => $itemId ]); exit; } // ========================================== // Ruta: Obtener research guardado de un item // ========================================== if ($action === 'obtener_item_research') { $itemId = (int) ($_POST['item_id'] ?? 0); $stmt = $db->prepare("SELECT contenido FROM roadmap_item_research WHERE item_id = ? AND usuario_id = ?"); $stmt->execute([$itemId, $uid]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row) { echo json_encode(['success' => true, 'contenido' => $row['contenido']]); } else { echo json_encode(['success' => false, 'error' => 'No hay investigación guardada.']); } exit; } // ========================================== // Ruta: Eliminar research de un item // ========================================== if ($action === 'eliminar_item_research') { $itemId = (int) ($_POST['item_id'] ?? 0); $db->prepare("DELETE FROM roadmap_item_research WHERE item_id = ? AND usuario_id = ?")->execute([$itemId, $uid]); echo json_encode(['success' => true]); exit; } // ========================================== // Ruta: Actualizar Imagen del Roadmap // ========================================== if ($action === 'actualizar_imagen') { $roadmap_id = (int) ($_POST['roadmap_id'] ?? 0); if (!$roadmap_id || empty($_FILES['imagen'])) { echo json_encode(['success' => false, 'error' => 'Faltan parámetros.']); exit; } $stmtCheck = $db->prepare("SELECT id, imagen FROM roadmaps WHERE id = ? AND usuario_id = ?"); $stmtCheck->execute([$roadmap_id, $uid]); $roadmap = $stmtCheck->fetch(PDO::FETCH_ASSOC); if (!$roadmap) { echo json_encode(['success' => false, 'error' => 'Roadmap no encontrado.']); exit; } $uploadDir = __DIR__ . '/../../uploads/roadmap/'; if (!file_exists($uploadDir)) mkdir($uploadDir, 0777, true); $ext = strtolower(pathinfo($_FILES['imagen']['name'], PATHINFO_EXTENSION)); $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp']; if (!in_array($ext, $allowed)) { echo json_encode(['success' => false, 'error' => 'Formato de imagen no permitido.']); exit; } // Delete old image if exists if (!empty($roadmap['imagen']) && file_exists(__DIR__ . '/../../' . $roadmap['imagen'])) { unlink(__DIR__ . '/../../' . $roadmap['imagen']); } $filename = 'roadmap_' . uniqid() . '.' . $ext; $filepath = $uploadDir . $filename; $dbPath = 'uploads/roadmap/' . $filename; if (!move_uploaded_file($_FILES['imagen']['tmp_name'], $filepath)) { echo json_encode(['success' => false, 'error' => 'Error al subir la imagen.']); exit; } $db->prepare("UPDATE roadmaps SET imagen = ? WHERE id = ?")->execute([$dbPath, $roadmap_id]); echo json_encode(['success' => true, 'imagen' => $dbPath]); exit; } // ========================================== // Ruta: Eliminar Imagen del Roadmap // ========================================== if ($action === 'eliminar_imagen') { $roadmap_id = (int) ($_POST['roadmap_id'] ?? 0); $stmtCheck = $db->prepare("SELECT id, imagen FROM roadmaps WHERE id = ? AND usuario_id = ?"); $stmtCheck->execute([$roadmap_id, $uid]); $roadmap = $stmtCheck->fetch(PDO::FETCH_ASSOC); if (!$roadmap) { echo json_encode(['success' => false, 'error' => 'Roadmap no encontrado.']); exit; } if (!empty($roadmap['imagen']) && file_exists(__DIR__ . '/../../' . $roadmap['imagen'])) { unlink(__DIR__ . '/../../' . $roadmap['imagen']); } $db->prepare("UPDATE roadmaps SET imagen = NULL WHERE id = ?")->execute([$roadmap_id]); echo json_encode(['success' => true]); exit; } // ========================================== // Ruta: Iniciar Deep Research para Optimizar Contexto // ========================================== if ($action === 'iniciar_deep_research_contexto') { $objetivo = trim($_POST['objetivo'] ?? ''); $contexto = trim($_POST['contexto'] ?? ''); if (empty($objetivo) && empty($contexto)) { echo json_encode(['success' => false, 'error' => 'Necesitas escribir al menos el objetivo o el contexto para investigar.']); exit; } $apiKey = getenv('GOOGLE_NEW_KEY') ?: (defined('GOOGLE_NEW_KEY') ? GOOGLE_NEW_KEY : ''); if (empty($apiKey)) { echo json_encode(['success' => false, 'error' => 'No hay API key configurada.']); exit; } $textoInvestigar = !empty($objetivo) ? $objetivo : ''; if (!empty($contexto)) { $textoInvestigar .= "\n\nCONTEXTO ADICIONAL DEL USUARIO:\n" . $contexto; } $researchPrompt = "Eres un Investigador Autónomo de Élite operando el {$fecha_hoy}. Tu misión es investigar a fondo el siguiente objetivo de aprendizaje para crear un roadmap técnico. REGLAS CRÍTICAS ANTI-ALUCINACIÓN: 1. ESTRICTA REALIDAD: Tienes PROHIBIDO inventar tecnologías, versiones o modelos que aún no existan. Básate ÚNICAMENTE en datos reales y comprobables hasta {$anio_actual}. 2. VANGUARDIA REAL: Descarta herramientas, versiones o enfoques obsoletos. Busca el verdadero 'State of the Art' actual en {$anio_actual}. 3. LEY DE PARETO (80/20): Identifica el 20% exacto de las habilidades que producen el 80% de los resultados en la industria de hoy. Genera un informe detallado con: - El estado actual de cada disciplina mencionada en {$anio_actual}. - El stack tecnológico exacto y production-ready para cada tema. - La ruta priorizada de aprendizaje condensada. - Qué herramientas/frameworks son reales y cuáles son hype vacío. OBJETIVO A INVESTIGAR:\n{$textoInvestigar}"; $url = "https://generativelanguage.googleapis.com/v1beta/interactions"; $payload = [ "input" => $researchPrompt, "agent" => "deep-research-preview-04-2026", "background" => true ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'x-goog-api-key: ' . $apiKey ], CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE), CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 60 ]); $db = null; $response = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $db = getDB(true); if ($error) { echo json_encode(['success' => false, 'error' => "CURL Error: {$error}"]); exit; } $resArr = json_decode($response, true); if ($httpCode >= 400) { $errMsg = $resArr['error']['message'] ?? "HTTP {$httpCode}"; echo json_encode(['success' => false, 'error' => "API Error: {$errMsg}"]); exit; } $interactionId = $resArr['id'] ?? $resArr['name'] ?? null; if (!$interactionId) { echo json_encode(['success' => false, 'error' => 'No se recibió interaction ID de Deep Research.']); exit; } echo json_encode(['success' => true, 'interaction_id' => $interactionId]); exit; } // ========================================== // Ruta: Poll Deep Research Contexto // ========================================== if ($action === 'poll_deep_research_contexto') { $interactionId = trim($_POST['interaction_id'] ?? ''); if (empty($interactionId)) { echo json_encode(['success' => false, 'error' => 'interaction_id es requerido.']); exit; } $apiKey = getenv('GOOGLE_NEW_KEY') ?: (defined('GOOGLE_NEW_KEY') ? GOOGLE_NEW_KEY : ''); $url = "https://generativelanguage.googleapis.com/v1beta/interactions/{$interactionId}"; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPGET => true, CURLOPT_HTTPHEADER => ['x-goog-api-key: ' . $apiKey], CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 30 ]); $db = null; $response = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $db = getDB(true); if ($error) { echo json_encode(['success' => false, 'error' => "CURL Error: {$error}"]); exit; } $resArr = json_decode($response, true); if ($httpCode >= 400) { $errMsg = $resArr['error']['message'] ?? "HTTP {$httpCode}"; echo json_encode(['success' => false, 'error' => "API Error: {$errMsg}"]); exit; } $status = $resArr['status'] ?? 'unknown'; if ($status === 'completed') { $outputs = $resArr['outputs'] ?? []; $researchText = !empty($outputs) ? end($outputs)['text'] ?? '' : ''; if (empty($researchText)) { echo json_encode(['success' => false, 'error' => 'Deep Research completado sin resultados.', 'status' => 'failed']); exit; } // Condensar con Gemini + Grounding $condenseSystem = "Eres el Filtro de Verdad de ProgressGym. Recibes un informe de investigación profunda. Tu misión es extraer la estrategia táctica ganadora en un formato EXTREMADAMENTE DENSO Y CORTO.\n\n" . "REGLAS DE CONDENSACIÓN OBLIGATORIAS (AÑO {$anio_actual}):\n" . "1. RESTRICCIÓN DE LONGITUD: Tu respuesta total DEBE tener un MÁXIMO ABSOLUTO de 15 viñetas cortas.\n" . "2. ESTILO TELEGRÁFICO: Usa frases directas, sin relleno. (Ej: 'Evitar React 18, usar React 19 con RSC').\n" . "3. DETECCIÓN DE BASURA: Si el informe menciona tecnologías irreales o que aún no existen en {$anio_actual}, elimínalas en silencio.\n" . "4. CONTENIDO EXIGIDO: Incluye solo: 1) Stack tecnológico exacto y maduro de {$anio_actual}, 2) Conceptos clave 80/20, 3) Advertencias de obsolescencia.\n" . "5. Tono dictatorial, directo y en español."; $condensedText = callGeminiRoadmap( $condenseSystem, "INFORME DE INVESTIGACIÓN PROFUNDA A CONDENSAR:\n\n" . mb_substr($researchText, 0, 30000), ROADMAP_AI_DEFAULT_MODEL, true, $db, getRoadmapAiOptions(8192, 300) ); if (strpos($condensedText, 'ERROR_API:') === 0 || strpos($condensedText, 'Error CURL:') === 0) { // Fallback: usar el texto raw truncado $condensedText = mb_substr(strip_tags($researchText), 0, 2000); } $condensedText = trim(strip_tags($condensedText)); if (mb_strlen($condensedText) > 2500) { $condensedText = mb_substr($condensedText, 0, 2497) . '...'; } // Guardar en DB $objetivoPost = trim($_POST['objetivo'] ?? ''); $contextoOrigPost = trim($_POST['contexto_original'] ?? ''); try { $stmtSave = $db->prepare("INSERT INTO roadmap_research (usuario_id, duo_id, objetivo, contexto_original, contexto_optimizado, informe_completo) VALUES (?, ?, ?, ?, ?, ?)"); $stmtSave->execute([$uid, $duo, $objetivoPost, $contextoOrigPost, $condensedText, mb_substr($researchText, 0, 50000)]); $researchId = $db->lastInsertId(); } catch (Exception $e) { $researchId = null; } echo json_encode([ 'success' => true, 'status' => 'completed', 'contexto_optimizado' => $condensedText, 'informe_completo' => mb_substr($researchText, 0, 50000), 'research_id' => $researchId ]); } elseif ($status === 'failed') { echo json_encode(['success' => false, 'status' => 'failed', 'error' => 'Deep Research falló.']); } else { echo json_encode(['success' => true, 'status' => 'in_progress']); } exit; } // ========================================== // Ruta: Obtener investigaciones guardadas // ========================================== if ($action === 'obtener_investigaciones') { $stmt = $db->prepare("SELECT id, objetivo, contexto_optimizado, created_at FROM roadmap_research WHERE usuario_id = ? AND duo_id = ? ORDER BY id DESC LIMIT 20"); $stmt->execute([$uid, $duo]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); echo json_encode(['success' => true, 'investigaciones' => $rows]); exit; } // ========================================== // Ruta: Obtener una investigación completa // ========================================== if ($action === 'obtener_investigacion') { $rid = (int) ($_POST['research_id'] ?? 0); $stmt = $db->prepare("SELECT * FROM roadmap_research WHERE id = ? AND usuario_id = ?"); $stmt->execute([$rid, $uid]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { echo json_encode(['success' => false, 'error' => 'No encontrada.']); exit; } echo json_encode(['success' => true, 'investigacion' => $row]); exit; } // ========================================== // Ruta: Eliminar investigación // ========================================== if ($action === 'eliminar_investigacion') { $rid = (int) ($_POST['research_id'] ?? 0); $stmt = $db->prepare("DELETE FROM roadmap_research WHERE id = ? AND usuario_id = ?"); $stmt->execute([$rid, $uid]); echo json_encode(['success' => true]); exit; } // ========================================== // Ruta: Instructor Operativo // ========================================== if ($action === 'instructor_ask') { $item_id = intval($_POST['item_id'] ?? 0); $roadmap_title = trim($_POST['roadmap_title'] ?? ''); $project_name = trim($_POST['project_name'] ?? ''); $objetivo = trim($_POST['objetivo'] ?? ''); $user_msg = trim($_POST['user_message'] ?? ''); if (!$item_id || empty($objetivo)) { echo json_encode(['success' => false, 'error' => 'Datos incompletos.']); exit; } // Verificar que el item pertenece al usuario $stmtCheck = $db->prepare("SELECT ri.id, ri.roadmap_id, ri.project_number, ri.descripcion FROM roadmap_items ri JOIN roadmaps r ON ri.roadmap_id = r.id WHERE ri.id = ? AND r.usuario_id = ? AND r.duo_id = ?"); $stmtCheck->execute([$item_id, $uid, $duo]); $currentItem = $stmtCheck->fetch(PDO::FETCH_ASSOC); if (!$currentItem) { echo json_encode(['success' => false, 'error' => 'No autorizado']); exit; } // Obtener el siguiente item en el roadmap (por orden de id) $stmtNext = $db->prepare("SELECT descripcion, project_name FROM roadmap_items WHERE roadmap_id = ? AND id > ? ORDER BY id ASC LIMIT 1"); $stmtNext->execute([$currentItem['roadmap_id'], $item_id]); $nextItem = $stmtNext->fetch(PDO::FETCH_ASSOC); $nextObjective = $nextItem ? $nextItem['descripcion'] : ''; $systemPrompt = "Eres el Instructor Operativo de ProgressGym ({$fecha_hoy}). Conviertes objetivos de roadmap en ejecución práctica. CAPACIDADES: Tienes Google Search Grounding activo. Verifica versiones, APIs y prácticas de {$anio_actual} con datos reales de la web. Si piden ejercicios, busca ejemplos actualizados. REGLAS DE COMPORTAMIENTO: - No regeneres el plan. - Sé directo: pasos concretos, código si aplica, error común, micro ejercicio y puente al siguiente objetivo. - Usa tono motivador y estricto, pero profesional y sin exageraciones. - No inventes reglas, estándares internos, slogans, metodologías, políticas, filosofías, nombres propios o prohibiciones atribuidas a ProgressGym, salvo que estén explícitamente indicadas en el contexto recibido. - Distingue entre buena práctica, recomendación y regla estricta. No presentes recomendaciones técnicas como si fueran políticas obligatorias. - Si una recomendación depende del contexto, acláralo explícitamente. - Mantente estrictamente dentro del objetivo actual. No introduzcas patrones, atajos o alternativas más avanzadas salvo que sean indispensables para entender el tema. - Si el objetivo es fundamental, enseña primero la versión más básica y directa antes de mostrar refactors o variantes modernas. - No reemplaces el tema central por alternativas “más elegantes”. Si el objetivo incluye switch, enséñalo; si incluye for y while, practícalos de forma directa. - No regeneres la secuencia de aprendizaje ni sugieras rutas alternativas; el orden ya viene definido por el roadmap. - Trabaja únicamente sobre el objetivo actual recibido. - En temas fundamentales, prioriza explicación operativa antes que optimización, refactor o estilo profesional. - Enseña primero la forma más simple y correcta del concepto. - En objetivos fundamentales, no conviertas patrones de refactor, optimización o estilo avanzado en el eje principal de la explicación. Si aparecen, deben ir solo como nota breve de mejora posterior. El núcleo debe enseñar primero la forma más simple, directa y operativa del objetivo actual. Además, el micro ejercicio debe practicar explícitamente todos los elementos centrales nombrados en el objetivo, aunque sea en su versión más mínima. - Si un concepto listado en el objetivo no aparece en el micro ejercicio, la respuesta está incompleta. - Las buenas prácticas avanzadas solo pueden aparecer al final como mejora posterior breve. - Cierra siempre con un micro ejercicio práctico, corto y directamente relacionado con el objetivo actual. - El micro ejercicio debe poder resolverse en pocos minutos y no debe adelantar contenidos posteriores. - No uses secciones como “Siguiente acción”, “Siguiente nivel”, “Temas relacionados” o recomendaciones libres de estudio. - Usa una sección final obligatoria llamada “Puente al siguiente objetivo”. - Si existe un siguiente objetivo oficial en el contexto, debes incluir siempre la sección “Puente al siguiente objetivo”. - En “Puente al siguiente objetivo”, conecta en 1 o 2 frases lo aprendido hoy con el siguiente objetivo oficial del roadmap, sin enseñarlo todavía. - Si no existe un siguiente objetivo oficial en el contexto, no inventes ninguno y omite esa conexión. - La sección “Puente al siguiente objetivo” nunca debe introducir teoría nueva; solo debe enlazar el objetivo actual con el siguiente. FORMATO OBLIGATORIO: - Usa Markdown limpio: ## para secciones, ### para subsecciones, **negrita**, listas con -, bloques de código con triple backtick y lenguaje (```js, ```python, etc). - PROHIBIDO usar tablas markdown (| col |). En su lugar usa listas con viñetas. - Los bloques de código SIEMPRE deben tener el lenguaje especificado después de los backticks."; $userPrompt = "Roadmap: {$roadmap_title}\nFase/Proyecto: {$project_name}\nObjetivo actual: {$objetivo}\n"; if (!empty($nextObjective)) { $userPrompt .= "Siguiente objetivo en el roadmap: {$nextObjective}\n"; } else { $userPrompt .= "Siguiente objetivo en el roadmap: (Este es el último objetivo del roadmap)\n"; } if (!empty($user_msg)) { $userPrompt .= "\nEl usuario pregunta/dice: \"{$user_msg}\"\nResponde a su mensaje enfocado en el objetivo."; } $result = callGroqInstructor($systemPrompt, $userPrompt, $db); if (isset($result['error'])) { echo json_encode(['success' => false, 'error' => $result['error']]); exit; } $respuesta = $result['content']; // Guardar en la DB try { $stmtSave = $db->prepare("INSERT INTO roadmap_instructor (item_id, roadmap_title, project_name, objetivo, respuesta) VALUES (?, ?, ?, ?, ?)"); $stmtSave->execute([$item_id, $roadmap_title, $project_name, $objetivo, $respuesta]); $instructor_id = $db->lastInsertId(); echo json_encode([ 'success' => true, 'response' => $respuesta, 'instructor_id' => $instructor_id ]); } catch (Exception $e) { echo json_encode(['success' => false, 'error' => 'Error al guardar la respuesta en la base de datos.']); } exit; } if ($action === 'instructor_load') { $item_id = intval($_POST['item_id'] ?? 0); if (!$item_id) { echo json_encode(['success' => false, 'error' => 'Falta item_id']); exit; } // Verificar propiedad $stmtCheck = $db->prepare("SELECT ri.id FROM roadmap_items ri JOIN roadmaps r ON ri.roadmap_id = r.id WHERE ri.id = ? AND r.usuario_id = ? AND r.duo_id = ?"); $stmtCheck->execute([$item_id, $uid, $duo]); if (!$stmtCheck->fetch()) { echo json_encode(['success' => false, 'error' => 'No autorizado']); exit; } $stmt = $db->prepare(" SELECT i.* FROM roadmap_instructor i WHERE i.item_id = ? ORDER BY i.created_at DESC LIMIT 1 "); $stmt->execute([$item_id]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row) { echo json_encode([ 'success' => true, 'instructor_id' => $row['id'], 'response' => $row['respuesta'] ]); } else { echo json_encode(['success' => true, 'instructor_id' => null]); } exit; } if ($action === 'instructor_delete') { $instructor_id = intval($_POST['instructor_id'] ?? 0); if (!$instructor_id) { echo json_encode(['success' => false, 'error' => 'Falta instructor_id']); exit; } // Join with roadmap_items to check ownership $stmt = $db->prepare(" DELETE i FROM roadmap_instructor i JOIN roadmap_items ri ON i.item_id = ri.id JOIN roadmaps r ON ri.roadmap_id = r.id WHERE i.id = ? AND r.usuario_id = ? AND r.duo_id = ? "); $stmt->execute([$instructor_id, $uid, $duo]); echo json_encode(['success' => true]); exit; } echo json_encode(['success' => false, 'error' => 'Acción no válida.']);
Coded With 💗 by
0x6ick