Historial de cambios
GitHubCada cambio relevante, versión a versión. El mismo archivo se publica en el repositorio y está traducido a los 17 idiomas.
Registro de cambios
Todos los cambios destacables de career-ops-ui. Formato según Keep a Changelog, versionado SemVer.
Traducciones: 🇬🇧 English · 🇧🇷 Português · 🇰🇷 한국어 · 🇯🇵 日本語 · 🇷🇺 Русский · 🇨🇳 简体中文 · 🇹🇼 繁體中文 · 🇫🇷 Français · 🇵🇱 Polski · 🇺🇦 Українська · 🇩🇰 Dansk · 🇸🇦 العربية · 🇩🇪 Deutsch · 🇮🇹 Italiano · 🇹🇷 Türkçe · 🇮🇳 हिन्दी
Nota i18n — desde v1.12.0 en adelante las entradas están localizadas en cada idioma. Las entradas anteriores (v1.11.x, v1.10.x) permanecen en ruso por convención del proyecto; el contenido normativo inglés está en 🇬🇧 CHANGELOG.md.
Nota de traducción (v1.22.0) — este archivo está ahora íntegramente traducido al español técnico. Se han retirado los avisos provisionales “ver CHANGELOG.md en inglés” que aparecían en las entradas v1.13.0 a v1.21.0.
[1.236.0] — 2026-09-18
Paridad con el padre — career-ops main @ 6a9c84c (VERSION 1.33.0, 128 commits). Dos fuentes nuevas, y tres correcciones reflejadas — dos de las cuales perdían ofertas aquí, en silencio.
Añadido
Dos fuentes nuevas para el escáner, 92 → 94 (89 EN + 5 RU). Python.org Jobs (pythonorg) lee el feed público del tablón; la atribución de empleador es obligatoria, así que una fila cuyo título no nombra a ningún empleador se descarta en vez de rellenarse con el nombre de la entrada del portal. Generalist World (generalist-world) extrae las tarjetas del tablón y falla de forma ruidosa ante marcado que no puede analizar — una tarjeta con una forma cambiada lanza una excepción en vez de leerse como un tablón vacío, que es el modo de fallo que vuelve poco fiable a una fuente por scraping. Ambas aparecen en el filtro de fuente de #/scan, ambas están ancladas a su host (regex anclada, solo HTTPS), y ambas están cubiertas de principio a fin por suites de transporte simulado (43 y 36 casos).
Corregido
Una oferta de Workday abierta en varias ciudades quedaba descartada por un filtro de ubicación. El endpoint de listado de Workday responde a una oferta multiubicación con un recuento donde toda otra oferta lleva un lugar: "53 Locations". Eso no es una ubicación, y buildLocationFilter compara por subcadena sin distinguir mayúsculas y minúsculas — así que una oferta abierta en Austin y otras 52 ciudades no coincidía con ninguna entrada allow: [austin] y se descartaba, mientras que la misma oferta listada en solitario sí pasaba. Medido en origen a través de tres tenants: 53 de 291 ofertas eran marcadores de posición, así que este es el caso habitual, no uno marginal. Los lugares reales de la oferta existen solo en el documento de detalle de CXS, así que cada marcador de posición ahora cuesta un GET adicional contra él, con un tope de 200 por entrada — superado el tope, el marcador de posición se deja visible en vez de falsearse, porque una oferta sin resolver nunca debe leerse como resuelta. Un detalle fallido o sin lugar deja la oferta exactamente como la devolvió el listado: un "53 Locations" visible es honesto sobre lo que no pudo resolverse, mientras que una ubicación vaciada pasaría todos los filtros en silencio. El mismo GET también lleva startDate, así que una oferta enriquecida queda fechada con exactitud en vez de por la prosa relativa del endpoint de listado ("Posted 30+ Days Ago" → undefined); todo lo que no sea un YYYY-MM-DD desnudo se deja tal cual en vez de pasarse por Date.parse, cuyo comportamiento de reserva no-ISO depende de la implementación.
Un nivel indicado ya no colapsa dos requisiciones en una sola republicación. "Insurance Specialist II" es la propia declaración del empleador de que esta requisición no es hermana de la otra — una banda salarial, un alcance y un número de requisición distintos bajo un mismo título base. El tokenizador no podía verlo: w.length > 3 descarta todo numeral romano hasta VIII y todo dígito suelto, así que "Insurance Specialist" e "Insurance Specialist II" se tokenizaban de forma idéntica, obtenían una ratio de Jaccard perfecta, y el detector de republicaciones marcaba la segunda como una relistada de la primera. role-matcher.mjs de web-ui no tenía manejo de nivel en absoluto, así que esto trae aterrizado el mecanismo completo del padre: los niveles se extraen plegando romano y arábigo sobre un mismo número ("Nurse II" y "Nurse 2" son la misma declaración — y ese par difería incorrectamente antes), dos niveles indicados deben coincidir, y un nivel presente en un solo lado se trata como una reescritura suelta salvo que cada título lleve también una palabra que falta en el otro, lo cual son dos discrepancias y no un adorno.
Un tablón de Avature fijado a un filtro recorría igualmente el tablón global completo. La interfaz de búsqueda de Avature genera parámetros de faceta opacos (?42386=[812132] es un filtro de país), y un operador que fija un tablón acotado en portals.yml espera que el escaneo recorra ese. resolveSearch reconstruía la URL como ${origin}${path} y descartaba la cadena de consulta sin más — así que cada petición, incluida la primera, recorría el tablón completo del tenant: más páginas, más peticiones, y ofertas que el operador había filtrado deliberadamente. Las facetas ahora acompañan cada petición paginada, con nuestro propio jobOffset aplicado al final para que una faceta homónima extraviada nunca pueda secuestrar la paginación.
Seguridad
devalue 5.8.1 → 5.9.2 (de forma transitiva vía astro), que cierra la única alerta abierta de Dependabot — una denegación de servicio ante una entrada malformada. Dependencia exclusiva del sitio; no cambió ningún código en tiempo de ejecución.
Notas
No se porta, cada uno con su motivo. El padre dio a lever un timeout de 30s y a manfred uno de 25s frente a su valor por defecto compartido de 10s — un no-port: el transporte de escaneo de web-ui ya permite 60s (DEFAULT_SCAN_TIMEOUT_MS), así que ambos tablones tienen aquí más margen del que concede la corrección allí. El prefijo careers- duplicado de iCIMS tampoco puede darse aquí: web-ui toma el origen del tenant directamente de api/careers_url y nunca construye un host a partir de un nombre de tenant; su nuevo recorrido de host de reserva es una funcionalidad para la configuración más laxa del padre, no una corrección de defecto. deutschebahn y manfred ganaron reintento ante abort transitorio, algo que el techo de 60s de web-ui vuelve mucho menos apremiante — anotado en vez de portado, así que un futuro timeout de recorrido largo ya tiene una primera cosa conocida que probar. Todo lo demás en el delta de 128 commits es CLI, verificador, dashboard o web/ (el frontend propio del padre): liveness-browser, check-jd-archive, verify-ats/verify-portals/verify-cv-facts, update-system, set-status, audit-portals, discover-ats, scaffolder, test-all, manifiestos de plugins.
Divergencias del fork reverificadas tras el pull: la corrección cirílica de \p{L} en providers/telegram-channel.mjs, el propio providers/telegram.mjs del fork, hermes en web/src/lib/clis.ts. El upstream career-ops-hq/main está completamente fusionado (0 commits faltantes). 3066 → 3164 pruebas; la ayuda ahora indica 94 adaptadores (89 EN + 5 RU) en los 17 paquetes, y la estructura de encabezados no cambia, en 32 H2 / 122 H3.
[1.235.0] — 2026-09-16
Paridad con el padre — career-ops main @ 68e6b94 (VERSION 1.33.0, 42 commits). Dos correcciones reflejadas; una de ellas perdía ofertas de MokaHR aquí, en silencio.
Corregido
Una segunda oferta del mismo tablero de MokaHR desaparecía en vez de añadirse. url-key.mjs reduce la URL de una oferta a una clave de comparación, y descartaba el fragmento sin condiciones — razonable, ya que casi todos los fragmentos son solo de presentación (#apply, #section-2). MokaHR es la excepción en nuestro propio registro: sus ofertas son …/social-recruitment/{tenant}/{id}#/job/{n}, donde todas las ofertas de un tenant comparten la ruta y el id de la oferta existe solo en el fragmento. Así que todas se normalizaban a una sola clave. Medido antes de la corrección: pega dos ofertas distintas de MokaHR en la pipeline y terminas con una línea — sin error, sin aviso, nada que notar; el filtro de novedad del escáner (en-scanner.mjs) lee la misma clave, así que en cuanto cualquier oferta de ese tenant estaba en el historial de escaneo, todas las demás se descartaban como “ya vista”. normalizeUrl ahora promueve una ruta reconocida #/job/{id} o #/jobs/{id} a una clave de consulta solo para comparación antes de descartar el fragmento — MokaHR conserva su nombre ya establecido mokahr_job_id para que las claves ya escritas sigan siendo comparables, cualquier otro host recibe un _career_ops_fragment_job_id interno. El id promovido se añade, no se fija: una URL que ya lleve ese parámetro contiene el id de otra oferta distinta, y sobrescribirlo fusionaría justo las dos cosas que la clave existe para mantener separadas. Todo fragmento no reconocido se sigue colapsando, que es la mitad que ya estaba bien.
accept-encoding ahora queda fijado en cada petición a una fuente. Sin fijar, Node negocia lo que sea que ofrezca su build de undici; desde Node ≥ 23 eso incluye zstd, y la respuesta zstd de amazon.jobs vuelve truncada exactamente en 1024 bytes con estado 200 — así que el fallo se lee como Unterminated string in JSON at position 1024, como si fuera una API malformada y no un bug de transporte. Cada petición ahora envía gzip, deflate, br. En nuestros runtimes esto es preventivo, no una reparación: Node 18/20/22 — la matriz de CI y el servidor (22.22.1) — envían gzip, deflate, verificado directamente en vez de asumido, así que aquí nada se truncaba; lo que cambia hoy es que se ofrece br, algo que esas versiones no piden por defecto pero decodifican correctamente. El valor por defecto se aplica sin distinguir mayúsculas y minúsculas, que es la parte que vale la pena revisar: los nombres de cabecera no distinguen mayúsculas de minúsculas y fetch une dos entradas con el mismo nombre en un solo valor separado por comas, así que un llamador que pasara Accept-Encoding: identity de otro modo habría terminado enviando gzip, deflate, br, identity.
Notas
Las cabeceras se mantienen como un objeto plano en el límite de fetchImpl, donde el padre le pasa a fetch una instancia de Headers. Es una divergencia deliberada, no un descuido: los helpers de fetch de web-ui reciben un fetchImpl inyectable, y 45 suites de fuentes leen opts.headers['User-Agent'] de lo que capturan — una instancia de Headers no tiene acceso por corchetes, así que el port fiel es del comportamiento (la sobrescritura sin distinguir mayúsculas y minúsculas, gana el llamador) y no de la implementación. El objeto del llamador se copia, nunca se muta, ya que varias fuentes comparten una misma constante de cabecera. La corrección hermana del padre — aplicar un user-agent por defecto sin distinguir mayúsculas y minúsculas — es un no-port: web-ui no inyecta ningún UA por defecto, cada fuente fija el que exige su tablero, así que no hay ningún valor por defecto con el que pueda colisionar el User-Agent de un llamador.
Dos suites nuevas y tres casos nuevos fijan el par: tests/http-accept-encoding.test.mjs (el valor exacto fijado, cada uno de los cuatro helpers de fetch, y una sobrescritura del llamador bajo tres capitalizaciones — comprobando que sobrevive una única clave accept-encoding, que es de lo que el bug de fusión producía dos) y tests/parsers-pipeline-hash-route.test.mjs, que trabaja sobre la costura visible para el usuario en vez de sobre la clave: dos ofertas de MokaHR deben llegar ambas a la pipeline, pegar de nuevo la misma bajo cuatro grafías distintas debe seguir colapsando a una línea, y un fragmento cosmético no debe convertirse en una identidad. Todos se confirmaron fallando primero. El anclaje vive en el transporte, no solo en esos helpers: 25 de las 92 fuentes llaman directamente al fetchImpl inyectado y nunca pasan por ellos, así que lo aplica makeTimeoutFetch — el envoltorio que ambos escáneres inyectan realmente — y ese es el único punto de paso común a todas las fuentes. (El padre no necesita equivalente: providers/_http.mjs es su único transporte.) Una guarda estructural ahora hace fallar CI si una fuente nueva llama a fetch directamente en vez de usar el transporte que recibió, y un caso de comportamiento conduce a greenhouse, una de las 25, a través del envoltorio para probar que la cabecera llega al cable.
No se porta, con su motivo: liveness-browser.mjs (guarda de reutilización de Playwright con interfaz gráfica — web-ui no conduce un navegador con interfaz gráfica), check-jd-archive.mjs (sin retransmisión en web-ui), test-all.mjs (arnés del padre; la divergencia vpFixtureEnv del fork sigue intacta), web/ (el frontend propio del padre, no reflejado), los manifiestos de plugins y SIGNATURES.md (solo del repositorio). batch/batch-runner.sh (ampersands escapados antes de la interpolación de sed) y jd-skill-gap.mjs (resuelve cv.md contra la raíz de datos) quedan retransmitidos de solo lectura — web-ui invoca por shell las copias del padre, así que ambos llegan sin ningún cambio de código aquí, y el de jd-skill-gap hace más correcta la retransmisión de /api/jds/:id/skill-gap: cv.md ahora se resuelve desde la raíz de datos en vez de desde el directorio en el que se haya generado el proceso. Divergencias del fork reverificadas tras el pull: la corrección cirílica de \p{L} en providers/telegram-channel.mjs, el propio providers/telegram.mjs del fork, hermes en web/src/lib/clis.ts. Fuentes sin cambios, en 92 (87 EN + 5 RU), así que la ayuda queda intacta. 3042 → 3066 pruebas.
[1.234.0] — 2026-09-15
Paridad con el padre — career-ops main @ 56cce8f (VERSION sigue en 1.32.0, 58 commits por delante de 1.32.0). Una corrección reflejada en 15 fuentes, más una guarda para que no pueda volver.
Corregido
Un único id de oferta malformado ya no vacía toda la página. encodeURIComponent lanza URIError ante un sustituto UTF-16 (surrogate) suelto, y una carga JSON puede llevar uno — JSON.parse('"\\uD800"') lo conserva. Quince fuentes construían la URL de cada oferta a partir de un id o slug controlado por el host dentro del bucle de parseo, así que una oferta defectuosa abortaba el bucle y perdía todas las ofertas de esa página; nada quedaba registrado como fallo de fuente porque la excepción se propagaba antes de que el catch por empresa tuviera algo que informar. El padre lo corrigió en providers/_safe-url.mjs (safeEncodeURIComponent → null ante URIError, relanza cualquier otra cosa) y encauzó por ahí el id de cada bucle, descartando solo esa oferta cuando da null. Ese helper es ahora server/lib/sources/_safe-url.mjs, y el mismo cambio se aplica a las trece fuentes reflejadas — alibaba, arbeitsagentur, bamboohr, feishu-jobs, garena, jibeapply, manfred, meituan, mokahr, phenom, thehub, tkms, vdab — y a dos fuentes exclusivas de web-ui que la nueva guarda sacó a la luz en su primera ejecución, jobstreet y trudvsem, que tenían la misma forma de bucle y el mismo lanzamiento latente. El alcance es deliberado: solo los ids que vienen de la API pasan por el helper. Los valores derivados de la configuración conservan su codificación estricta y fallan de forma ruidosa — el office de garena (un segmento de portals.yml) se queda con el urlSegment que lanza, mientras que su id de API recibe un idUrlSegment que devuelve null; el corpName de csod y el slug de 4dayweek validado por SLUG_RE quedan en la lista blanca por nombre, con su motivo.
El fallback de título de rheinmetall podía lanzar ante un href malformado. Cuando una tarjeta no tiene titular, el título se reconstruye a partir del slug de la URL vía decodeURIComponent, que lanza ante una secuencia de porcentaje inválida en el href extraído — la misma forma de «una fila mala mata la página», pero del lado del decode. Ahora decodifica de forma defensiva y recurre al slug en crudo.
El registro de fuentes omite los archivos con prefijo _. El autodescubrimiento importaba cada .mjs de server/lib/sources/ salvo a sí mismo y avisaba de cualquiera que careciera de export const meta — así que el helper generaba un aviso al arrancar. Se adopta la convención del padre: un guion bajo inicial significa no es una fuente; esos archivos no se importan ni generan aviso. Un archivo no declarado y sin meta sigue avisando, y la prueba de descubrimiento ahora comprueba ambas mitades (confirmada en rojo contra el filtro anterior). El script de build del sitio sync-assets.mjs cuenta los archivos de adaptadores en disco con la misma regla, así que el helper ya no dispara su guarda contra caídas silenciosas.
Notas
tests/sources-url-encoding-surrogate.test.mjs refleja la suite del padre en tres partes: el contrato del helper (un sustituto emparejado — un emoji normal — sigue codificándose; un lanzamiento del propio toString del valor es un bug del llamador y se propaga), el comportamiento por fuente para las quince (un id malo + un id limpio → sin lanzamiento, el limpio se conserva, el malo se descarta, cada caso llevado a través del parser puro exportado de la fuente o, para thehub, de su fetcher con un transporte simulado), y una guarda a nivel de fuente: todo archivo convertido debe importar el helper y llamarlo, ningún archivo puede referenciarlo sin importarlo, y ningún archivo no revisado puede construir una URL de oferta con un encodeURIComponent a secas — una fuente nueva con la forma antigua falla en CI con el número de línea del culpable. Los quince casos por fuente se confirmaron fallando antes del port.
Todo lo demás en el delta del padre no se porta, con su motivo: batch/batch-runner.sh --cli es una opción exclusiva de CLI (web-ui no invoca el batch runner por shell); invite-match.mjs ahora resuelve set-status.mjs desde la raíz del código — web-ui no lo retransmite; company-funded.mjs recibió el mismo cambio de safe-url internamente y la forma de su salida --json, que parsea /api/company-funded, no cambia; test-all.mjs solo movió un presupuesto de timeout (la divergencia vpFixtureEnv del fork sigue intacta); web/ es el frontend propio del padre y no se refleja; AGENTS.md/SIGNATURES.md son solo del repositorio. Divergencias del fork reverificadas tras el pull: la corrección cirílica de \p{L} en providers/telegram-channel.mjs, el propio providers/telegram.mjs del fork, hermes en web/src/lib/clis.ts. El número de fuentes no cambia, en 92 (87 EN + 5 RU), así que la ayuda queda intacta. 3022 → 3042 pruebas.
[1.233.2] — 2026-09-10
Corregido — dos alertas de CodeQL js/remote-property-injection de severidad alta.
Corregido
El bucle que copia los valores guardados a process.env recorría Object.entries(safe) — un objeto construido a partir del cuerpo de la petición — con una guarda explícita contra claves de prototipo dentro. El código era seguro, porque safe solo se rellena desde KNOWN_KEYS veinte líneas antes, pero leído por sí solo el bucle dice asigna a una propiedad cuyo nombre salió de un objeto construido con el cuerpo de la petición, y CodeQL no puede seguir esa restricción entre dos bucles. Ahora recorre el array constante, así que el nombre de la propiedad es demostrablemente un literal de módulo. La guarda sobra y se fue con él. El comportamiento es idéntico.
Notas
Descartarlas como falsos positivos era la alternativa; corregir era preferible, porque una afirmación en un comentario vale menos que una forma verificable. La prueba que fijaba la guarda era un grep del texto fuente; ahora hay una estructural (confirmada en rojo contra la forma antigua) y una de comportamiento que envía __proto__ como JSON en crudo — un literal de objeto habría fijado el prototipo en vez de crear la propiedad. 3021 → 3022 pruebas.
[1.233.1] — 2026-09-10
Corregido — la guía no describía el control que añadió v1.233.0, y la misma sección aún traía instrucciones que dejaron de ser ciertas hace 200 versiones.
Corregido
DOC-1 — la entrada «Usar el valor por defecto (…)» expresa el estado «la clave no está definida», y la diferencia con elegir ese mismo valor de la lista es invisible y con consecuencias: la primera no escribe nada y te sigue dando el valor del proyecto según cambia; la segunda lo fija en .env y te deja en el viejo, en silencio. Tres viñetas en el ### Behavior existente ×17.
DOC-2 — una fila de la tabla no nombraba clave alguna: la celda contenía (server uses default UA), describiendo HH_USER_AGENT, que v1.19.0 eliminó, y mandaba registrarse en dev.hh.ru — inútil desde v1.65.0. Las tres menciones obsoletas desaparecen; la fila de solución de problemas se reescribe, no se borra: desde julio de 2026 hh.ru devuelve HTTP 451 a IPs no rusas, así que una IP residencial rusa sí hace falta.
La tabla de claves lista 6 de 45 mientras su título promete un registro. Una frase ×17 la declara una selección y apunta a #/config.
Notas
El nuevo help-recognized-keys.test.mjs compara la primera columna de las tablas de claves con KNOWN_KEYS en vivo. 3018 → 3021 pruebas; la ayuda sigue en 32 H2 / 122 H3.
[1.233.0] — 2026-09-10
Corregido — tres hallazgos en la ruta de guardado de #/config; CONFIG-3 lo causó la propia corrección de v1.232.1.
Corregido
CONFIG-3 — una elección deliberada que coincidía con el valor sembrado se descartaba como «sin tocar». initial confunde dos orígenes: sembrado desde .env (la igualdad significa que el campo no se editó — correcto omitirlo) y sembrado desde defaultValue (la igualdad es ambigua: nunca se abrió, o se abrió y se estuvo de acuerdo). Con HOST ausente el control muestra 127.0.0.1; escribir ese mismo valor y guardar enviaba {} y la clave seguía sin existir — el único camino era escribir un valor erróneo, guardar y volver a poner el correcto. Corregido registrando seededFromFile junto a initial; donde la semilla viene del archivo nada cambia, y donde viene del valor por defecto dirty pasa a ser una segunda señal válida.
Eliminar una clave informaba «· 0» porque el servidor solo devolvía lo escrito. Ahora responde también con removed, calculado contra la instantánea previa, y el aviso cuenta ambos.
Un desplegable podía fijarse pero nunca vaciarse: ninguno de los 18 ofrecía una opción vacía. Ahora cada uno encabeza con «Usar el valor por defecto (…)», y una clave sin definir selecciona esa entrada en lugar del valor. Una nueva clave i18n ×17.
Notas
Línea base de pruebas unitarias sin cambios (3018); navegador 111 → 116, y las cinco nuevas se confirmaron fallando contra el código anterior.
[1.232.1] — 2026-09-10
Corregido — Guardar en #/config escribía campos que el usuario nunca tocó.
Corregido
CONFIG-2. v1.57.1 siembra un control sin valor con su defaultValue para que el campo muestre lo que el servidor realmente usa, mientras que save() consultaba dirty solo para los secretos y enviaba todos los demás campos siempre. Así, el valor puesto ahí para MOSTRAR viajaba al servidor como si el usuario lo hubiera elegido: cambiar un desplegable enviaba 28 claves y fijaba 18 que nadie había abierto. Nada se comporta distinto ese día, porque lo escrito son los valores por defecto; lo que cambia es el estado de cada ajuste, de «sin definir, sigue el valor del proyecto» a «fijado en .env». Cuando cambie un valor por defecto, quien haya pulsado Guardar alguna vez se quedará atrás en silencio.
Notas
Comparar con cfg.values no basta: un select sin valor muestra su defaultValue mientras lo almacenado es '', y difieren con toda honestidad — el cuerpo bajó de 27 claves a 19, no a una. Ahora se compara con aquello con lo que se sembró cada control. Además, el número de claves escritas vuelve a verse en el aviso. 105 → 111 pruebas de navegador.
[1.232.0] — 2026-09-09
Corregido — dos defectos de una pasada de QA en navegador; ninguno proviene de v1.231.5.
Corregido
PROFILE-1 — #/profile desplazaba lateralmente toda la página. El valor de cada campo era un <div> anónimo con solo estilos de fuente en línea, así que su overflow-wrap calculado seguía siendo normal. Una URL de LinkedIn no ofrece a CSS ningún punto de corte legal, de modo que el ancho min-content es la cadena entera; los elementos de rejilla usan min-width: auto y se niegan a encogerse por debajo, así que la tarjeta reventó su pista, luego la fila, la columna principal y el documento — 23 px de desplazamiento real a 1280 px. Corregido con una clase compartida .card-value con overflow-wrap: anywhere, más .card-row > * { min-width: 0 }. anywhere, no break-word: solo anywhere cuenta al calcular min-content.
CONFIG-1 — POST /api/config aceptaba cualquier valor en un campo select. {"LLM_PROVIDER":"not-a-provider"} devolvía 200 y se escribía en el .env; después nada fallaba, porque el resolutor caía en su alternativa en silencio y la elección del usuario no surtía efecto. LLM_PROVIDERS ya se exportaba desde env-config.mjs: el validador simplemente no lo consultaba.
Notas
Solo se valida LLM_PROVIDER, un estrechamiento deliberado del informe: el dominio real de los modelos pertenece al proveedor y cambia entre nuestras versiones. Un valor ya guardado queda tolerado — el formulario reenvía todos los campos no secretos en cada guardado, así que la versión estricta congelaba la página. También se añadió el descriptor de OLLAMA_API_KEY. 3013 → 3018 pruebas.
[1.231.5] — 2026-09-09
Seguridad — siete avisos cerrados, uno de ellos una ejecución remota de código crítica. No cambió ningún archivo de código fuente.
Seguridad
Fusionado desde Dependabot: multer 2.2.0 → 2.3.0 (alta — denegación de servicio por fuga de descriptores de fichero al abortar) y svgo 4.0.2 → 4.1.0 en el sitio (alta y media — removeScripts dejaba pasar enlaces ejecutables mediante espacios de nombres y manejo de contenido, y saneaba de forma incompleta el HTML ejecutable dentro de foreignObject).
Después otros cinco, revelados por el reescaneo que dispararon esas fusiones. Cada rango afectado ya estaba cubierto por el especificador ^ existente, así que hacía falta una actualización, no una edición del manifiesto: astro 7.1.0 → 7.3.2 — crítica, ejecución remota de código por la optimización de imágenes AVIF (corregida en 7.2.8), y con ella un bypass de autorización por falta de comprobación del límite de segmento de ruta (7.2.4); sharp 0.35.3 → 0.35.4 — alta, vulnerabilidades de libheif; y js-yaml 4.3.1 → 4.3.2 en ambos manifiestos — alta.
Notas
«Solo dependencias» no equivale aquí a «sin riesgo». js-yaml analiza cada configuración que lee la aplicación, multer acepta cada subida, y Astro avanzó una versión menor. Verificado: npm run test:ci 3013/3013, la suite de navegador 4/4 y una reconstrucción completa del sitio con las mismas 86 páginas, los 17 espejos del changelog y un facts.json correcto. npm audit: 0 vulnerabilidades en ambos manifiestos.
[1.231.4] — 2026-09-07
Corregido — v1.231.3 arregló las secuelas del clic en Doctor, no el momento. Reportado de nuevo, desde móvil y desde escritorio.
Corregido
withSpinner seguía reescribiendo el botón mientras la petición estaba en vuelo. v1.231.3 hizo que capturara los nodos hijos y los restaurara en finally, y volvían — pero la señal de ocupado seguía ejecutando button.replaceChildren(document.createTextNode('⏳ ' + original)). Así que durante toda la ejecución de doctor.mjs el cuadrado de 36 px contenía el texto ⏳ 🩺Doctor y se convertía en una pastilla ancha que rompía la fila; en escritorio se veía como un ⏳🩺Doctor apelmazado. Restaurar los hijos arreglaba las secuelas, no el momento. Un botón con hijos elemento ya no se reescribe en absoluto: su señal es la clase .is-loading, que la hoja de estilos ya atenuaba y que ahora cambia el icono por un reloj de arena en CSS, de modo que la geometría no puede variar: 36 px en reposo, 36 px en vuelo, 36 px después a 320 px. Los botones de solo texto — 23 de los 24 puntos de llamada — conservan el reloj antepuesto.
Notas
La lección es sobre la aserción, no sobre el código. La comprobación de v1.231.3 esperaba a que se limpiara aria-busy y solo entonces medía: por construcción únicamente podía ver el estado que el bloque finally ya había reparado. La nueva comprobación mantiene ella misma la promesa abierta en lugar de competir con una petición real, toma la muestra a mitad de vuelo y exige que el ancho no cambie. 3012 → 3013 pruebas. Ambas se confirmaron fallando contra el código de v1.231.3.
[1.231.3] — 2026-09-07
Corregido — pulsar Doctor rompía la barra superior en el móvil. Reportado desde un teléfono el mismo día que salió v1.231.2.
Corregido
UI.withSpinner aplastaba los hijos del botón que decoraba. Su señal de ocupado pasaba por textContent: guardar button.textContent, escribir '⏳ ' + original, volver a escribirlo. Asignar textContent reemplaza todos los hijos por un único nodo de texto — y v1.231.2 acababa de dividir cada acción de la barra superior en .btn-ico + .btn-label, ocultando la etiqueta bajo max-width: 900px. Así que la primera pulsación en Doctor destruía ambos <span> para siempre: las reglas móviles se quedaban sin nada que coincidir, la etiqueta volvía como texto desnudo y el cuadrado de 36 px crecía hasta una pastilla 🩺Doctor que se montaba sobre el conmutador de tema. No se curaba solo — únicamente recargar la página restauraba el marcado. La corrección captura los nodos hijos y los restaura con replaceChildren; los botones de solo texto, que son la mayoría de los 24 puntos de llamada, van y vuelven exactamente igual que antes.
- FIND-3 — el campo de búsqueda se reducía a 8 px en un teléfono de 320 px. Al poner la barra en una sola fila, el searchbar quedó como el único elemento flexible, y el changelog anterior lo decía: «puede encogerse hasta nada». Y se encogió hasta nada: 8 px de campo a 320 px, 28 px a 340 px, un carácter de un marcador de 21. Pulsable y escribible, pero ilegible como campo. Ningún
min-widthlo arregla: a 320 px la barra sostiene 32 px de relleno, un menú de 40 px y 162 px de acciones, sin 120 px que dar. Por eso, por debajo de 420 px — el ancho en que el campo por fin supera los 100 px solo — se oculta tras una lupa y se despliega por toda la barra al tocarla: 182 px a 320 px, 222 px a 360 px, 252 px a 390 px, sin desbordamiento en ningún ancho. El botón conserva un único nombre accesible y comunica el estado conaria-expanded, y el cambio 🔍/✕ es CSS puro — ningún JS escribe en ese botón, que es lo que rompió Doctor en esta misma versión. Por encima de 420 px nada cambia. Detectado por una pasada de regresión en navegador contra la build en vivo.
Notas
Es la misma clase de defecto que la trampa de applyI18n() señalada en v1.231.2 — código que asigna textContent a un elemento que ahora tiene hijos elemento — alcanzada por otra vía. La lección se generaliza: dividir el contenido de un elemento en <span> hijos convierte a cada escritor de textContent ya existente sobre ese elemento en un fallo latente, y esos escritores no siempre están en el archivo que estás editando.
Asegurado por partida doble: node:vm sobre el withSpinner real y un clic en un navegador real a 320 px. Ambos se confirmaron fallando contra el código anterior. 3009 → 3012 pruebas (el caso de navegador es opcional y no entra en test:ci).
[1.231.2] — 2026-09-06
Corregido — la barra superior móvil se partía en dos filas, el servidor ejecutaba un padre más antiguo y el redirect de cvstart.ru daba 404 en cualquier ruta que ya nombrara un idioma.
Corregido
- La barra superior móvil vuelve a una sola fila, con acciones solo de icono. Los botones de acción se forzaban a una segunda fila a todo el ancho, así que un teléfono mostraba
[☰ · búsqueda]sobre[🔔 🌙 Doctor Abrir Scan]— dos píldoras anchas en una línea propia. Ahora son cuadrados de 36 px (🩺 / ⚡) junto a la campana y el conmutador de tema, y todo cabe en una fila a 320 px. Medido, no estimado a ojo: el ancho del documento iguala al viewport a 320, 360, 390 y 430 px, y en toda la banda 560–760 px, donde antes los botones se superponían a la barra de búsqueda. El idioma ya no afecta a la disposición móvil, porque la etiqueta está oculta y el botón es un cuadrado fijo; por encima de 900 px las etiquetas vuelven sin cambios. El nombre accesible viene dearia-label, ya que una etiqueta condisplay:nonequeda fuera del cálculo del nombre. - El servidor ejecutaba el padre 1.31.0 aunque la versión se compiló contra 1.32.0.
/opt/career-ops/srctenía archivos de 1.31.0, así que/api/healthen resumecraft.ru informabaparentVersion: 1.31.0. Se sincronizaron 95 archivos de ejecución y se eliminaron dos. Detectado por QA de navegador contra la superficie en producción. cvstart.ruanteponía/ru/a cualquier ruta, sin mirar lo que ya había.cvstart.ru/ru/helppasaba acvstart.org/ru/ru/helpycvstart.ru/en/helpacvstart.org/ru/en/help; ambos 404. Lo primero es lo que se obtiene al copiar una URL del sitio principal; lo segundo dejaba al visitante angloparlante sin acceso alguno a la versión en inglés. Ahora una ruta que ya nombre uno de los 16 prefijos de idioma pasa tal cual,ense elimina (cvstart.org sirve el inglés en la raíz y no tiene/en/) y el resto sigue recibiendo/ru/. La comprobación está anclada, así que/enterprise/no se confunde conen.
Cambiado
- 98 prompts de QA superados movidos a
qa/archive/superseded-prompts/.qa/README.mddocumenta desde v1.137.0 que en el nivel superior solo queda el prompt de la versión actual — la regla no se aplicó durante 95 versiones. Ahora el README dice que el archivado ocurre al publicar una versión, y se corrigen sus propias líneas base obsoletas (≥1238→≥3009pruebas, 13 → 17 idiomas). - Otros cinco prompts salieron del nivel superior: se declaraban vigentes sin serlo.
REGRESSION-FINAL.mdse llamaba a sí mismo «el único prompt de regresión autorizado e independiente de versión», pero sus §§11–15 son cinco registros de ciclos cerrados (v1.55.x → v1.59.7) y su línea base de ayuda declarada era 28 H2 / 103 H3 en 16 idiomas frente a 32 / 122 y 17 hoy: un agente de QA que lo siguiera habría fallado en §9 al primer paso.QA-REGRESSION-PROMPT.mdtenía nombre sin versión y contenido de v1.226.0.QA-REGRESSION-PROMPT-v1.76.0-FULL.mdestaba fijado 155 versiones atrás,REGRESSION-PROMPT-FINAL.mdcierra el ciclo v1.58.52 → v1.59.10 yQA-FULL-REGRESSION.mdlos duplicaba a todos con base v1.131.1. Con ellos se fuePARENT-SYNC-WORKLIST-v1.26.0.md(el proyecto padre ya va por 1.32.0) y el fechadoUX-AUDIT-2026-07-06.mdpasó areports/. Quedan cuatro drivers, con sus líneas base actualizadas a los valores reales de v1.231.2.qa/README.mdse reescribió para describir la carpeta que existe.
Notas
- Dos trampas de especificidad quedan comentadas en
app.css: las reglas de escritorio se declaran más abajo que el bloquemax-width: 900px, así que a igual especificidad ganan — elgapmóvil se quedó en 24 px (lo que desbordaba 320 px) y el icono siguió oculto (botones cuadrados vacíos). applyI18n()asignael.textContent, así que la clave i18n pasó del<button>a un<span class="btn-label">interno; en el botón habría borrado el icono en cada cambio de idioma.- El primer rsync del padre se abortó a mitad en un archivo que ya no existe localmente, dejando un despliegue parcial que informó éxito. Los despliegues del padre se verifican ahora por suma de comprobación, nunca por código de salida.
[1.231.1] — 2026-09-06
Corregido — el barrido de recuentos de v1.231.0 reescribió el nombre de la cuenta en todos los enlaces.
Corregido
- Cada enlace
Fighter90pasó aFighter92. El barrido del recuento de fuentes de v1.231.0 sustituía90por92con una guarda de límite de dígito, que descarta correctamente290y901pero coincide con el90dentro deFighter90: delante hay una letra y detrás el token simplemente termina. 562 enlaces en los 17 README, los 17 paquetes de ayuda y los espejos de ayuda del sitio, más 138 en el wiki, apuntaban a una cuenta inexistente; el bannerproviders.pngy todos los enlaces «editar en GitHub» daban 404. Lo detectó un usuario, por la imagen que faltaba. - Cinco referencias históricas a versiones se desplazaron. El barrido
85 → 87convirtióv1.85.0env1.87.0, inventando una versión que nunca se publicó.
Notas
- Un límite de dígito no basta: protege un número de los dígitos vecinos, no de formar parte de un identificador o de una cadena de versión. Los recuentos en sí son correctos: 92 fuentes, 87 adaptadores EN, 3009 pruebas.
- El repositorio padre no se vio afectado.
[1.231.0] — 2026-09-05
Añadido — dos fuentes de escaneo del career-ops padre v1.32.0 (Collage, Telegram estricto), el modo REST de Gem y un defecto que el port sacó a la luz en el código del propio padre.
Añadido
- Dos fuentes nuevas, 90 → 92 (87 EN + 5 RU),
ALL_ADAPTERS85 → 87. Ambas sin token.- Collage (
provider: collage) — la API pública de sitios de empleo de Collage HR. El segmento de ruta es la dirección del sitio que el inquilino eligió, no un slug del nombre de la empresa, así que se lee deapi:o de una URLsecure.collage.co/jobs/<dirección>y nunca se adivina: una dirección adivinada escanearía el tablero de otra persona. Una entrada que no nombre ninguna de las dos falla ruidosamente. - Telegram (estricto) (
provider: telegram-channel) — lee las mismas vistas previast.me/s/<canal>que la fuentetelegramexistente, con el intercambio opuesto: un mensaje se convierte en fila solo si nombra a un empleador Y enlaza a una página de vacante. Se descartan los resúmenes, los mensajes de empleador oculto y los de solo contacto. Poca cobertura por diseño —aguas arriba se midió un 32-77% de mensajes que pasan— pero cada fila lleva un empleador real y un enlace real. Ambas se publican, como en el padre: son productos distintos, no duplicados.
- Collage (
- Gem ganó un modo REST opcional junto a su ruta GraphQL (
api: https://api.gem.com/job_board/v0/<board>/job_posts). Nunca se deriva de un id de tablero —una URL derivada sería una suposición sobre qué superficie expone un inquilino— y la ruta GraphQL sigue siendo la predeterminada. Portado del padre #3783.
Corregido
- El filtro de ubicación de
telegram-channelno veía el cirílico y nombraba ciudades como empleadores. ElLOCATIONISH_REdel padre usaba\b, un límite solo ASCII que nunca se activa junto al cirílico, así que todas sus alternativas rusas (москва,спб,офис,удалёнк*) eran inalcanzables ySenior Engineer | Москваdevolvía «Москва» como empleador. Es exactamente la sustitución de campo equivocado que toda la política de atribución existe para impedir, y golpeaba al público principal del proveedor: un canal en ruso escribe la ubicación en cirílico. El port de web-ui usa comprobaciones Unicode (\p{L}/\p{N}), lo mismo que el padre ya hace una constante más abajo enROLE_WORD_RE. Fijado por una prueba que falla con la forma ASCII. Aún no corregido aguas arriba.
Notas
- El trabajo de recuperación por facetas de Workday del padre (#3851, #3874) no necesitó port. web-ui no pagina ni divide Workday por facetas en absoluto —un solo POST con
offset: 0—, así que ninguna de esas rutas de código existe aquí. Misma conclusión que en v1.229.0, verificada de nuevo. providers/_types.jscambió solo en documentación (el campo opcionalJob.salary).- Pruebas: 2962 → 3009 (+47).
[1.230.0] — 2026-09-05
Cambiado — tres correcciones de proveedores desde el career-ops padre v1.32.0, y un defecto que el propio port sacó a la luz.
Añadido
- Welcome to the Jungle acepta una expresión
filtersde Algolia aplicada en el servidor (wttj: { filters: "offices.country_code:FR AND contract_type:full_time" }). Una palabra clave por sí sola no estrecha un tablero global: es el ranking de relevancia de Algolia, y no los filtros del escáner, quien decide quémax_hitsresultados llegan — los nuestros se aplican después, sobre lo ya recibido, así que todo lo que queda más allá del tope es invisible por bien que encaje. Un filtro reduce el conjunto en lugar de reordenarlo, de modo que el tope por consulta sube de 200 a 1000. Con un filtro,queriespasa a ser opcional: la consulta vacía significa «todo lo que pase el filtro». Portado del padre #3757.
Corregido
- Radancy ya no avisa «truncated» cuando un inquilino simplemente sirve menos ofertas de las que anuncia su propio banner. El padre contrastó
data-total-resultscon nueve inquilinos reales: cinco coincidieron, pero cuatro — uno pequeño y otro enorme — entregaron entre un 10 % y un 56 % menos de ofertas únicas de las anunciadas, sin una sola fila duplicada. El banner exagera lo que el índice de búsqueda del inquilino sirve realmente, así que comparar el recuento acumulado contra él daba falsos positivos en la mayoría de tableros. El aviso ahora solo aparece cuando el recorrido lo detuvo nuestro propiomax_jobs/max_pages, el único caso sobre el que quien llama puede actuar;totalResultsqueda en el mensaje como contexto y nunca decide. Portado del padre #3839. - Radancy evita la caché en cada petición de fragmento JSON. Una capa de caché delante de esa ruta repite respuestas obsoletas en algunos inquilinos: la misma oferta reaparece páginas después mientras otra no se sirve nunca, reproducido nueve veces de nueve aguas arriba. Las cabeceras
Cache-Control/Pragmano cambiaron nada; un parámetro aleatorio por petición sí. Debe ser único por llamada, nunca derivado del número de página: un parámetro determinista sigue dando a la caché una clave estable y, con ella, la misma correspondencia estable y equivocada. Portado del padre #3839. - Las listas de oficinas de Greenhouse tienen orden determinista. Greenhouse no promete el orden de
offices[]entre respuestas, y el conjunto recuperado del árbol/officesva en su orden de recorrido. Ambos llegan al campolocationde la oferta, así que un tablero que reordenase sus oficinas reescribía esa cadena — y una oferta sin cambios se leía como nueva. El padre arregló el conjunto (#3839); web-ui tenía una segunda instancia del mismo defecto que el padre no tiene — el respaldooffices[0], usado cuando la oferta no trae localización propia — y ambos van ahora ordenados.
Notas
- La corrección de iCIMS del padre (#3728) no necesitó port. Lee todas las entradas
jobLocationdel JSON-LD de la página de detalle, porque algunos inquilinos emiten una entrada íntegramenteUNAVAILABLEantes de la dirección real. El iCIMS de web-ui toma la localización del HTML de la página de listado y nunca abre una de detalle, así que esa ruta de código no existe aquí. - El cambio de
_http.mjsno necesitó port. ExtraeisRefusedRedirectErrorpara quediscover-ats.mjsy la capa de reintentos dejen de discrepar sobre el mismo error. El comportamiento deisRetryableErrorno cambia, y web-ui no tiene un segundo consumidor con quien compartir el veredicto. - La deduplicación por localización (#3751) no se replica: vive en el pipeline
scan.mjsde la CLI del padre; la ruta de escaneo de web-ui tiene la suya. - Registro sin cambios: 90 fuentes (85 EN + 5 RU). Pruebas: 2956 → 2962 (+6).
[1.229.0] — 2026-09-03
Añadido — cuatro fuentes de escaneo del career-ops padre v1.31.0: Built In, Feishu Jobs, Garena y MokaHR.
Añadido
- Cuatro fuentes nuevas, 86 → 90 (85 EN + 5 RU),
ALL_ADAPTERS81 → 85. Las cuatro sin tokens: ni clave de API ni inicio de sesión.- Built In (
provider: builtin) — el tablón tecnológico estadounidense, en sus nueve mercados. Los empleadores publican allí directamente, así que es un agregador como RemoteOK. No trae consulta por defecto: una entrada sinqueries:nicategories:no escanea nada y lo dice, porque una fuente compartida no debe llevar los términos de búsqueda de una persona. Un mercado fuera de la lista permitida se rechaza, nunca se sustituye en silencio por el tablón nacional. - Feishu Jobs (
provider: feishu-jobs) — 飞书招聘, el endpoint que llama la propia página de empleo del inquilino. Dos formas de host y ninguna más. Usan rutas distintas para la página de la vacante, así que una sola plantilla habría generado enlaces muertos para la mitad. - Garena (
provider: garena) — una sola empresa, un host fijo.officeda forma al enlace, no al listado. - MokaHR (
provider: mokahr) — SaaS de RR. HH. china. La respuesta viene cifrada: un sobre AES-128-CBC cuya clave viaja junto al texto cifrado. Es ofuscación, no seguridad, pero un análisis JSON normal no ve nada, así que descifrar es parte de leer el tablón.
- Built In (
Corregido
htmlToTextpodía dejar pasar un abridor de etiqueta incompleto al texto plano.safe <img src=x onerror=1salía comosafe <img src=x onerror=1. Ahora cada decodificación va seguida de un barrido de marcado y el abridor final pierde su ángulo: el texto sigue legible y queda inerte. Portado del padre (#3491).
Notas
- Del arreglo de Workday del padre no se portó nada. Recupera inquilinos cuyo backend limita la paginación en el offset 2000 — web-ui no pagina Workday en absoluto (un POST en
offset: 0, 100 filas), así que no hay límite que sortear. - Los arreglos de
verify-cv-factsymerge-trackerno necesitaron port: web-ui ejecuta esos scripts desde el checkout del padre en vez de replicar su lógica. - El salario de Built In se formatea aquí como texto: el
parseSalarydel padre devuelve{ min, max }y el contrato de web-ui guardasalarycomo cadena. - Pruebas: 2909 → 2956 (+47).
[1.228.5] — 2026-08-30
Corregido — la versión informada describía el archivo en disco, no el código en ejecución.
Corregido
-
/api/healthreleíapackage.jsonen cada petición, así que un proceso obsoleto informaba una versión que no estaba ejecutando. QA encontró una instancia local respondiendoversion: 1.228.4mientras devolvía 404 para/api/ping, una ruta añadida en 1.228.3: arrancada antes del despliegue, sirviendo el código viejo e informando la versión del archivo nuevo. Así es exactamente como un despliegue que copia archivos pero nunca reinicia parece un éxito: la única cadena que revisa un operador es la que no puede ver el problema. Misma clase que el enlace simbólico de v1.228.1, donde rsync abortaba a mitad de transferencia y aun así informaba éxito.La versión se captura ahora al cargar el módulo, de modo que describe el código en ejecución. Un proceso obsoleto informa la versión antigua — la verdad, y visible de inmediato.
parentVersionsigue leyéndose por petición: describe el checkout del proyecto padre, que este proceso no carga y que legítimamente puede cambiar bajo un servidor en marcha.
[1.228.4] — 2026-08-30
Corregido — la sonda de vida sin autenticación hacía trabajo de disco en cada petición.
Corregido
GET /api/pingleía y analizabapackage.jsonen cada petición. Es el único endpoint alcanzable sin credenciales, así que una lectura de disco más un análisis JSON por petición es una palanca de denegación de servicio entregada a cualquiera — CodeQL lo marcó como falta de limitación de tasa. La versión se lee ahora una sola vez al registrar la ruta; no puede cambiar sin reiniciar, así que el manejador no hace E/S alguna. Limitar la tasa habría acotado el daño; quitar el trabajo quita la palanca.- El enmascarado del nombre real del titular del perfil fuera de loopback queda fijado por una prueba.
/api/healthya lo sustituía porhidden, pero solo porque esa fila comparte el guardiánhidden ?? valuecon la raíz del proyecto. Nada lo comprobaba, así que una edición que le diera valor propio habría filtrado el nombre de una persona real a la red local sin que nadie lo notara.
[1.228.3] — 2026-08-30
Fixed — the docs assistant could not answer in English, Telegram company names arrived truncated, and a monitor had no way to see the service was up.
Fixed
-
The docs assistant answered “no matching help sections were found” in English while the same question worked in Russian. Retrieval was fine; the context builder was not.
buildAskPromptdidif (ctx.length + sec.body.length > MAX_CONTEXT) break— abreak, so one oversized section ended the loop and left the context empty, and the assistant truthfully reported it had nothing. The English §5 had grown to 16 081 bytes against a 14 336-byte budget (partly because v1.228.0 addedtelegram_channelsto it); the Russian §5 was 13 009 and fit. Same ranking in both languages — §5 was first — only the size differed.Two changes: a section past 6 KB is now split at its
###boundaries, sotelegram_channelsis its own retrievable chunk (1 340 bytes) that ranks first in both languages; and an oversized section is skipped or truncated rather than ending the loop. The guide went from 32 chunks to 75. -
A Telegram company label yielded the sentence after it, not the name.
Компания: FinCore Technology (продуктовая разработка, высоконагруженные системы)becameFinCore Technology (продуктовая разработка, высоконагруженны— capped at 60 characters, cut mid-word. Three of 141 rows. The name now ends at the first separator that introduces a description — a spaced dash (soCoca-Colasurvives), a comma, semicolon, pipe or opening parenthesis — and any remaining cap falls on a word boundary. This source attributes a company as fact; a truncated sentence is as wrong as a guessed name, only less obviously so.
Added
GET /api/ping— a liveness probe that leaks nothing.{ ok, version }and no more./api/healthreports absolute paths, the profile owner’s real name and which API keys are set: legitimate for an authenticated operator, never for the public internet. A monitor behind a Basic-auth gate previously had no way to see the service was up at all.
[1.228.2] — 2026-08-30
Corregido — una búsqueda de dos palabras en /api/scan-results buscaba la frase, no las palabras, y contaba de menos.
Corregido
-
?q=ahora compara cada término por separado en lugar de la frase literal. La consulta era un únicoincludes(q)sobre título, empresa y ubicación, así queпродакт менеджерsolo encontraba filas con esas dos palabras exactamente contiguas: 32 de las 162 que contienen ambas.Así una coincidencia estrecha se convierte en una respuesta equivocada. El asistente de Telegram informó 9 — con honestidad, porque 9 fue lo que dijo el endpoint. Su recuento era veraz; el del endpoint no.
La comparación es Y entre términos: una fila debe contener todos.
менеджер разработчикdevuelve cero, no la unión.
[1.228.1] — 2026-08-29
Corregido — un enlace simbólico roto en el repositorio que truncaba despliegues en silencio.
Corregido
-
.claude/skills/refero-designera un enlace simbólico versionado a una ruta que nunca ha existido aquí. Apuntaba a../../.agents/skills/refero-design, es decir a.agents/en la raíz del repositorio; ese directorio no existe, y el enlace llegó en el paquete de paridad v1.118.0 desde una máquina con otra anidación de carpetas. Desde entonces, quien clonara el repositorio obtenía un enlace roto.No era inofensivo. rsync aborta al llegar a él — un despliegue de hoy informó éxito mientras el servidor seguía con la versión anterior, y la discrepancia solo salió a la luz porque después se comprobó la versión. El
node test-all.mjsdel proyecto padre también falla cuando hay un checkout de web-ui dentro de su árbol. Nada lo referenciaba: las únicas coincidencias dereferoson la palabra polaca preferowany.
[1.228.0] — 2026-08-29
Añadido — canales de Telegram como fuente de escaneo y un modo de consulta paginado en /api/scan-results.
Añadido
-
Los canales de Telegram son ahora una fuente escaneable. Un bloque
telegram_channels:enportals.ymllos lista; cada uno se lee desde la vista previa públicahttps://t.me/s/<canal>. Registro: 85 → 86 fuentes (81 EN + 5 RU),ALL_ADAPTERS80 → 81.Telegram no publica RSS y la Bot API no puede leer un canal que el bot no administra, así que ninguna de las dos vías obvias sirve. La vista
/s/es HTML plano renderizado en servidor: sin autenticación, sin JS, sin cookies. Verificado en 16 canales: 15 devolvieron 20 publicaciones cada uno con un GET simple.channel:acepta cualquier forma de escribirlo. El analizador se ancla en las tres cosas que un rediseño de Telegram moverá al final:data-post, el contenedor del texto y la fecha ISO<time datetime>. -
GET /api/scan-resultsincorpora un modo de consulta paginado.?q=<texto>&limit=50[®ion=ru|en][&set=filtered|fresh][&offset=N]devuelve{ total, returned, offset, limit, set, rows }, dondetotales el recuento ANTES de paginar. Sin parámetros la ruta sigue devolviendo el snapshot íntegro: la tabla#/scandepende de esa forma.
Corregido
- El asistente de Telegram respondía con una fracción de los datos. Su skill ya apuntaba a
/api/scan-results, pero la ruta solo sabía devolver el snapshot completo —unos 2 MB— que no cabe en el contexto de un agente. Ante «encuentra todos los Product Manager» informó 9 coincidencias donde el snapshot tenía cientos, y dijo que «trabajaba con un índice local limitado». Era cierto: veía el endpoint pero no podía consumirlo. Con el modo paginado la misma pregunta cuesta 2 KB y devuelve untotalhonesto. El skill también se reescribió: pedía resumir las publicaciones nuevas, es decir apuntaba afreshen lugar defiltered.
Notas
- Una publicación de canal es prosa, no un registro de empleo. No hay campos estructurados: se toma el título de la primera línea sustantiva, y empresa/ubicación/salario de etiquetas explícitas. Separar ofertas reales de anuncios y resúmenes queda en manos del
title_filterexistente, el mismo ya afinado para otras fuentes. Son pistas, no un listado. - La empresa nunca se adivina. Sin etiqueta, la fila se atribuye al canal (
@rabotaphp): un empleador equivocado entra en el tracker como un hecho. - Un análisis vacío es un error, no «no hay vacantes». t.me responde con una redirección a un canal privado o inexistente; devolver cero filas se leería como un día tranquilo y ocultaría una errata en la configuración para siempre.
jobGeekshace exactamente eso (HTTP 302) y deliberadamente no está configurado. - La detección de remoto usa una frontera de palabra Unicode en lugar de
\b, que es solo ASCII: la misma trampa que el filtro de títulos sufrió en v1.227.3, aquí detectada por un test antes de publicar. - 15 canales configurados y verificados en vivo: 299 publicaciones, 15/15 canales. 30 tests nuevos. Pruebas: 2865 → 2895.
- Los recuentos de tests del README saltaron cientos — y no, esta versión no los añadió. Los README localizados llevaban totales congelados: 1856 / 1945 / 1955 / 549 / 419 / 284 / 2527, cada uno obsoleto desde una versión distinta, y varios contradiciendo la insignia de su propia página. Ahora todos dicen 2895, verificado ejecutando la suite y no copiando una insignia. Igual de obsoletos estaban Playwright (70 / 90 / 12 → 101), e2e smoke (20 → 21), módulos de rutas (12 / 32 → 37), flujos smoke (12 → 22) y el número de archivos de test (218 → 329). La fila
Totalrepetía el recuento unitario y por eso no sumaba las tres suites de debajo; ahora muestra la suma real (3040).
[1.227.5] — 2026-08-29
Corregido — el escaneo regional agotaba la memoria del servidor y una suite de Playwright fallaba por su propio desmontaje.
Corregido
- El escaneo regional mataba el servidor por OOM.
ru-scanneracumulaba todos los resultados en bruto de todas las consultas y solo deduplicaba y filtraba al final. La lista de consultas está deliberadamente llena de casi sinónimos (Golang,Go разработчик,Golang разработчик,Senior Go…), así que la misma vacante vuelve una vez por consulta y el array retenía varias veces el recuento único — con las descripciones, que son el grueso del objeto y que los filtros descartan casi por completo. Medido: 742 MB de heap en una ejecución real de 21 consultas. El servidor limita el heap de Node a 490 MB (V8 lo dimensiona a partir de los 956 MB de RAM de la máquina), así que el escaneo tumbaba el servicio conFATAL ERROR: Reached heap limit— cuatro veces en un día. Ahora la deduplicación y el filtrado ocurren por consulta, y la misma ejecución bajó a 177 MB, un 76% menos. Los recuentos informados no cambian: el total único lo lleva unSetde cadenas URL, así que «Total found» significa exactamente lo mismo que antes. net::ERR_SOCKET_NOT_CONNECTEDhacía fallar CI al azar. Tres suites de Playwright llaman awindow.stop()para detener las peticiones en vuelo antes de afirmar que no hubo errores de consola. Chromium informa de ese único acto deliberado con tres nombres según dónde estuviera el socket:ERR_ABORTEDal cancelar,ERR_EMPTY_RESPONSEen vuelo yERR_SOCKET_NOT_CONNECTEDcuando el socket ya no estaba. El filtro compartido solo listaba los dos primeros, así que el mismo desmontaje tumbaba una ejecución al azar. Apareció en el barrido de locales entr— la suite con más navegaciones — mientras queplaywright-formsyplaywright-smoke, que también llaman awindow.stop(), estaban a una ejecución del mismo fallo.
Notas
- Por qué el escaneo empezó a fallar ahora. La lista de consultas pasó de 14 a 21 al añadir búsquedas de product manager. La memoria de la implementación anterior escalaba con el total de resultados, así que un 50% más de consultas superó el techo de la máquina. Las consultas nuevas no tienen nada de malo: solo expusieron un patrón de asignación que llevaba mucho tiempo a un cambio de configuración de fallar.
- El cambio de deduplicación preserva el comportamiento, y eso está probado, no afirmado.
tests/ru-scanner-dedup-order.test.mjsfija la dirección incómoda —un duplicado posterior que NO pasa los filtros debe expulsar a uno anterior que sí pasó, porque la pasada final antigua conservaba solo la última copia y luego la filtraba— y ejecuta 200 comprobaciones aleatorias de equivalencia contra la implementación anterior. - El filtro de consola sigue estrecho. Solo la familia de peticiones canceladas es benigna. Conexión rechazada, fallo de DNS, reinicio, cualquier 5xx y toda excepción JS no capturada siguen haciendo fallar la aserción, fijado por tests nuevos.
- Dos hipótesis se probaron y descartaron en vez de enviarse: un timeout de Caddy (su configuración está limpia,
flush_interval -1es correcto para SSE) y elrequestTimeoutpor defecto de 5 minutos de Node (una reproducción a escala mostró que no corta una respuesta larga en streaming). La evidencia real fueReached heap limiten el journal. Pruebas: 2860 → 2865.
[1.227.4] — 2026-08-29
Corregido — word: / stem: seguían ignorándose en content_filter; la v1.227.3 solo arregló el filtro de títulos.
Corregido
content_filterno respetaba los prefijos. La v1.227.3 enseñó atitle_filterlos prefijosword:/stem:del padre, pero dejó acontent_filtercon su propiolower.includes(k)en crudo, así que allí una entrada con prefijo seguía comparándose como el texto literal"word:java"— el mismo no-op silencioso, un filtro más allá.content_filterlee la DESCRIPCIÓN de la oferta y su valor por defecto siempre fue subcadena simple: por eso un negativo simplejavarechaza todo lo que apenas mencione «JavaScript»; el prefijo es la forma de excluir una entrada de esa regla. Portado elcompileContentKeyworddel padre, que comparte la maquinaria de prefijos pero deliberadamente no ancla palabras cortas: el filtro de títulos ancla siglas de 2–3 letras porque «COO» dentro de «Coordinator» siempre es un error, mientras que una secuencia corta dentro de un párrafo de prosa suele ser intencionada (aws,gcp,sql,go).
Notas
- Detectado en la revisión por IA del PR de la v1.227.3, que notó que el código no cubría lo que afirmaba su propio comentario: decía «
title_filter/content_filter» cuando solo estaba conectada la ruta de títulos. Ahora el comentario distingue explícitamente: ambos filtros respetan los prefijos, solo el de títulos ancla siglas. - También verificado a partir de esa revisión, y sin cambios: pasar la regla de siglas de 2–3 letras de
\ba la frontera Unicode es idéntico en comportamiento para ASCII y más estricto solo donde debe serlo —COO_leadycoo1se conservan (guion bajo y dígito son caracteres de palabra en ambos),Coordinatorse conserva, y «Директор COO» y «VP «Продукт»» se descartan correctamente. 14 casos límite comprobados. - Las entradas sin prefijo de
content_filtermantienen su coincidencia previa byte a byte, una descripción vacía o ausente sigue pasando, y unword:/stem:vacío no coincide con nada en vez de con todo. 6 casos nuevos entests/content-filter.test.mjs. Pruebas: 2852 → 2858.
[1.227.3] — 2026-08-29
Corregido — los prefijos word: / stem: del filtro de títulos se ignoraban en silencio, dejando esas líneas de filtro sin efecto.
Corregido
- web-ui no implementaba los prefijos
word:/stem:del proyecto padre.title_filter(ycontent_filter) usan por defecto coincidencia de subcadena sin distinguir mayúsculas, y por eso un negativo simpleinterntambién rechaza «International Product Manager» e «Internal Tools Engineer». En vez de cambiar ese valor por defecto —un cambio rompedor para toda instalación configurada— el padre hizo la precisión opcional por entrada:word:internsolo coincide con la palabra completa ystem:agentdebe iniciar una palabra pudiendo continuar (separando «Agentforce» de «Reagents»). web-ui no tenía ninguno, así que hacía coincidir la entrada con el texto literal"word:intern", que no aparece en ningún título de empleo. La línea quedaba en nada: el mismoportals.ymlfiltraba bien por CLI y nada aquí, de modo que cada oferta de prácticas excluida seguía llegando a la tabla#/scan. Portado deltitle-keywords.mjsdel padre, incluida la frontera de palabra compatible con Unicode (\p{L}\p{M}\p{N}_) en lugar de\b, que es solo ASCII y coincidía dentro de palabras en títulos acentuados o cirílicos.
Notas
- Detectado al revisar un
portals.ymlreal que acababa de adoptarword:internprecisamente para que uninternsimple dejara de descartar «International Product Manager». El prefijo funcionaba en la CLI; web-ui se quedaba con todas las prácticas en silencio. Ahora el comportamiento es idéntico. - Los valores por defecto no cambian. Una palabra clave sin prefijo mantiene su coincidencia previa: subcadena para frases y para todo lo que contenga caracteres no alfabéticos (
.NET,SAP,L&D), y anclaje a límites de palabra para siglas de 2–3 letras. Unword:/stem:vacío se trata como errata y no coincide con nada: como negativo, un patrón vacío coincidiría con todos los títulos y anularía un escaneo entero por un solo dos puntos suelto. - La regla de siglas de 2–3 letras pasa a usar esa misma frontera Unicode: antes usaba
\b, así quecooyword:coodiscrepaban en títulos no ASCII — dos formas de escribir una misma regla. - 6 casos nuevos en
tests/title-filter.test.mjs. Pruebas: 2846 → 2852.
[1.227.2] — 2026-08-29
Corregido — #/config se desplazaba lateralmente en móviles estrechos.
Corregido
- MOBILE-1:
#/configdesbordaba horizontalmente en viewports ≤ 352 px. Cada campo de tipo select llevaba unmin-width: 300pxen línea. Un estilo en línea gana a la hoja de estilos, así que el control no podía encogerse con su contenedor: en un viewport de 320 px el borde derecho del select caía en 353 px, la página ganaba scroll horizontal y los controles se salían por la derecha. Afectaba a los 18 selects de proveedor/modelo (cfg-llm-providery todos loscfg-*-model). Ahoramin-width: min(300px, 100%)+max-width: 100%, de modo que los 300 px cómodos se aplican solo cuando hay espacio. Dispositivos en el umbral o por debajo: iPhone SE (1.ª gen, 320 px), Android antiguos, pantalla exterior del Galaxy Fold.
Notas
- Por qué las pasadas móviles anteriores no lo vieron. 360 px y 375 px tienen justo el margen suficiente para ocultar el desbordamiento, y las suites de Playwright corren en una ventana de 1280 px. Nada en CI había medido nunca un viewport más estrecho que 375 px. El nuevo
tests/playwright-narrow-viewport.mjsrecorre las rutas con formularios a 320 px y falla ante cualquier desbordamiento horizontal, nombrando en el mensaje el elemento más ancho; un segundo caso fija que ningún select de#/configsobrepasa el viewport. Se verificó primero que reprodujera el fallo (scrollWidth 353 > clientWidth 320, 18 selects con borde derecho 353). Playwright: 99 → 101. - Conocido y deliberadamente aún sin gate: 280 px (pantalla exterior del Galaxy Fold) sigue desbordando ~5 px, por una causa distinta — contenido largo dentro de los bloques
<details>de la tarjeta de configuración y la barra lateral de 256 px frente a un viewport de 280 px, no elmin-widthen línea que corrige esta versión. Reflujar a un ancho que ningún móvil mayoritario declara es trabajo aparte; la listaWIDTHSde la suite ya está preparada. - También se corrigió el prompt de QA de v1.227.1, que llamaba a la ruta de seguimiento
#/modes/followup. Las mode-pages se registran con el slug desnudo (Router.register(cfg.slug)), así que la ruta es#/followupy#/modes/followupda 404 correctamente. Solo documentación. - Suite unitaria sin cambios en 2846 (la cobertura nueva es de navegador).
[1.227.1] — 2026-08-29
Corregido — el desglose de fuentes de la ayuda no cuadraba consigo mismo y el banner de GitHub se imprimía como marcado en bruto en la app.
Corregido
- §17 daba un total y un desglose que no sumaban. Todos los paquetes de ayuda decían «el registro
server/lib/sources/incluye 85 adapters — 78 en inglés + 5 rusos». 78 + 5 = 83, no 85; en ja/ko la deriva llegaba a 77 (= 82). Cada fuente nueva subía el total y dejaba el sumando intacto, y ambos números llevaban varias versiones divergiendo. Ahora el desglose es 80 EN + 5 RU en las 17 locales. Importa más que la estética:docs/help/<lang>.mdes el ÚNICO corpus que fundamenta «Ask the docs», así que quien preguntara cuántas fuentes hay recibía dos números contradictorios de una misma frase. - La misma frase fijaba un ancla de versión obsoleta. «A partir de v1.213.0 el registro …» sobrevivió en 16 locales mientras el contador se movía dos veces (83 en v1.219.0, 85 en v1.226.0). El ancla ya se había arreglado a mano en v1.210.1 y simplemente volvió a derivar. Se elimina en favor de una redacción sin versión: el contador se comprueba contra el registro vivo, que no puede quedarse obsoleto.
- El banner de proveedores se imprimía como marcado escapado al inicio de
#/help. v1.225.0/v1.225.1 lo daban por «eliminado en la app porUI.md()(sin soporte de imágenes)». Eso mezcla dos cosas:UI.md()escapa primero — escapa cada byte del origen antes de cualquier transformación markdown, que es la frontera XSS correcta pero lo contrario de eliminar. Nadie quitaba el banner porque nadie lo intentaba, y las 17 locales abrían#/helpcon 268 caracteres de<p align="center"><img src="https://…literal. Un nuevo punto de entrada compartido (server/lib/help-markdown.mjs::stripGithubOnlyBlocks) descarta bloques HTML de imagen aislados al leer, tanto paraGET /api/help/:langcomo para el corpus del docs-assistant. Los archivos conservan el banner, así que GitHub sigue mostrando el escaparate. - La columna «#» del tablero de cadencia estaba vacía desde v1.117.0. Leía
entry.appNum, perofollowup-cadence.mjsemitenum;appNumnunca existió en el payload. Ahora se leen ambos. La línea «Next up» de v1.227.0 heredó el mismo fallo y se corrige con ella.
Notas
- Los cuatro pasaron desapercibidos porque nada los vigilaba. Los gates de ayuda (
canonical-docs-coverage,help-ui,help-ru-config-section) cuentan ENCABEZADOS — 32 H2 / 121 H3 — y ningún test había leído nunca un número dentro de un párrafo ni mirado qué sirve realmente la entrada de ayuda. Dos suites nuevas lo cierran:tests/help-source-counts.test.mjs(el total declarado es igual aSOURCES.length; los sumandos son el reparto EN/RU vivo y suman el total; el párrafo no fija versión) ytests/help-banner-strip.test.mjs(el banner permanece en disco para GitHub, nunca sobrevive al filtrado, la primera línea renderizada de cada paquete es su encabezado, el filtrado sigue siendo estrecho y ambos consumidores pasan por él). Se verificó que ambas fallaran contra estos mismos errores antes de confirmarlas. Pruebas: 2837 → 2846. - Sin cambios en registro, escáner ni contratos de rutas; las fuentes siguen en 85 (80 EN + 5 RU) y
ALL_ADAPTERS80.
[1.227.0] — 2026-08-28
Añadido — SEEK Hong Kong (JobsDB), un filtro de solo pendientes en los seguimientos y los empleadores confidenciales muestran quién los presentó.
Añadido
- JobsDB Hong Kong a través de la fuente Jobstreet/SEEK existente. Hong Kong opera bajo la marca JobsDB pero se apoya en la misma plataforma SEEK tras el mismo endpoint v5, así que no necesita una fuente aparte: basta con
hk.jobsdb.comen la lista de hosts permitidos ysiteKey: HK-Main. El registro no cambia: 85 fuentes (80 EN + 5 RU) yALL_ADAPTERS80 — esto es un MERCADO nuevo sobre un adaptador existente, no un adaptador nuevo. - Filtro de solo pendientes en el tablero de cadencia (
#/modes/followup). Una casilla nueva reduce el tablero a lo accionable ahora mismo. La condición se lee de laurgencyque ya calculafollowup-cadence.mjsdel proyecto padre (urgent/overdue), nunca delstatusdel tracker, que esapplied/responded/interviewy no coincidiría con nada. Cuando no hay nada pendiente, el tablero nombra el seguimiento programado más próximo en lugar de mostrar un vacío. La lógica vive en el módulo puropublic/js/lib/followup-view.js, espejo defollowup-view.mjsdel padre. - Los empleadores confidenciales se atribuyen a la agencia que los presentó. Una oferta con el empleador reservado se registra como
?en el tracker y el reclutador va en la columnaVia.#/trackermostraba un?pelado, así que varias filas confidenciales eran indistinguibles y el resolutor de logos no tenía nada que buscar. Ahora esa fila se lee «Confidencial · vía <agencia>», resuelve su logo por el nombre de la agencia y se encuentra por ese nombre en el buscador. Solo presentación: el campo canónicocompanynunca se sobrescribe con el reclutador. Nuevo módulo puropublic/js/lib/company-presentation.js, espejo decompany-presentation.mjsdel padre.
Corregido
- Jobstreet/SEEK llamaba a un endpoint obsoleto. La fuente seguía apuntando a chalice-search v4 (
/api/chalice-search/v4/search), que SEEK retiró; el padre migró a/api/jobsearch/v5/searchy web-ui nunca lo siguió. Ahora toda entrada Jobstreet/SEEK consulta v5. La forma del ítem v5 se lee de forma nativa (URL construida desdeid,locations[0].label,advertiser.description,salaryLabel), desaparece el parámetro de proyecciónsolrFieldspropio de v4, y una entrada cuyoportals.ymlaún fije la ruta v4 muerta conserva su hostname de mercado mientras la ruta se reconstruye sobre v5 — sin edición del usuario.
Notas
- Paridad con career-ops 1.30.0 (HEAD del padre
8d64f65→6cc46a4; suVERSIONsigue en 1.30.0 — release-please va por detrás demain). No portado, con motivos: relleno de ubicación JSON-LD de iCIMS — la fuente iCIMS de web-ui no tiene ganchoenrichDate()(solo páginas de listado), así que no hay ruta que corregir; stderr no fatal del CLI (#1974) — el runner de web-ui decide el fallo solo por el código de salida y nunca tuvo esa heurística; trazado de rutas de Turbopack — web-ui no usa bundler; arreglos decv-sync-check— se relevan en solo lectura y fail-soft; secuenciación de Playwright enscan.mjs, respuestas Block H, guardas de updater/doctor/update-system, la verificación de sintaxis recursiva, el dashboard en Go y las autopruebas dejd-skill-gap/verify-cv-facts— superficies solo-CLI que web-ui no invoca, o autopruebas sin cambio de comportamiento. Pruebas: 2818 → 2837.
[1.226.0] — 2026-08-27
Añadido — dos fuentes de empleo vietnamitas: ITviec y CareerViet.
Añadido
- ITviec + CareerViet (Vietnam). Dos portales vietnamitas con host fijado se unen al escáner vía entradas
provider: itviec/provider: careerviet: ITviec (itviec.com, tecnología) y CareerViet (careerviet.vn, amplia cobertura). Ambos son scrapers HTML sin tokens, con host-pin anti-SSRF, solo-HTTPS, fail-soft por empresa y topes de página. El registro ya trae 85 fuentes (80 EN + 5 RU),ALL_ADAPTERS80 — ambos aparecen en el filtro Fuente de#/scan.
Notas
- Paridad con career-ops 1.30.0 (parentVersion 1.29.0 → 1.30.0). No portado (solo CLI/comunidad o ya cubierto): el Hired Wall,
/calibrate, clean-markers, packs de plantillas, doctor, pdf/LaTeX y las CLIs de eval/tracker/scan que web-ui no invoca; el arreglo de_html-to-textyroleFuzzyMatchya estaban; el prefijotitle-filter stem:se difiere. Conjunto de pruebas: 2818.
[1.225.1] — 2026-08-27
Corregido — el banner de logotipos de proveedores ahora se muestra en el README y la ayuda.
Corregido
- El banner de v1.225.0 no se veía en GitHub: el
images/providers.svgtenía unfont-familyinválido (comillas dobles anidadas) que GitHub rechaza, el<img>usaba unsrcrelativo (el HTML<img>no resuelve rutas relativas en GitHub) y al bloque<p>le faltaba una línea en blanco final. Ahora apunta a unimages/providers.pngrenderizado con URL absoluta y separación por líneas en blanco; el SVG también se corrigió a XML válido.
Notas
- Solo docs; sin cambios de código ni pruebas. La vitrina de cvstart.org y las fichas de la app ya eran correctas (SVG en línea, no este archivo). 2784
[1.225.0] — 2026-08-27
Añadido — un banner de logotipos de proveedores en el README y la guía de ayuda.
Añadido
- Banner de logotipos de proveedores. Un único
images/providers.svgautónomo — los 18 proveedores de LLM con el logotipo real de marca (simple-icons, CC0) para los 11 que publican uno y un monograma con color de marca para los 7 que no — encabeza ahora el README (×17) y la guía de ayuda integrada. (La app renderiza las fichas con el logotipo real; el banner se muestra en GitHub y el renderizador seguro para XSS lo elimina en la app.)
Notas
- Solo docs — sin cambios de código ni pruebas; el SVG está incrustado (sin recurso remoto). Fuentes de escaneo sin cambios: 83. Conjunto de pruebas: 2784.
[1.224.0] — 2026-08-26
Cambiado — las fichas de proveedores LLM ahora muestran los logotipos reales de marca.
Cambiado
- Logotipos reales de proveedores. Las fichas de proveedor — campos de clave API en Ajustes, las filas de Uso, el chip del panel, el resumen “Activo” de Ajustes, el resultado de la ⚡ eval y el banner de bienvenida — ahora muestran el logotipo real de marca de los 11 proveedores que publican un icono de código abierto (Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub · DeepSeek · Kimi · MiniMax · Mistral · Ollama; SVG de una sola ruta de simple-icons, CC0). Los 7 sin icono publicado (Hermes · GLM/Z.ai · Grok · Together · Fireworks · BytePlus Ark · Volcengine Ark) conservan el monograma con color de marca. Sigue siendo seguro para la CSP por construcción: la ruta del logo es una constante estática incrustada — sin recurso remoto, sin
innerHTML.
Notas
- Sin cambios de ruta ni de comportamiento; la misma API
ProviderLogo.el(slug).provider-logo.jscreció (las rutas de logos incrustadas). Fuentes de escaneo sin cambios: 83. Conjunto de pruebas: 2784.
[1.223.0] — 2026-08-26
Corregido — una evaluación ⚡ en vivo exitosa se pintaba como error; además, instala el coworker de OpenWorker desde su URL de GitHub o .zip.
Corregido
- Tono del badge de resultado de la ⚡ eval. Una evaluación en vivo exitosa ya no se colorea como fallo. Los proveedores en proceso (OpenRouter, DeepSeek, …) no devuelven
codede subproceso, así que el estrictocode === 0era falso y todo éxito salía rojo (aun mostrando “exit 0”). El tono ahora sigue el resultado — solo un subproceso que sale con código distinto de 0 lo pinta de rojo; un resultado en vivo sin código esbadge-ok, y el sufijo “· exit N” solo aparece cuando hay un código real.
Cambiado
- Instala el coworker de OpenWorker desde su URL de GitHub / .zip. La documentación del coworker ahora describe el instalador de un comando (
curl … | bash, idempotente — reutiliza un career-ops / web-ui existente) y las tres vías de instalación en OpenWorker (URL de GitHub ·.zip· importación de un solo archivo) endocs/integrations/openworker.md, la ayuda §32 (×17) y el README.
Notas
- Se cerró una brecha de cobertura antigua: la fuente Recruitee ahora tiene una prueba de extremo a extremo del arreglo de ángulo entrecomillado de v1.214.2 (un
>dentro de un atributo de etiqueta no debe filtrarse a la descripción). Fuentes de escaneo sin cambios: 83. Conjunto de pruebas: 2783.
[1.222.0] — 2026-08-26
Cambiado — las ayudas de campo de los proveedores extendidos (DeepSeek … Volcengine Ark) ahora están localizadas en los 17 idiomas.
Cambiado
- Ayudas de campo de proveedores localizadas. Cada
config.<slug>Hintde los 11 proveedores compatibles con OpenAI añadidos en v1.216.0–v1.217.0 (DeepSeek · GLM/Z.ai · Kimi · MiniMax · Mistral · Grok · Together · Fireworks · Ollama · BytePlus Ark · Volcengine Ark) — las ayudas de clave API, modelo y URL base en Ajustes → Claves API — ahora se traduce en los 17 idiomas en lugar de recurrir al inglés. Las URLs de registro, los ids de modelo y el marcador ⚡ de eval en vivo se conservan tal cual.
Notas
- Sin cambios de comportamiento: el
hintFallbacken inglés defield-specs.jssigue protegiendo una clave ausente, y una nueva prueba de contrato verifica que cadahintKeyde los descriptores de campo se resuelve en los 17 idiomas. Fuentes de escaneo sin cambios: 83. Conjunto de pruebas: 2779.
[1.221.0] — 2026-08-26
Seguridad — defensa en profundidad contra DNS-rebinding en la ruta de fetch del escáner.
Seguridad
- El núcleo HTTP del escáner (
fetchJson/fetchTextenserver/lib/http-json.mjs) ahora resuelve el host de cada fuente en la ruta de red real y se niega a conectar si resuelve a una dirección privada, loopback, link-local, CGNAT o de metadatos de nube — cerrando un vector de DNS-rebinding. Los hosts del escáner ya están fijados a dominios públicos, así que es defensa en profundidad; la ruta de URL del usuario ya tenía fijación de conexión más fuerte (safe-fetch.mjs). El guard corre solo en el transportefetchreal — unfetchImplde prueba inyectado nunca se resuelve.
Notas
- Sin cambio de comportamiento para un escaneo sano. Fuentes de escaneo sin cambios: 83. Pruebas: 2775.
[1.220.0] — 2026-08-26
Añadido — escanea varias categorías de Get on Board desde una sola entrada.
Añadido
- Una entrada
getonbrdpuede definircategories: [programming, operations-management, machine-learning-ai](o un únicocategory:) en vez de solo el feedprogramming; los avisos se deduplican por URL entre categorías, con un tope de 12. Las entradas existentes quedan idénticas (el valor por defecto sigue siendoprogramming).
Notas
- Solo servidor; el anclaje de host SSRF (www.getonbrd.com) y
redirect:'error'no cambian; un slug de categoría inválido se rechaza antes de cualquier petición. Fuentes de escaneo sin cambios: 83. Pruebas: 2771.
[1.219.0] — 2026-08-26
Añadido — Torre se une al escáner, y ahora toda etiqueta de proveedor es correcta.
Añadido
- Torre (
provider: torre) — el marketplace de talento pan-LatAm de torre.ai, búsqueda pública sin autenticación con roles remotos que no llegan a Greenhouse/Lever/Ashby; una petición acotada por entrada. El registro trae ahora 83 fuentes (78 inglesas + 5 rusas). Además, monograma del proveedor activo en el banner de bienvenida.
Corregido
- El escaneo de a16z Speedrun se detenía antes de tiempo en un tablero que rota (una página corta a mitad de barrido se tomaba como la última, truncando a 149 de ~300); ahora confía en el
total_pagesdel feed. Y el banner de bienvenida resolvía el proveedor desde un mapa de 4 entradas (mostrando el slug crudo para los otros 14) — ahora usa la etiqueta+monograma de los 18; la nota de proveedores de Ajustes pasa de 7 a 18.
Notas
- La opción de varias categorías por entrada de Get on Board aún no está aquí (requiere un refactor a nivel de fuente). Pruebas: 2768.
[1.218.0] — 2026-08-26
Añadido — insignias de proveedor (y nombres correctos) allí donde se muestra un proveedor.
Añadido
- Monogramas de proveedor en el chip del panel, el resumen “Activo” de Ajustes y la cabecera del resultado de la ⚡ evaluación.
Corregido
- Etiquetas obsoletas: tres superficies resolvían el nombre desde un mapa de 5 entradas (el resultado de evaluación etiquetaba como “Gemini” a cualquier proveedor no-Anthropic); ahora usan
ProviderStatus.label(18). El recuento de claves mostraba “/ 7” → 18. La lista de slugs del hintLLM_PROVIDERganóark/arkcnen los 17 idiomas.
Notas
- Solo cliente, seguro para CSP. Sin cambios de servidor. Fuentes de escaneo: 82. Pruebas: 2758.
[1.217.1] — 2026-08-26
Endurecimiento de pruebas — cobertura de endpoint para todo el catálogo de proveedores.
Añadido
- Una prueba parametrizada de
run<Provider>que verifica endpoint, modelo por defecto y autenticación Bearer para Kimi, MiniMax, Mistral y Fireworks.
[1.217.0] — 2026-08-26
Añadido — Ark se une a la lista de LLM (18 proveedores). BytePlus Ark y Volcengine Ark —los modelos Doubao de ByteDance— ya son proveedores ⚡ en vivo, con el mismo núcleo compatible con OpenAI y una URL base por región.
Añadido
- BytePlus Ark (
ARK_API_KEY, internacional) y Volcengine Ark (ARK_CN_API_KEY, China) — Chat Completions compatible con OpenAI víarunOpenAICompatible(). DefineARK_MODEL/ARK_CN_MODEL(un modelo Doubao o tu id de endpointep-…);ARK_BASE_URL/ARK_CN_BASE_URLcambian de región. Ambos entran en el ordenauto(al final), con su monograma en Ajustes y Uso. La interfaz web abarca ya 18 proveedores en vivo.
Notas
- Mismo camino de configuración de confianza + comprobación de esquema http(s) que el resto (nunca el validador SSRF de URL de empleo). Fuentes de escaneo sin cambios: 82. Pruebas: 2755.
[1.216.0] — 2026-08-26
Añadido — nueve proveedores LLM más, cada uno con su insignia de marca. Tus evaluaciones ⚡ en vivo ya pueden ejecutarse con DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together, Fireworks o un Ollama totalmente local — a una clave de distancia, cada uno marcado por un monograma junto a su campo en Ajustes.
Añadido
- Nueve proveedores compatibles con OpenAI. DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together AI, Fireworks AI y Ollama (totalmente local, sin clave). Configura una clave — o
OLLAMA_BASE_URLpara Ollama — en Ajustes y se une al ordenautodespués de Hermes; fija cualquiera conLLM_PROVIDER. Together también aloja Inkling de Thinking Machines (thinkingmachines/Inkling). La interfaz web abarca ya 16 proveedores en vivo. - Insignias de proveedor. Un mosaico con la inicial y el color de marca, seguro para CSP, marca cada proveedor junto a su campo de clave en Ajustes y en su fila de la página de Uso — SVG en línea, sin logos externos, nada sale de tu equipo.
Notas
- Las URL base de los proveedores son configuración de confianza que pasa por
runOpenAICompatible()con comprobación de esquema http(s) (se permite el loopback de Ollama), nunca el validador SSRF de URL de empleo. Fuentes de escaneo sin cambios: 82. Pruebas: 2752.
[1.215.0] — 2026-08-26
Añadido — ejecuta toda tu búsqueda de empleo desde OpenWorker. Un nuevo coworker mueve este pipeline desde la app de coworker de IA open source de Andrew Ng, y la ayuda en la app ahora lo cubre para que el asistente Pregúntale a los docs te guíe.
Añadido
- Coworker de OpenWorker. Un coworker sin código de OpenWorker — Fighter90/career-ops-coworker — ejecuta toda la búsqueda de empleo (escanear, puntuar, adaptar, rastrear, seguir) y puede abrir este panel, todo desde tu escritorio. Es una persona de un solo archivo en 17 idiomas, verificada como instalable con el propio cargador de OpenWorker.
- Ayuda §32 “Run it from OpenWorker” (los 17 idiomas) lo documenta, para que el asistente Pregúntale a los docs de la app y el botón flotante de ayuda respondan sobre él. También enlazado desde el pie del landing, el README, la wiki y
docs/integrations/openworker.md.
Notas
- Solo docs + sitio — sin cambios de código de la app. Fuentes de escaneo sin cambios en 82. Conjunto de pruebas: 2742.
[1.214.2] — 2026-08-25
Corregido — la canalización HTML→texto compartida (descripciones de Greenhouse / Recruitee) ya no filtra atributos de etiqueta en la descripción cuando un valor de atributo contiene un carácter >.
Corregido
- Las descripciones HTML con
>dentro de un atributo se limpian correctamente. El removedor de etiquetas tras las descripciones de Greenhouse y Recruitee usaba una coincidencia ingenua que se detenía en el primer>, así que una etiqueta como<a title="salary > 100k">dejaba el texto de atributo final (100k">) en la descripción. Ahora respeta los valores de atributo entre comillas (simples o dobles), quita el marcado antes de decodificar entidades (para que una comilla codificada dentro de un atributo no se vuelva un delimitador falso), y sigue preservando un<>literal en texto plano.
Notas
- Conjunto de pruebas: 2742. Fuentes de escaneo sin cambios en 82. Nada más nuevo del padre esta ronda: los demás cambios se retransmiten en solo lectura (arreglos de follow-up / weekly-digest / rejection-latency / salary-gap no requieren cambios en web-ui) o no son superficies de web-ui (un plugin externo X/Xquik BYO-key, arreglos de CLI/verify). Los elementos opcionales — descripciones de detalle de SmartRecruiters, salto de agregadores en detect-reposts, prefijos de título
stem:/word:, protección anti-DNS-rebinding — siguen en cola.
[1.214.1] — 2026-08-25
Corregido — las descripciones de Ashby ahora se limitan a la misma longitud que Greenhouse y Recruitee, así que un filtro de contenido se comporta igual en las tres bolsas y una bolsa no puede inflar la caché de escaneo.
Corregido
- Las descripciones de Ashby ahora respetan el mismo límite de longitud que las demás bolsas. v1.214.0 pasaba el cuerpo de Ashby al filtro de contenido, pero — a diferencia de Greenhouse y Recruitee, que limitan a 4000 caracteres — el de Ashby se mapeaba directo sin límite (algunos eran 4× más largos). Así, la misma palabra de
content_filterpodía coincidir en Ashby pero no en Greenhouse solo según dónde cayera en el texto, y una bolsa podía añadir cientos de KB a la caché de escaneo que carga#/scan. Ashby ahora se limita a los mismos 4000 caracteres (su texto ya es plano, así que se trunca, nunca se le quitan etiquetas).
Notas
- Conjunto de pruebas: 2738. Fuentes de escaneo sin cambios en 82.
[1.214.0] — 2026-08-25
Corregido — las descripciones de Greenhouse, Ashby y Recruitee ahora alimentan el filtro de contenido, así que los filtros de palabras / país / visado sí coinciden con esas bolsas; y “Associate <título senior>” (incluidos rangos académicos) ya no se clasifica como entry-level.
Corregido
- Los filtros de contenido ya funcionan en Greenhouse, Ashby y Recruitee. Estas bolsas ahora llevan el texto completo de cada publicación como su descripción, así que un
content_filter(o un filtro de palabras de país / visado) coincide con el cuerpo del puesto en vez de dejar pasar la bolsa a ciegas. Ashby y Recruitee traen el cuerpo en su payload de lista gratis; Greenhouse ya lo hacía. Los tres se depuran por una única canalización HTML→texto compartida. - “Associate Director” / “Associate Professor” es senior, no entry-level. El clasificador de seniority (usado por
skip_tiers:) tomaba la palabra “associate” como marca de entry-level aun cuando prefija un título senior. Ahora lee “Associate” — incluidos rangos académicos (Associate Professor, Associate Dean) — como senior, manteniendo las variantes junior reales (Associate Attorney, Associate Editor) como entry-level, y aún clasifica un “Intern, Associate Dean” inicial como prácticas.
Notas
- Conjunto de pruebas: 2736. Fuentes de escaneo sin cambios en 82. En cola para más adelante: enriquecimiento opcional de descripciones de SmartRecruiters (una petición de detalle por publicación), el salto de tableros
aggregator: truede detect-reposts, y los prefijos de título de palabra ancladastem:/word:— cada uno una configuración opcional, no un cambio de comportamiento por defecto. El endurecimiento anti-DNS-rebinding sigue en cola para su propia versión de seguridad.
[1.213.0] — 2026-08-22
Añadido — MyCareersFuture, la bolsa de empleo nacional de Singapur, como fuente de escaneo. Corregido — las vacantes de Greenhouse ahora llevan su texto completo para que los filtros de contenido funcionen, y las vacantes remotas de Ashby ya no quedan ocultas tras una ubicación de solo-ciudad.
Añadido
- MyCareersFuture (Singapur) (mycareersfuture.gov.sg) — una nueva fuente de escaneo sin tokens para la bolsa de empleo pública nacional de Singapur, gestionada por Workforce Singapore. Selecciónala en el filtro Fuente de
#/scan, o añade una empresa conprovider: mycareersfuturey una lista opcional dekeywords(recurre a los roles objetivo de tu perfil, como Job Bank). Lee la API de búsqueda pública, con host fijado, sin clave.
Corregido
- Las vacantes de Greenhouse ahora se pueden filtrar por contenido. Las bolsas de Greenhouse se obtienen con el cuerpo completo de la publicación, decodificado a texto plano como la descripción — así un
content_filter(o un filtro por palabras de país/visado) que lea la descripción ahora sí coincide con las vacantes de Greenhouse en vez de dejarlas pasar a ciegas. - Las vacantes remotas de Ashby ya no se descartan por un filtro de ciudad. Ashby mantiene el modelo de trabajo (Remote/Hybrid/Onsite) separado de la ciudad de la oficina, así que un rol totalmente remoto seguía leyéndose como p. ej. “San Francisco” — y un filtro de ubicación que bloquee esa ciudad ocultaba un puesto que podrías aceptar. Ahora se añade “Remote” a la ubicación cuando el rol es remoto, y
workplaceTypeprevalece sobre unisRemoteobsoleto para no etiquetar mal un rol híbrido anclado a oficina.
Notas
- Fuentes de escaneo: 82 (77 en inglés + 5 rusas). Conjunto de pruebas: 2724. Un endurecimiento contra DNS-rebinding (validar la dirección resuelta de un host antes de conectar) queda en cola para una versión dedicada — necesita un diseño propio de web-ui, no un port directo.
[1.212.1] — 2026-08-21
Corregido — la landing de cvstart.org contaba de menos las fuentes de empleo del escáner (mostraba 80 y omitía Job Bank (Canadá)); ahora vuelve a coincidir con las 81 de la app, y la compilación del sitio falla de forma ruidosa si ambas difieren.
Corregido
- El recuento de “Fuentes de empleo” de la landing vuelve a estar sincronizado con la app. Tras v1.212.0, cvstart.org mostraba 80 bolsas y le faltaba el nuevo chip de Job Bank (Canadá), mientras que la app, el desplegable de escaneo y la guía de ayuda listaban 81. La landing construye su lista cargando el registro en vivo del escáner, y una fuente no se cargó en esa compilación por cómo importaba una dependencia YAML, así que se descartó en silencio. Ahora Job Bank carga esa dependencia de forma diferida, igual que el resto de la app en el momento del escaneo, así que siempre aparece.
- La compilación del sitio ahora se niega a publicar un recuento de fuentes desajustado. Si el registro enumera menos fuentes de las que existen en disco (la firma de una fuente que no cargó), la compilación falla con un mensaje claro en vez de publicar el número equivocado.
Notas
- El comportamiento de la app no cambia — el escáner siempre tuvo las 81 fuentes; solo la landing se vio afectada. Fuentes de escaneo: 81 (76 en inglés + 5 rusas) — sin cambios. Conjunto de pruebas: 2687.
[1.212.0] — 2026-08-21
Añadido — Job Bank (Canadá), la bolsa de empleo nacional federal. Eliminado — EchoJobs (su feed ahora está bloqueado por anti-bots). Corregido — las bolsas con Consider vuelven a dar resultados y las vacantes de Lever multi-ubicación ya no ocultan la mitad de sus ubicaciones.
Añadido
- Job Bank (Canadá) (jobbank.gc.ca) — una nueva fuente de escaneo sin tokens para el servicio nacional de empleo federal de Canadá, una bolsa de alto volumen que ningún agregador cubre bien. Selecciónala en el filtro Fuente de
#/scan, o añade una empresa conprovider: jobbankcay una lista opcional dekeywords(recurre a los roles objetivo de tu perfil). Lee el feed ATOM público, con host fijado, sin clave.
Eliminado
- EchoJobs — retirada. Su feed público ahora está tras protección anti-bots y no devuelve nada, así que mantenerla solo malgastaba un espacio de escaneo.
Corregido
- Las bolsas con Consider vuelven a dar resultados. Consider ahora exige un handshake anónimo (un GET que siembra una cookie de sesión + token CSRF) antes de aceptar la búsqueda; sin él la petición se rechazaba en silencio y la bolsa parecía vacía.
- Las vacantes de Lever multi-ubicación ya no ocultan la mitad de sus ubicaciones. Lever pone una ciudad principal en
locationy el resto enallLocations; leer solo la principal hacía que una vacante abierta en Barcelona Y Montevideo pareciera solo-Barcelona (y se descartara por error con un filtro de ubicación). Ahora se fusionan ambas.
Notas
- Ritmo entre páginas más suave (250 ms, antes 150) en las bolsas paginadas, para ser corteses con los sitios de empleo de un solo host. Fuentes de escaneo: 81 (76 en inglés + 5 rusas) — sin cambios (entra Job Bank, sale EchoJobs). Conjunto de pruebas: 2685.
[1.211.0] — 2026-08-19
Añadido — Yourator, bolsa de empleo tech de Taiwán. Corregido — las entidades acentuadas de título/empresa ahora se decodifican, y una empresa con acento en su nombre ya no se marca por error.
Añadido
- Yourator (yourator.co) — una nueva fuente de escaneo sin tokens para el mercado tech y digital de Taiwán. Selecciónala en el filtro Fuente de
#/scan, o añade una empresa conprovider: yourator. Lee la API JSON pública (sin clave, sin navegador), recorre todas las páginas del portal y emite el enlace real del empleador (su propio ATS) con los parámetros de seguimiento eliminados.
Corregido
- Las entidades con acento ahora se decodifican en todas partes. El decodificador HTML compartido incorporó las letras Latin-1 (
é→ é,ç→ ç, …), así que un portal europeo que escribeDéveloppeuroFrançaisya no deja ese literal en un título, el tracker o un documento generado. (Las mayúsculas siguen en mayúsculas —Ées É, no é— y una búsqueda como&constructor;ahora se resuelve a sí misma.) - Una empresa con un acento en su nombre ya no se marca por error por estar en su propio dominio. «Işık» ahora se pliega a «isik» y coincide con isik.com.tr; «Société Générale» coincide con societegenerale.com. La comprobación anterior borraba las letras acentuadas en lugar de plegarlas a su base ASCII.
Notas
- Fuentes de escaneo: 81 (76 en inglés + 5 rusas). Conjunto de pruebas: 2667.
[1.210.1] — 2026-08-19
Corregido — los títulos y nombres de empresa de Habr Career con «&» o comillas ya no llegan corruptos.
Corregido
- La fuente Habr Career ahora decodifica las entidades HTML del título y del nombre de la empresa antes de continuar. Las tarjetas renderizadas en el servidor llegan con entidades (“Changellenge >>”, “Demand Forecasting & Inventory Optimization”, “ООО "М-ТЕХ"”), así que un «&» sin decodificar fallaba en silencio tu propio filtro de título con «&» —el mismo síntoma que la versión anterior cerró en otros cinco portales— y los nombres de empresa llegaban corruptos al tracker y a los informes. La decodificación de entidades queda completa en las seis fuentes afectadas.
Notas
- Conjunto de pruebas: 2644.
[1.210.0] — 2026-08-19
Añadido — Senjob, la primera bolsa de empleo africana del escáner (Senegal); coincidencia de títulos más precisa en cinco portales más.
Añadido
- Senjob (senjob.com) — una nueva fuente de escaneo sin tokens para Senegal, la primera bolsa africana del escáner. Selecciónala en el filtro Fuente de
#/scan, o añade una empresa conprovider: senjob. Lee el listado público por HTTP simple (sin clave, sin navegador), fija cada solicitud a senjob.com y —al analizar HTML— trata un listado que de repente no arroja nada como un portal roto (un error visible) en lugar de un país sin empleos.
Corregido
- Los títulos con «&» ya no descartan ofertas en cinco portales — en beesite, Cornerstone (csod), Hacker News “Who is hiring”, Phenom y TKMS los títulos llegan con entidades HTML, así que un «&» escapado en un puesto como “R&D Engineer” fallaba tu propia palabra clave “r&d” y la oferta desaparecía en silencio (un veto “sales & marketing” tampoco se activaba). Ahora los títulos —y las ubicaciones de Phenom— se decodifican antes de filtrar.
Notas
- Fuentes de escaneo: 80 (75 en inglés + 5 rusas). Conjunto de pruebas: 2643.
[1.209.0] — 2026-08-17
Añadido — la ayuda integrada ahora cubre cómo registrar el resultado de una candidatura, y «Pregunta a la ayuda» puede guiarte hasta ahí.
Añadido
- La ayuda del Seguimiento (§11) ganó una sección «Registrar un resultado» en los 17 idiomas, que recorre el botón Resultado: elige qué pasó (rechazado / oferta / contratado / rechazada / ignorado / pasó a entrevista), previsualiza lo que hará y regístralo — lo que anota el resultado, archiva el CV y la carta que enviaste, y sincroniza el Estado de la fila por ti. El asistente flotante «Pregunta a la ayuda» lee la guía, así que ahora te lleva a ese botón en lugar de solo sugerirte editar el Estado a mano.
Notas
- Cada paquete de ayuda es ahora 31 H2 / 119 H3 (antes 118); los guards de paridad se ajustaron. Solo documentación — sin cambios de código ni de comportamiento. Conjunto: 2625.
[1.208.2] — 2026-08-16
Corregido — en el móvil los botones de notificaciones y tema ya no quedan sobre el cuadro de búsqueda.
Corregido
- La v1.208.1 evitó que los botones de la barra superior se solaparan con el título de la página, pero en un móvil estrecho —aunque no el más estrecho— y sobre todo en idiomas con etiquetas largas, toda la barra seguía apretándose en una sola fila, así que los botones 🔔 y 🌙 podían quedar sobre el cuadro de búsqueda. Ahora los botones de acción (notificaciones, tema, Diagnóstico, Abrir Scan) siempre pasan a su propia segunda fila a lo ancho en el móvil, así el cuadro de búsqueda se ve completo y nada se solapa.
Notas
- En el móvil los botones de acción de la barra pasan a una segunda fila a todo el ancho, eliminando la frágil franja de “fila casi llena” donde el diseño repartía el espacio negativo sobrante como solape. Un guard de Playwright reproduce ahora el detonante exacto —un idioma de etiquetas largas en la franja 565–640px— y comprueba que los controles de la barra nunca compartan píxeles. Conjunto: 2621.
[1.208.1] — 2026-08-16
Corregido — en el móvil los botones de la barra superior ya no se solapan con la página.
Corregido
- La v1.208.0 pasaba los botones de la barra superior (Diagnóstico, Abrir Scan, notificaciones, tema) a una segunda fila en pantallas estrechas, pero la barra mantenía una altura fija, así que la fila envuelta se salía y quedaba sobre el título de la página. Ahora la barra crece para acoger sus filas y el contenido fluye debajo.
Notas
- La
heightfija de la barra pasó amin-height, así crece con su contenido en cualquier ancho (el escritorio no cambia). Un guard de Playwright ahora también comprueba que la barra no se salga sobre la página. Conjunto: 2621.
[1.208.0] — 2026-08-16
Corregido — la app ya cabe en la pantalla de un móvil: se acabó el desplazamiento lateral.
Corregido
- En pantallas estrechas toda la app se desplazaba de lado —la barra superior, las tablas, los artículos de ayuda y las pestañas de ajustes se salían por el borde derecho. Ahora cada página cabe en cualquier ancho: los botones de la barra superior pasan a una segunda fila, las tablas y bloques de código anchos se desplazan dentro de su propia caja, la ayuda apila su índice sobre el artículo, las filas de botones/pestañas se ajustan, y las rutas o URL largas se parten en lugar de estirar la página.
Notas
- La causa era la clásica trampa min-width: auto de flex/grid más algún elemento ancho sin envolver; resuelto con
min-width: 0en los ítems de grid,overflow-wrapen el markdown/títulos, una tabla de markdown desplazable y el apilado de la cuadrícula de ayuda en el punto móvil. Un guard de Playwright verifica 0 desbordamiento horizontal a 375 px en las rutas principales.tests/playwright-smoke.mjs. Conjunto: 2621.
[1.207.2] — 2026-08-16
Corregido — los planes de IA y los perfiles de orientación ya no se muestran como un volcado de código.
Corregido
- Algunos modelos envuelven toda su respuesta en una valla de código
markdown …. Cuando pasaba, el plan de desarrollo y el perfil de orientación aparecían como un bloque monoespaciado en vez de un documento con encabezados y listas. Ahora se quita esa valla envolvente —solo cuando envuelve toda la respuesta y es explícitamentemarkdown/md, así que una respuesta real depython/js/``` sin lenguaje se deja intacta.
Notas
- Resuelto una sola vez en el paso compartido de limpieza de LLM (
cleanLlmMarkdown), así todas las rutas de IA se benefician, y los bloques de código internos sobreviven.tests/llm-output.test.mjs(+3). Conjunto: 2621.
[1.207.1] — 2026-08-16
Corregido — la página de inicio ya no se desborda de lado en móviles pequeños.
Corregido
- En un móvil estrecho el hero —el titular, la línea de introducción y la terminal de instalación— podía quedar recortado por el borde derecho porque un comando de instalación largo y las columnas del diseño no se encogían a la pantalla. Ahora caben en cualquier ancho; el comando de instalación se desplaza dentro de su propia terminal.
Notas
- También se reforzó una comprobación E2E inestable que podía fallar por un 404 transitorio de un recurso: ahora ignora el ruido de red benigno (favicon / conexión / recurso fallido) como las comprobaciones hermanas, sin dejar de detectar errores reales de script. Sin cambios en el comportamiento de la aplicación. Conjunto: 2618.
[1.207.0] — 2026-08-15
Añadido — registra el resultado de una candidatura directamente desde el seguimiento.
Añadido
- Cada fila del seguimiento tiene una acción Resultado: elige qué pasó (rechazado, oferta recibida, contratado, oferta rechazada, sin respuesta, avanzó a entrevista), añade una nota opcional, previsualiza el estado resultante y regístralo. Registrar archiva los artefactos del CV y la carta enviados y sincroniza el seguimiento al estado canónico — una acción determinista en vez de editar el seguimiento a mano.
Notas
- Nueva
POST /api/outcomeretransmite la CLI de resultados:dryRun:truees una vista previa de solo lectura (localiza la fila, informa del estado resultante, no escribe nada); una llamada real lo registra. Seguridad de escritura: el tipo de resultado se limita al conjunto conocido y todo campo de texto se rechaza si tiene caracteres de control antes del shell-out (argumentos como array, spawn — sin shell).tests/outcome-route.test.mjs. Conjunto: 2618.
[1.206.0] — 2026-08-15
Documentación — la guía de ayuda integrada ahora cubre las cinco funciones más recientes, en los 17 idiomas.
Añadido
- La guía de ayuda integrada —y el asistente «Pregunta a la ayuda» que responde a partir de ella— ahora documenta cinco funciones recientes: Doctor de configuración (Ajustes — revisa tu CV y perfil en busca de huecos y datos de ejemplo olvidados), Descubrir tableros ATS (Portales — encuentra automáticamente el portal de empleo de una empresa), la comprobación «¿sigue activa?» (Seguimiento — si una oferta sigue abierta), la sugerencia «¿reutilizar un CV anterior?» (CV Studio — avisa cuando un CV ya adaptado encaja con un nuevo puesto) y el Registro de habilidades (Analítica — registra puntuaciones de autoevaluación). Cinco subsecciones nuevas, traducidas a los 17 idiomas.
Notas
- La estructura de la guía crece a 31 H2 / 118 H3, con paridad garantizada en cada idioma. Documentación de referencia actualizada:
docs/architecture/API.mddocumenta las cinco rutas de estas funciones, y los contadores de rutas/versión enCLAUDE.mdydocs/sdd/CONVENTIONS.mdestán al día (36 módulos de ruta). Conjunto: 2610.
[1.205.0] — 2026-08-15
Añadido — un Registro de habilidades para anotar resultados de pruebas/evaluaciones.
Añadido
- Un nuevo Registro de habilidades (Analítica → Registro de habilidades) te permite anotar una autoevaluación — empresa, plataforma, habilidad, puntuación % y una nota opcional — que se añade a
data/assessments.tsv, con una lista de entradas anteriores (más recientes primero). Sin tokens, determinista; el formato lo gestiona la CLI del proyecto padre.
Notas
- Nueva
GET /api/assessments(relaya la lista JSON por defecto deassessment-log.mjs; tolerante{available:false}) +POST /api/assessments(escritura explícita: campos pasados como args de array aassessment-log.mjs add). Seguridad de escritura: se rechaza cualquier campo con caracteres de control (un TAB rompería una columna, un salto de línea inyectaría una fila) → 400 antes de escribir; puntuación/umbral limitados a 0–100; longitudes acotadas.tests/assessments-route.test.mjs. Conjunto: 2610.
[1.204.0] — 2026-08-15
Añadido — un panel “Doctor de configuración” en Ajustes que detecta un CV/perfil incompleto o con datos de ejemplo.
Añadido
- Ajustes → Doctor de configuración ahora hace una comprobación sin tokens de tu
cv.mdyconfig/profile.ymly lista los problemas bloqueantes (archivos/campos faltantes) y avisos (datos de ejemplo residuales, métricas fijas) — para detectar una configuración incompleta antes de que debilite tus escaneos y adaptaciones. Solo lectura; se re-ejecuta con un clic.
Notas
- Nueva ruta de solo lectura
GET /api/cv-sync-checkque relayacv-sync-check.mjsdel proyecto padre, que imprime texto + un código de salida (sin--json); la ruta parsea ligeramente sus líneas establesERROR:/WARN:en{ok, errors[], warnings[]}— decide el banner, no el código de salida. Tolerante a fallos{available:false}en instalaciones autónomas.tests/cv-sync-check-route.test.mjs. Conjunto: 2602.
[1.203.0] — 2026-08-15
Añadido — una pista “¿reutilizar un CV anterior?” en CV Studio.
Añadido
- Al abrir una oferta guardada en CV Studio, la app ahora la compara con tus otras ofertas guardadas (solapamiento de palabras determinista, cero tokens) y te dice si la más parecida es suficiente para reutilizar ese CV adaptado, reutilizarlo con retoques o adaptar uno nuevo — para no regenerar desde cero un puesto que ya trabajaste.
Notas
- Nueva ruta de solo lectura
GET /api/jds/:name/reuseque relayajd-similarity.mjsdel proyecto padre (solapamiento Jaccard + guarda de seniority; JSON{decision, score, reason}) una vez por oferta previa (fan-out limitado a 25, gana la mejor); tolerante a fallos{available:false}si falta el script o las ofertas previas.tests/jd-similarity-reuse-route.test.mjs. Conjunto: 2594.
[1.202.0] — 2026-08-15
Añadido — descubre el tablero ATS de una empresa desde #/portals y empieza a seguirlo.
Añadido
- En #/portals, escribe el nombre de una empresa y la app sondea Greenhouse, Ashby y Lever por su tablero público — cero LLM, sin navegador — y muestra los tableros que existen y listan ≥1 empleo. Un clic añade el tablero elegido a las empresas que vigila tu escáner. El sondeo es de solo lectura; la escritura en
portals.ymlocurre solo al pulsar Añadir.
Notas
- Nuevo
server/lib/discover-ats.mjs(sondeo de slug con host fijo y charset validado víasafeGetcon DNS fijado, ≤12 sondeos/petición) +POST /api/portals/discover(solo lectura) yPOST /api/portals/track(escritura explícita:withFileLock+ empalme de texto + reverificación + renombrado atómico; solo hosts ATS conocidos, idempotente). Reutiliza el registro de adaptadores del escáner. i18n ×17.tests/discover-ats-resolver.test.mjs+tests/discover-ats-route.test.mjs. Conjunto: 2588.
[1.201.0] — 2026-08-15
Corregido — un seguimiento con encabezados de columna localizados o variantes ya no se muestra en blanco.
Corregido
- Si tu
data/applications.mdusa encabezados no ingleses o variantes —empresa/puesto/estado/fecha/enlace, oposition/stage/link— el seguimiento los leía con las claves equivocadas y mostraba columnas Empresa / Puesto / Estado / Fecha / Enlace en blanco. Ahora esos encabezados se pliegan a los nombres de campo canónicos y el seguimiento se muestra bien. Un seguimiento en inglés se procesa igual que antes.
Notas
- Nuevo mapa
HEADER_ALIASESy un plegado de normalización enparseApplications(server/lib/parsers.mjs); los encabezados desconocidos o ya canónicos pasan sin cambios.tests/tracker-header-aliases.test.mjs. Conjunto: 2563.
[1.200.0] — 2026-08-15
Añadido — comprobación de un clic “¿sigue activa?” para empleos en ATS desde tu seguimiento.
Añadido
- En #/tracker, una candidatura cuya URL sea una publicación de Greenhouse / Lever / Ashby / Workday / SmartRecruiters muestra ahora un botón “¿Sigue activa?”. Un clic consulta el JSON público del propio ATS — cero tokens, sin navegador — y muestra Activa / Expirada / Desconocida, para detectar vacantes muertas sin abrir cada una. Conservador por diseño: solo un 404/410 definitivo se lee como Expirada; lo ambiguo queda Desconocida (nunca una falsa Expirada).
Notas
- Nuevos
server/lib/liveness-core.mjs+liveness-api.mjsy una ruta de solo lecturaGET /api/liveness?url=(sin escrituras, sin LLM). Segura ante SSRF: la URL pasa porisValidJobUrly luego la API del ATS se alcanza solo mediantesafeGet(DNS fijado) con host fijo y segmentos de ruta validados.tests/liveness-core.test.mjs+tests/liveness-route.test.mjs. Conjunto: 2557.
[1.199.0] — 2026-08-15
Corregido — las tablas anchas ahora se desplazan lateralmente en vez de cortarse.
Corregido
- En la página Scan (y en el resto de tablas — Seguimiento, Estadísticas, Uso, Panel) una tabla más ancha que la ventana quedaba recortada sin barra de desplazamiento, dejando inaccesibles las últimas columnas. Ahora las tablas anchas muestran una barra de desplazamiento horizontal cuando hace falta, así toda columna sigue siendo accesible en cualquier ancho.
Notas
.table-wrapenpublic/css/components.csspasó deoverflow: hiddenaoverflow-x: auto(replica el contenedor.reports-scroll); se conserva el borde redondeado.tests/table-wrap-scroll.test.mjs. Conjunto: 2540.
[1.198.0] — 2026-08-15
Añadido — los reintentos de escaneo ahora usan retroceso exponencial, jitter y respetan el Retry-After de un limitador de tasa.
Añadido
- Cuando un tablero de empleo limita la tasa o falla brevemente (HTTP 429 / 5xx) durante un escaneo, el reintento ahora espera con retroceso exponencial + jitter en lugar de un retardo corto fijo — así un tablero ocupado no recibe golpes al mismo ritmo y los reintentos concurrentes no vuelven a colisionar al unísono. Un
Retry-Afterdel tablero se respeta (pero acotado, para que unRetry-After: 86400hostil no bloquee todo el escaneo). Los errores permanentes (404, redirecciones rechazadas) siguen fallando de inmediato — sin cambios.
Notas
- Nuevos
parseRetryAfterMs()y el purocomputeRetryDelayMs()enserver/lib/http-json.mjs;fetchJsonahora captura.retryAfteren una respuesta no-ok yfetchJsonWithRetryacepta unmaxDelayMsopcional (por defecto 8000).tests/http-json.test.mjs(+9). Conjunto: 2536.
[1.197.0] — 2026-08-14
Añadido — sigue un tablero de empleo de un fondo Getro solo con su careers_url; el id de la colección se resuelve solo.
Añadido
- Un tablero Getro seguido (b2venture, Earlybird, Point Nine, …) ya no necesita un
getro_collectionnumérico buscado a mano. Dale el propiocareers_urldel tablero y el id se resuelve solo desde esa página en el primer escaneo — un único GET seguro contra SSRF lee elnetwork.idnumérico directamente de los datos incrustados de la página. Ungetro_collectionexplícito sigue teniendo prioridad y evita la descarga por completo.
Notas
- Nuevos
httpsCareersUrl(),extractCollectionId()y el asíncronoresolveCollectionId()enserver/lib/sources/getro.mjs; la página del tablero se descarga a través desafeGet(DNS fijado, tamaño acotado), y el id resuelto sigue anclado al hostapi.getro.comporassertGetroUrl. El adaptador ahora coincide con una entradaprovider: getroque lleva uncareers_urlhttps aunque no tenga id.tests/sources-getro.test.mjs(+8). Conjunto: 2527.
[1.196.0] — 2026-08-14
Corregido (seguridad) — el adaptador de Workday valida un endpoint api por su hostname, no por una subcadena.
Corregido
- Un valor
api:de Workday enportals.ymlahora se acepta solo cuando su hostname esmyworkdayjobs.com(o un subdominio.myworkdayjobs.com). La comprobación anterior era una coincidencia de subcadena, así que cualquier URL que solo contuviera la cadena — p. ej.https://example.com/?x=myworkdayjobs.com— pasaba y se habría usado como endpoint. Los endpoints reales de Workday no se ven afectados. (Reportado por CodeQL, #443.)
Notas
- Nuevo
isWorkdayApi()analiza la URL y comprueba el host enserver/lib/portals/adapters/workday.mjs.tests/workday-adapter-endpoint.test.mjs(+1). Suite: 2522.
[1.195.0] — 2026-08-14
Rendimiento (escáner) — la detección de reposts sigue siendo rápida en historiales de escaneo grandes.
Rendimiento
- La detección de publicaciones duplicadas ya no degrada a O(N²) en un
scan-history.tsvgrande. El agrupamiento de títulos por empresa era un bucle anidado que pagaba unroleFuzzyMatchcompleto en cada par; ahora es un índice invertido — agrupa filas por título exacto en una pasada, luego hace coincidencia difusa solo sobre buckets DISTINTOS que comparten un token discriminante (no base). La salida es idéntica — los mismos clústeres de repost — probado con un test diferencial contra el algoritmo anterior en 200+ historiales aleatorios.
Notas
groupRowsByTitleenserver/lib/detect-reposts.mjs(exportado para el test diferencial).tests/detect-reposts-grouping.test.mjs(+2). Suite: 2521.
[1.194.0] — 2026-08-14
Corregido (escáner) — las páginas de empleo de Workday con una URL de un solo segmento ahora se escanean correctamente.
Corregido
- El adaptador de Workday ahora analiza las URLs de empleo cuyo path es un solo segmento — p. ej.
https://parsons.wd5.myworkdayjobs.com/Search,.../KBR_Careers,.../Careers. Antes, el sitio caía aExternal, así que el adaptador golpeaba el endpoint CXS equivocado y una prueba podía parecer sana sin devolver nada. Ahora toma el primer segmento no vacío del path como sitio (descartando un prefijo de idioma comoen-US); el caso documentado/en-US/Externalno cambia. (Reportado en #255.)
Notas
- Análisis estructural del path en
server/lib/portals/adapters/workday.mjs.tests/workday-adapter-endpoint.test.mjs(+7). Suite: 2519.
[1.193.0] — 2026-08-14
Añadido (estadísticas) — una pestaña “Silencio tras la entrevista” que muestra entrevistas que merecen un recordatorio.
Añadido
- Una pestaña Silencio tras la entrevista en
#/stats: entrevistas que quedaron en silencio más allá de una ventana de cortesía (30 días por defecto), uniendo tus entrevistas activas y tu tracker — con cuánto lleva cada una en silencio, la fecha de la última entrevista y el motivo. Una lista suave de recordatorio/cierre; solo sugerencias, nunca afirma un rechazo. Sin tokens.
Notas
- Nueva ruta
GET /api/stats/rejection-latency(fail-soft{available:false}).tests/stats-rejection-latency-route.test.mjs(+2). +10 claves i18n ×17; help-hint de#/statsde 7→8 pestañas. Suite: 2510.
[1.192.0] — 2026-08-14
Añadido (cv-studio) — un control “Verifica los datos de tu CV” que caza números que nunca tuviste.
Añadido
- Una tarjeta Verifica los datos de tu CV en
#/cv-studio: pega un CV o carta adaptados y comprueba cada métrica y dato afirmado contra tu CV, perfil y two-pager reales. Obtienes un veredicto pass / warn / block más las métricas inventadas, los datos sin respaldo y las frases prohibidas / de advertencia exactas. Sin LLM; no se escribe nada.
Notas
- Nueva ruta
POST /api/cv-studio/verify-facts: escribe el texto en un archivo temporal desechable y ejecutaverify-cv-facts.mjs, confiando en el veredicto JSON aunque el script salga con 1 en un block.tests/cv-studio-verify-facts-route.test.mjs(+4). +15 claves i18n ×17. Suite: 2508.
[1.191.0] — 2026-08-14
Añadido (estadísticas) — una pestaña “Qué aprender a continuación” que ordena las habilidades a aprender primero.
Añadido
- Una pestaña Qué aprender a continuación en
#/stats: un resumen de todo el tracker — las habilidades que más a menudo hundieron una baja compatibilidad, ponderadas y por niveles Critical / High / Medium — más las que ya cubre tu CV/perfil. Solo lectura, solo sugerencias; sin tokens.
Notas
- Nueva ruta
GET /api/stats/upskill(con campo{ error }si hay pocos datos; fail-soft{available:false}).tests/stats-upskill-route.test.mjs(+3). +15 claves i18n ×17. Suite: 2504.
[1.190.0] — 2026-08-14
Añadido (tracker) — un panel “Historial de empresa” que te dice qué empresas realmente te responden.
Añadido
- Una tarjeta Historial de empresa en
#/tracker: elige una empresa y obtén evidencia de solo lectura — cómo de receptiva ha sido contigo (en silencio contigo / mixto / respondió antes) y si el mismo puesto se republica — combinando tu tracker, seguimientos e historial de escaneo. Sin tokens; nunca se llama al escáner del proyecto.
Notas
- Nueva ruta
GET /api/stats/company-history[?company=](fail-soft{available:false}).tests/stats-company-history-route.test.mjs(+3). +18 claves i18n ×17. Suite: 2501.
[1.189.0] — 2026-08-14
Corregido (escáner) — los niveles de antigüedad en números romanos ahora cuentan también en títulos no latinos.
Corregido
- El clasificador de niveles detrás de
skip_tiersahora lee un sufijo de nivel en números romanos (I / II / III / IV / V) tras la palabra del puesto en cualquier alfabeto — “Инженер III”, “エンジニア I”, “Ingénieur IV” — no solo tras palabras ASCII. Antes, un número de nivel tras una palabra no latina se ignoraba y la oferta caía a mid, así queskip_tiers: [senior]o[entry]no las filtraba.
Notas
- Lookbehind independiente del alfabeto en
server/lib/classify-tier.mjs; eliminado un matcherSr.duplicado muerto.tests/classify-tier.test.mjs(+1). Suite: 2498.
[1.188.0] — 2026-08-14
Corregido (UI) — los botones de acción principales ya no quedan pegados al subtítulo de la página.
Corregido
- La fila de acción / control principal en Resumen semanal de entrevistas, Empresas financiadas, Portales, Plan de carrera y Orientación profesional ahora tiene un margen superior adecuado, así el botón respira bajo el subtítulo en lugar de pegarse a él.
Notas
- Guardia de regresión
tests/lead-row-top-margin.test.mjs(+5). Suite: 2497.
[1.187.0] — 2026-08-14
Corregido (escáner) — la opción skip_tiers vuelve a funcionar: se descartan las publicaciones que pediste omitir por antigüedad.
Corregido
- Una lista
skip_tiers:enportals.yml(p. ej.skip_tiers: [intern, entry]) ahora se respeta en el escaneo. El título de cada publicación se clasifica en un nivel de antigüedad (intern / entry / mid / senior) y se descarta si su nivel está en tu lista. Antes el escaneo aplicaba los filtros de título / ubicación / contenido / confianza pero sin filtro de nivel, así queskip_tiersse ignoraba en silencio. Los títulos sin palabra de nivel caen en mid (asískip_tiers: [mid]también descarta la mayoría de publicaciones normales), y el clasificador lee la palabra de nivel MÁS A LA IZQUIERDA.
Notas
- Nuevo
server/lib/classify-tier.mjspuro (classifyTier+buildTierFilter), conectado a las cadenas de filtros de los escáneres EN y RU.tests/classify-tier.test.mjs(+7). Suite: 2492.
[1.186.0] — 2026-08-14
Añadido (CV Studio) — un panel “Brecha de habilidades”: cuáles de las habilidades requeridas de un empleo nombra tu CV, insinúa o le faltan.
Añadido
- Un nuevo panel Brecha de habilidades en CV Studio. Elige una descripción de empleo guardada y clasifica cada habilidad requerida en nombrada en tu CV, insinuada en tu CV o faltante — comparación de palabras sin IA, sin escribir nada. Aparece una nota de baja confianza cuando la oferta no tenía una sección clara de requisitos.
Notas
- Nuevo
GET /api/jds/:name/skill-gap(el nombre de la oferta se sanea y se confirma bajojds/antes de ser un argumento; degradación suave a{available:false}sin el script). +13 claves i18n ×17. Pruebas:tests/jds-skill-gap-route.test.mjs(+4, incl. rechazo de path-traversal). Suite: 2485.
[1.185.0] — 2026-08-14
Añadido (estadísticas) — una pestaña “Embudo y velocidad”: cómo se compara tu embudo con el mercado y con qué rapidez avanzas entre etapas.
Añadido
- Una nueva pestaña Embudo y velocidad en Estadísticas muestra tus tasas de respuesta e entrevista junto a rangos de referencia del mercado (con las advertencias de muestra pequeña y sesgo de selección), una lista de espera de postulaciones en curso pasadas la ventana típica de primera respuesta, y días medianos por etapa (Postulado → Respondido → Entrevista → Oferta), con las filas lentas censuradas por la derecha para que no sesguen las medianas. Solo lectura y sin tokens; lee solo tu propio seguimiento.
Notas
- Nuevo
GET /api/stats/funnel(degradación suave a{available:false}sin el script). +18 claves i18n ×17. Pruebas:tests/stats-funnel-route.test.mjs(+2). Suite: 2481.
[1.184.0] — 2026-08-14
Corregido (UI) — los mosaicos de acción rápida del Panel ahora se alinean en una cuadrícula pareja.
Corregido
- En el Panel (Centro de mando), un grupo de 3 mosaicos se veía más ancho que uno de 4, dejando las secciones con un borde derecho irregular. Ahora cada grupo usa columnas de igual ancho (4 en pantalla amplia, bajando a 3 / 2 / 1 al estrecharse la ventana), así todos los mosaicos son del mismo tamaño y sus bordes derechos se alinean.
Notas
- Solo CSS (
.qa-grid:repeat(N, minmax(0,1fr))fijo en vez deauto-fill). Protegido portests/dashboard-grid-align.test.mjs(+2). Suite: 2479.
[1.183.0] — 2026-08-14
Añadido (escáner) — detección de duplicados más inteligente: el mismo empleo re-publicado con un enlace de seguimiento ya no aparece dos veces.
Añadido
- El escáner ahora reconoce un puesto por una clave de URL canónica, así que el mismo empleo re-publicado con un parámetro de seguimiento (
?utm_…,gclid, …), porhttpvshttps, o con una barra final /#fragmentose trata como el único puesto que es — sin fila duplicada en tus resultados o pipeline, y sin evaluación desperdiciada en un empleo ya visto. Los puestos realmente distintos (un id funcional conservado comogh_jid) siguen contando por separado.
Notas
- Nuevo
server/lib/url-key.mjs, conectado al dedup de ambos escáneres y al escritor de pipeline. Sub-normaliza a propósito: nunca fusiona dos puestos distintos. Pruebas:tests/url-key.test.mjs(+5),tests/parsers.test.mjs(+1). Suite: 2477 (+6).
[1.182.0] — 2026-08-14
Corregido (escáner) — los rangos salariales ahora se leen igual en todos los idiomas.
Corregido
- Las cifras de salario en las filas de escaneo y seguimiento usan los símbolos neutrales ≥ y ≤ (p. ej.
≥ 120000 EUR,≤ 90000) en lugar de las palabras inglesas “from” / “up to”, que se filtraban sin traducir a interfaces no inglesas. Se aplica a cada tablero con rango de un solo lado (Getro, Remotli, Manfred, Agentic Jobs, JustJoin, Jobicy); los rangos de dos lados (100000–150000 USD) ya eran neutrales.
Notas
- Solo visualización — el filtro de salario del cliente analiza los números sin importar el prefijo, así que el filtrado no cambia. Suite: 2471.
[1.181.0] — 2026-08-14
Añadido (escáner) — los tableros de Getro ahora muestran salario, todas las ubicaciones y puestos remotos.
Añadido
- El escáner de Getro (tableros de redes de talento de fondos) ahora muestra una cifra de salario en cada puesto (rango anual + moneda), lista todas las ubicaciones en lugar de solo la primera y etiqueta los puestos remotos. Un puesto de Getro en tu escaneo y seguimiento ahora tiene el mismo detalle de salario + ubicación que los demás tableros.
Notas
- Solo escáner; sin nueva dependencia, sin cambios de ruta / CSP / SSRF. Pruebas:
tests/sources-getro.test.mjs(+5). Suite: 2470 (+5).
[1.180.0] — 2026-08-14
Corregido (MEDIO, informes) — la lista #/reports ahora es una tabla y se recupera una puntuación real que un marcador de posición de Machine Summary ocultaba.
Corregido
- La lista
#/reportses una tabla (Informe · Fecha · Legitimidad · Puntuación), no una cuadrícula de 4 tarjetas. Un chip largo de “Puntuación no detectada” reducía la columna del título casi a cero, y eloverflow-wrap: anywheredel título de la tarjeta rompía el nombre del informe carácter por carácter. Ahora cada campo tiene su propia columna, la celda del nombre se ajusta por palabras y la tabla se desplaza horizontalmente en pantallas estrechas (nuevo contenedor.reports-scroll). Nueva clave i18nrep.colReport×17. - Una puntuación real en el cuerpo (
**Итоговый балл:** 1.8 / 5) ya no queda oculta por un marcador de posición de Machine Summary (score: —). Cuando el bloque## Machine Summarytraía una puntuación no numérica o fuera de rango, ocupaba el hueco de la puntuación analizada y bloqueaba el respaldo de forma-valor en negrita, por lo que el informe mostraba “Puntuación no detectada” pese a unX / 5real en el cuerpo.parseReportHeaderahora recupera la forma-valor del cuerpo cuando no sobrevive ningún número utilizable (paso 4.5).
Notas
- Solo cliente + analizador; sin cambios de ruta / CSP / SSRF / escritura al padre. Pruebas:
tests/reports-table.test.mjs(+5),tests/report-header-locale.test.mjs(+2). Suite: 2465 (+7).
[1.179.0] — 2026-08-13
Cambiado (LOW, escáner) — se consolidaron 20 decodificadores de entidades HTML duplicados en el módulo compartido (seguimiento de paridad, cierra el worklist).
Cambiado
- 20 fuentes de escaneo tenían cada una su propio
decodeEntities/decodeXmlEntities(+ un helperfromCodePoint) — copias que habían derivado (tres podían lanzarRangeError, corregido en v1.172.0; otras admitían NUL/C0 o interpretaban mala2;). Ahora todas pasan por el únicoserver/lib/html-entities.mjs(decodificador seguro según XML 1.0 Char), eliminando ~237 líneas de duplicación. Las 8 fuentes tipo RSS ganaron la decodificación de (antes solo manejaban 5 entidades); se preserva la doble decodificación deliberada de cryptocurrencyjobs mediante un alias.hhmantiene su decodificador (maneja—/–, fuera de las 6 compartidas). Una nueva prueba de guardia falla si alguna fuente vuelve a crear un decodificador local.
Notas
- Refactor que preserva el comportamiento; sin cambios de ruta / CSP / SSRF / escritura al padre. Pruebas:
tests/decoder-consolidation.test.mjs(+2). Suite: 2458 (+2).
[1.178.0] — 2026-08-13
Corregido (LOW, paridad con el padre) — se actualizaron dos constantes obsoletas para igualar al padre (PARENT-SYNC GAP #4 + #5).
Corregido
- User-Agent del navegador (GAP #4) —
BROWSER_LIKE_USER_AGENT(que envían workable/workday/oraclecloud/a16z/eightfold para pasar filtros WAF/bot) subió de Chrome 131 a 151, igualando eluser-agent.mjsdel padre; una versión vieja es más propensa a ser bloqueada. Protegido por una pruebaChrome major ≥ 151. - FALLBACK de estados del tracker (GAP #5) — el
FALLBACKde último recurso enstates.mjs(usado solo cuando eltemplates/states.ymlen vivo no es legible — clon nuevo / raíz aislada de CI) ganó los alias de estado en turco del padre (#2615): değerlendirildi, başvuruldu, yanıt verildi, mülakat, teklif, reddedildi, iptal edildi, uygun değil, kabul edildi/işe alındı. En producción el archivo en vivo ya los proveía.
Notas
- Solo dos constantes; sin cambios de ruta / CSP / SSRF / escritura al padre. Pruebas:
tests/http-json.test.mjs(+1) +tests/states.test.mjs(+1). Suite: 2456 (+2).
[1.177.0] — 2026-08-13
Corregido (MEDIUM, escáner) — csod (Cornerstone) devolvía 0 empleos en inquilinos que protegen la API de búsqueda con cookies de sesión (parent #2769, PARENT-SYNC GAP #1).
Corregido
- Algunos inquilinos de Cornerstone fijan cookies de sesión en la página de inicio del sitio de carreras y responden
401 CSOD Unauthorizeden la API de búsqueda salvo que esas cookies vuelvan junto al token portador anónimo.sources/csod.mjsahora lee el bootstrap con un nuevo helperfetchResponse, arma una cabeceraCookiecon sus valoresSet-Cookie(cookieHeaderFrom— solo nombre=valor, semántica de jar) y la reenvía en el POST de búsqueda. Solo mismo origen (host fijado +redirect:'error'), así las cookies de sesión nunca llegan a un tercero; un inquilino sin cookies se comporta igual que antes.
Notas
- Nuevo
server/lib/http-json.mjs::fetchResponse(aditivo; fuentes existentes sin afectar). Sin cambios de ruta / CSP / SSRF / escritura al padre. Pruebas:tests/sources-parity-v1118a.test.mjs(+1). Suite: 2454 (+1).
[1.176.0] — 2026-08-13
Corregido (MEDIUM, informes) — una puntuación bajo una etiqueta en negrita que la tabla RU no lista seguía mostrando “Score not detected” (FIND-5).
Corregido
- Dos informes RU escribían la puntuación como
**Итоговый балл:** 1.8 / 5/**Скор:** 1.8 / 5— etiquetas en negrita queREPORT_LABELS.runo enumera (solo conoce “Оценка”/“Балл”), así que la puntuación quedaba sin analizar. En vez de ampliar la lista de sinónimos,parseReportHeaderahora recurre a la forma del valor: una fracción sobre la rúbrica /5 bajo CUALQUIER etiqueta en negrita. Es independiente del idioma, inmune a un encabezado (sin**, sin valor/5) y rechaza una fecha como5/5/2026(lookahead negativo en el denominador).
Notas
- Solo el analizador del servidor; sin cambios de ruta / CSP / SSRF / escritura al padre. Pruebas:
tests/report-header-locale.test.mjs(+2). Suite: 2453 (+2).
[1.175.0] — 2026-08-13
Corregido (LOW, endurecimiento) — una guardia de regresión para la descripción SEO de FIND-3 + un strip de legitimidad a prueba de nulos (seguimiento de la AI-review).
Corregido
- Guardia de paridad de la descripción SEO — el arreglo de v1.174.0 que cambió un “~55” fijo en el
meta.descde cada idioma por un marcador{adapters}derivado del registro no tenía prueba, así que podía regresar en silencio en la próxima edición de un idioma. El nuevotests/site-meta-desc-parity.test.mjs(aislado de CI) falla si alguno de los 17site/src/i18n/*.jsonpierde el marcador o vuelve a fijar un recuento, o siLanding.astrodeja de interpolarlo en las tres metadescripciones. - Strip de legitimidad a prueba de nulos —
stripEmphasisdevuelve''para una entrada nula en lugar de la cadena “undefined” (los campos se inicializan como cadena, así que es defensa en profundidad).
Notas
- Prueba + una guarda de una línea en el analizador; sin cambios de ruta / CSP / SSRF / escritura al padre. Pruebas:
tests/site-meta-desc-parity.test.mjs(+3). Suite: 2451 (+3).
[1.174.0] — 2026-08-13
Corregido (HIGH, informes) — los informes localizados mostraban “Score not detected”; la descripción SEO estaba desactualizada.
Corregido
- Análisis de puntuación (FIND-1) — un informe no inglés cuyo H1 contiene la palabra de la etiqueta de puntuación (
# Оценка вакансии: <título>) ya no confunde ese título con la puntuación.parseReportHeaderahora se ancla en la etiqueta en negrita localizada (**Оценка:** 1.5 / 5), omite las líneas de encabezado y exige la etiqueta junto a sus dos puntos — así los informes RU que decían “Score not detected” muestran su puntuación real. - Chip de legitimidad (FIND-2) — se elimina el énfasis Markdown del valor, de modo que el chip muestra “High Confidence” en vez de “** High Confidence”.
- Desbordamiento de puntuación — una línea de puntuación con texto de estado extra (“1.8, Status: Evaluated, …”) se compacta a solo la puntuación;
.score-pillgana un tope de no-ajuste/desbordamiento y la columna del título puede encogerse, así un chip de color nunca se sale de la tarjeta. - Descripción SEO (FIND-3) — las descripciones meta / OG / Twitter de cvstart.org (los 17 idiomas) fijaban “Scan ~55 job boards” mientras el cuerpo contaba el registro real (“~75”). Ahora la descripción interpola el recuento del registro, así no vuelve a desviarse.
Notas
- Analizador del servidor + render/CSS del cliente + i18n del sitio; sin cambios de ruta / CSP / SSRF / escritura al padre. Pruebas:
tests/report-header-locale.test.mjs(+4). Suite: 2448 (+4).
[1.173.0] — 2026-08-13
Añadido (LOW, configuración) — Hermes se suma al roster de CLIs de IA detectadas (paridad con career-ops).
Añadido
- La pestaña
#/config→ “Herramientas CLI de IA” ahora detecta Hermes (Nous Research), el nuevo entorno de agente compatible del proyecto padre (binariohermes).server/lib/routes/cli-detect.mjsamplía su lista fija de 10 a 11 herramientas; la detección sigue siendo un escaneo de PATH de solo lectura (nunca se ejecuta ningún binario).
Notas
- Sin cambios en i18n / rutas / CSP / SSRF / escritura al padre; el roster es una lista fija, nunca entrada. Suite: 2444 (el canario de cli-detect se actualizó de 10 a 11).
[1.172.0] — 2026-08-13
Corregido (MEDIUM, escáner) — una entidad HTML mal formada podía bloquear una fuente de escaneo (paridad con career-ops #2150).
Corregido
- Las fuentes
oraclecloud,gemydassaultdecodificaban entidades HTML numéricas con una comprobaciónNumber.isFiniteinsuficiente antes deString.fromCodePoint— una referencia por encima de0x10FFFF(p. ej.�en un feed mal formado o malicioso) lanzaba unRangeErrorno capturado y abortaba todo el análisis de esa fuente. Un módulo compartidoserver/lib/html-entities.mjs(espejo del_html-entities.mjsdel proyecto padre) ahora restringe las referencias numéricas al conjunto Char de XML 1.0 §2.2, de modo queString.fromCodePointnunca puede lanzar, y distingue hexadecimal de decimal por separado para quea2;ya no se interprete mal. Las tres fuentes lo importan.
Notas
- Sin cambios para feeds válidos; sin cambios en JS / i18n / rutas / CSP / SSRF / escritura al padre. La consolidación de las ~20 copias restantes del decodificador se rastrea en
qa/PARENT-SYNC-WORKLIST-v1.26.0.md. - Pruebas:
tests/html-entities.test.mjs(+7). Suite: 2444 (+7).
[1.171.0] — 2026-08-13
Cambiado (BAJA, sistema de diseño) — tokens de escala tipográfica + capas z-index (D-4, primer paso). Los tamaños y el apilamiento eran literales por componente.
Cambiado
- Capas z-index — se añadieron tokens
--z-*(--z-topbar…--z-skiplink) y se migró cada literal z-index a ellos. Los valores se conservan, el apilamiento es idéntico; un nuevo canario prohíbe nuevos números mágicos. - Escala tipográfica — rampa
--font-size-*(xs 11…2xl 28, base = Inter 15px); se migraron los tamaños centrales (sin cambio visual). Los valores fuera de la rampa migran de forma incremental (docs/UX-ROADMAP.md).
Notas
- Solo token CSS; sin cambios de comportamiento/JS/i18n/ruta/CSP/SSRF/escritura. Sin cambios de píxeles.
tests/design-tokens-scale.test.mjs(+3). Conjunto: 2437 (+3).
[1.170.0] — 2026-08-13
Añadido (BAJA) — indicaciones de ETA honestas en generaciones IA largas (P4-ETA). Las generaciones pesadas (plan de carrera ~40 s, orientación / mercado / networking ~30 s, two-pager ~20 s) mostraban solo “Generando…” sin indicar la duración.
Añadido
- Cada botón de generación larga muestra ahora una pista
⏱ ~Nsal lado, como la ETA de#/auto. Estilo.eta-hintcompartido + dos claves genéricas (common.eta~{n}s,common.etaTitle).
Notas
- Solo cliente; sin cambios de ruta/CSP/SSRF/escritura. +2 claves i18n ×17 (snapshot 1219 → 1221).
tests/generation-eta-hint.test.mjs(+2). Conjunto: 2434 (+2).
[1.169.0] — 2026-08-13
Añadido (BAJA) — vista previa de PDF en línea (D-5). GET /api/output/pdfs/:name forzaba Content-Disposition: attachment, así que incluso el botón “Abrir” de #/cv descargaba en vez de mostrar.
Añadido
?inline=1sirve el MISMO archivo saneado conContent-Disposition: inline, para una 👁 Vista previa en una pestaña nueva; el valor por defecto sigue siendo descarga. Sin nueva ruta; se aplican los mismos guardas de nombre.- El primer botón de la lista de PDFs en
#/cves ahora 👁 Vista previa junto a ⬇ Descargar.cv.openPdfcambiado a “Vista previa” ×17.
Notas
- Sin cambios de CSP/SSRF — mismo
sanitizePathName. Una clave i18n reformulada ×17 (snapshot 1219).tests/output-pdfs.test.mjs(+3). Conjunto: 2432 (+3).
[1.168.0] — 2026-08-13
Corregido (BAJA, a11y) — las filas de casilla cumplen ahora el mínimo de 24×24 de WCAG 2.5.8 (D-2). Las etiquetas de casilla/radio en #/scan, #/config, #/evaluate y #/cv-studio estaban en una banda de ~22 px.
Corregido
- Una regla acotada
label:has(> input[type="checkbox"/"radio"]) { min-height: 24px }garantiza una banda ≥24 px. Solomin-height— las etiquetas ya son flex, no se desplaza nada;.apply-checklist(32 px) ya cumplía.
Notas
- Solo CSS; sin cambios de comportamiento/JS/i18n/ruta/CSP/SSRF/escritura.
tests/checkbox-target-size.test.mjs(+1). Conjunto: 2429 (+1).
[1.167.0] — 2026-08-13
Corregido (BAJA, sistema de diseño) — las superficies elevadas ahora se separan de las líneas divisorias (D-3). Los tokens --panel-2 / --surface-elev1 se resolvían como --slate, igual que las líneas --line / --border, sin separación visual.
Corregido
- Un token temático dedicado
--elevrespalda ahora las superficies elevadas (#eef1f6claro /#1e232eoscuro, distinto de--slate); las líneas siguen en--slate. Los hallazgos restantes (D-2, D-4, D-5, P4-ETA) quedan como backlog endocs/UX-ROADMAP.md.
Notas
- Solo token CSS; sin cambios de comportamiento/JS/i18n/ruta/CSP/SSRF/escritura.
tests/elevation-token.test.mjs(+2). Conjunto: 2428 (+2).
[1.166.0] — 2026-08-13
Corregido (BAJA) — la terminología de la rúbrica ahora refleja los docs canónicos. career-ops.org/docs describe “cinco dimensiones más una puntuación global holística”, pero el web-ui, cvstart.org y el wiki decían “rúbrica de seis dimensiones” (5 + 1 = 6, pero el vocabulario no coincidía).
Corregido
- Se adoptó la redacción de los docs — “cinco dimensiones más una puntuación global holística” — en README ×17, el sitio cvstart.org ×17, la guía de ayuda ×17,
docs/career-ops-canonical.mdy el wiki (Home ×17 + Features).
Notas
- Solo docs/marketing; sin cambios de código/clave i18n/ruta/CSP/SSRF/escritura.
tests/rubric-terminology.test.mjs(+2). Conjunto: 2426 (+2).
[1.165.0] — 2026-08-13
Corregido (BAJA) — el término “Two-pager” ahora es coherente en cada idioma. En árabe la barra lateral mostraba el latino “Two-pager” mientras el <h1> estaba localizado — la única cadena latina en una navegación RTL por lo demás reflejada.
Corregido
- Decisión aplicada: por idioma,
nav.twoPagerytwoPager.titlecoinciden en el término (ambos en latín o ambos localizados). Solo el árabe estaba dividido; su etiqueta de navegación ahora está localizada (“الصفحتان”). Un nuevo canario falla si algún idioma vuelve a dividirlos.
Notas
- Solo texto; sin cambios de ruta/CSP/SSRF/escritura. Un valor i18n cambiado (ar); sin nuevas claves (snapshot 1219).
tests/two-pager-term-consistency.test.mjs(+2). Conjunto: 2424 (+2).
[1.164.0] — 2026-08-13
Corregido (BAJA) — el marcador de posición de la búsqueda ya no se desborda en ningún idioma. “Find a company, role or URL…” se recortaba (nowrap) al encoger la barra; la mitad “…or URL” nunca se veía.
Corregido
top.search(×17) ahora es el corto “Buscar o pegar una URL” (≤24 caracteres en cada idioma), cabe incluso en una barra estrecha y mantiene la referencia a URL. El fallback enindex.htmlcoincide; elaria-labelconserva el detalle completo.
Notas
- Solo texto; sin cambios de ruta/CSP/SSRF/escritura. Una clave i18n reformulada ×17 (sin nuevas; snapshot 1219).
tests/search-placeholder-fit.test.mjs(+2). Conjunto: 2422 (+2).
[1.163.0] — 2026-08-13
Corregido (BAJA) — el asistente “Pregunta a los docs” ya cubre exportar un informe a PDF. Antes respondía que la guía no lo cubría, pese a existir el control 📄 Generate PDF en #/reports/:slug.
Corregido
- Se añadió un H3 “Exportar un informe a PDF” en §10 Informes en los 17 paquetes de ayuda (dónde está el botón, que el archivo va a
output/*.pdf, requiere Playwright, revisar antes de enviar). La recuperación del asistente ahora muestra la sección Informes.
Notas
- Solo docs/ayuda; sin cambios de código/ruta/CSP/SSRF/escritura. Umbral de ayuda 112 → 113 H3 (31 H2 sin cambios).
tests/help-reports-pdf-section.test.mjs(+2). Conjunto: 2420 (+2).
[1.162.0] — 2026-08-13
Corregido (MEDIA) — el “?” de ayuda ahora es un objetivo de ≥24×24 (WCAG 2.5.8). .help-hint medía 18×18 px con padding:0, por debajo del mínimo, en cada encabezado.
Corregido
- La caja de
.help-hintahora es 24×24 (el objetivo medible) mientras el anillo visible sigue en 18px mediante un::beforecentrado — el glifo y la línea base del<h1>no cambian. Estados hover/activo/foco se mueven con el anillo; margen 6→3px para conservar el espacio.
Notas
- Solo CSS; sin cambios de JS/i18n/ruta/CSP/SSRF/escritura.
tests/help-hint-target-size.test.mjs(+2). Conjunto: 2418 (+2).
[1.161.0] — 2026-08-13
Corregido (MEDIA) — #/reports muestra un chip “puntuación no detectada” en vez de espacio vacío. Tras el analizador multi-idioma de v1.159.0, un informe sin puntuación analizable mostraba un área vacía, indistinguible de un fallo.
Corregido
- La celda de puntuación ahora se ramifica: con puntuación → píldora de tono; sin puntuación → chip
.score-mutedcon “Puntuación no detectada” (×17) y una pista “Abre el informe…”. La tarjeta sigue siendo unrole="link"operable por teclado y la fecha se mantiene. - Reutiliza el token neutro existente; sin color nuevo.
Notas
- Solo cliente; sin cambios de ruta/CSP/SSRF/escritura. +2 claves i18n ×17 (snapshot 1217 → 1219). Conjunto: 2416 (+3).
[1.160.0] — 2026-08-13
Corregido (ALTA) — el texto sobre proveedores ya no contradice la promesa de 7 proveedores. #/config decía que la evaluación en vivo “usa tu clave de Anthropic o Gemini” y que la de OpenAI “no la usa el web UI”; el panel decía “Score Anthropic-first” — falso desde la cascada de 7 proveedores (v1.157.0).
Corregido
config.providerModelNote(×17): ahora dice que la ⚡ evaluación en vivo headless funciona con cualquiera de tus siete claves (Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub Models · Hermes), con orden automático y reserva. Eliminada la frase falsa sobre OpenAI.dash.quick.evaluateSub(×17): neutral respecto al proveedor (“Puntuación de ajuste 0–5”).Keys: N / 5→N / 7.
Notas
- Solo texto; sin cambios de ruta/CSP/SSRF/escritura. Sin nuevas claves i18n (snapshot 1217). Conjunto: 2413 (+3).
[1.159.0] — 2026-08-13
Corregido (ALTA) — los metadatos del informe ya no dependen del idioma. Los informes generados en un idioma distinto del inglés mostraban una franja de metadatos vacía en #/reports (sin puntuación, fecha ni legitimidad), porque parseReportHeader solo reconocía las etiquetas en negrita en inglés.
Corregido
parseReportHeaderahora analiza el bloque YAML invariante## Machine Summary(clavesscore:/legitimacy:/date:, la misma fuente que ya usaauto-pipeline): etiquetas en inglés → bloque Machine Summary → etiquetas localizadas (REPORT_LABELS, los 17 idiomas). Los informes en inglés quedan byte a byte idénticos.- Análisis numérico tolerante:
1.5/5,1.5 / 5,1,5/5,1.5 из 5,4.5 out of 5. La fecha recurre al mtime del archivo si el cuerpo no la tiene.
Notas
- Solo lectura/análisis; sin cambios de ruta, CSP, SSRF ni escritura en el padre. Sin nuevas claves i18n. Conjunto: 2410 (+8).
[1.158.0] — 2026-08-12
Corregido — dos fallos cosméticos de visualización (una «?» filtrada en el título de la pestaña y un recuento de proveedores incorrecto en la landing). Solo visual; sin cambios de comportamiento, seguridad ni flujo de datos.
Corregido
- La «?» de HelpHint ya no se filtra en
document.title. El router derivaba el título de la pestaña delh1.textContentsin procesar, así que la pestaña mostraba «Vacancy search?» en vez de «Vacancy search».router.js::focusNewViewahora clona el encabezado, elimina.help-hinty luego lee el texto; la «?» visible del encabezado no se toca. - cvstart.org mostraba «17 AI providers» en lugar de «7». El helper
sub()deFeatures.astroreescribía todos los{n}con el número de idiomas (17) antes de la sustitución por tarjeta; ahora{n}se resuelve por tarjeta (proveedores → 7, idiomas → 17).
Notas
- Sin cambios de servidor, ruta, CSP, SSRF ni claves i18n; forma de
facts.jsonsin cambios. Conjunto: 2402 pruebas (+1).
[1.157.0] — 2026-08-12
Corregido — las evaluaciones en vivo ahora se ejecutan con CUALQUIER proveedor configurado, no solo Anthropic/Gemini. Un usuario con solo OPENROUTER_API_KEY era forzado erróneamente al modo manual.
Corregido
- Causa raíz: un pin
LLM_PROVIDERsin clave (p. ej.LLM_PROVIDER=claudedeinit) llevaba a un callejón sin salida; ahora recurre al orden auto entre los proveedores configurados (enselectActiveProvider+ ambas cascadas de despacho). - El gating del cliente (
#/deep+ vistas mode-page) ahora usawindow.ProviderStatus(/api/status/providers, los 7) en vez del sondeo obsoleto de Anthropic/Gemini; textos reformulados (deep/eval × 17) + insignia «Evals en vivo» del dashboard +config.llmProviderHint.
Notas
- Sin cambios de seguridad. Conjunto: 2401 pruebas (+5).
[1.156.0] — 2026-08-12
Refactor — dividir scan.js bajo el límite de tamaño (P-16) + un arreglo de CodeQL. scan.js tenía 906 líneas; se extrajeron dos fábricas que preservan el comportamiento → 648. Completa el par de divisiones de vistas P-15/P-16.
Cambiado
- Nuevos
scan/runner.js(motor de ejecución del escaneo) yscan/filters.js(máquina de estado de filtros), vía bolsasctx/refs;scan.jsconecta ambos.
Corregido
- CodeQL
js/useless-assignment-to-local(#428) enconfig/tab-controller.js:let n = i;→let n;.
Notas
- Refactor puro, sin cambio de comportamiento; 4 tests que leen el fuente se repuntaron. Ambas vistas grandes ahora bajo 800 (P-15/P-16 hecho). Conjunto: 2396 pruebas.
[1.155.0] — 2026-08-12
Refactor — dividir config.js bajo el límite de tamaño (P-15). config.js tenía 1030 líneas (sobre el límite de 800); se extrajeron dos módulos que preservan el comportamiento, dejándolo en 783.
Cambiado
- Nuevos
config/field-specs.js(datos de campos + listas de modelos) yconfig/tab-controller.js(fábrica de la barra de pestañas);config.jslos referencia, la lógica de render no cambia.
Notas
- Refactor puro, sin cambio de comportamiento; 6 tests que leen el fuente se repuntaron.
scan.js(906) se deja como está (ya parcialmente dividido; núcleo demasiado acoplado para una división mecánica limpia). Conjunto: 2396 pruebas.
[1.154.0] — 2026-08-12
Nueva guía — “Ejecutar todo el stack en la nube.” career-ops no trae historia propia de nube/servidor, así que se añade una: una receta paso a paso para poner el pipeline padre career-ops, este visor career-ops-ui y el motor de IA (una suscripción de Claude vía Claude Code, un Hermes local, o claves de API) en un servidor pequeño siempre encendido. Llega como Ayuda §31 en los 17 idiomas, una sección del README y una página wiki.
Añadido
- Ayuda §31 “Ejecutar todo el stack en la nube” (× 17) — las tres partes, aprovisionar + instalar, elegir motor y exponer con seguridad (proxy inverso HTTPS + auth + las invariantes CSP/SSRF/XSS/sin-secretos). El bundle de ayuda crece a 31 H2 / 112 H3.
- README — una sección “Ejecuta todo el stack en la nube” (× 17) + una página Cloud-Deployment en la wiki.
Notas
- Solo docs — sin ruta, servidor ni cambio de cliente; sin nueva clave i18n. Los 4 tests de la ayuda pasan al contrato 31 H2 / 112 H3. Conjunto: 2396 pruebas (sin cambios).
[1.153.0] — 2026-08-12
El scanner de Jobvite migró al feed XML público (sync con el padre). El padre retiró la API JSON de Jobvite (ahora devuelve cero empleos); el source de web-ui usaba ese mismo endpoint muerto, así que cualquier empresa Jobvite rastreada quedaba vacía en silencio. Porta el fix del padre (#2623): ahora lee el feed XML público por inquilino, con clave companyEId.
Corregido
- El source pedía la API JSON retirada y devolvía cero empleos; ahora pide
https://app.jobvite.com/CompanyJobs/Xml.aspx?c={companyEId}y parsea el XML<result><job>…(CDATA + entidades,detail-urlsobreapply-url).
Cambiado
- Resolución de
companyEId: (1)company_eid:en el portal, (2) elc=de unapi:explícito, (3) descubrimiento por la página del board.fetchText(http-json.mjs) adjunta.location/.retryAfteral error non-ok (solo lectura, retrocompatible).
Notas
- Seguridad — dos hosts fijados (
jobs.jobvite.com,app.jobvite.com) víaassertJobviteUrl: solo https, allowlist estricta, ninguna redirección seguida. ElcompanyEIdes solo un valor?c=; recuento de sources sin cambios. - Conjunto: 2396 pruebas (+4).
[1.152.0] — 2026-08-12
Proveedor Hermes — cableado completado + actualización de docs. Una revisión de código de la integración de Hermes de v1.151.0 encontró dos fallos reales y cuatro puntos de completitud; todos corregidos aquí, y el roster de proveedores LLM de toda la app se lleva a los siete completos en todas las superficies de docs y los 17 idiomas.
Corregido
#/configno podía forzar Hermes — el desplegableLLM_PROVIDERlistaba solo seis proveedores, así que se podía definirHERMES_API_KEYpero no forzar Hermes desde la UI. Ahorahermeses la 8.ª opción, y una nueva prueba de paridad impide que el desplegable vuelva a desviarse deLLM_PROVIDERS.- Claves locales cortas se rechazaban en silencio — el umbral de 20 caracteres de
isUsableKeyestaba calibrado para claves de nube;hasHermesKeyusa ahora un umbral relajado de 8 (el ejemplo de los docs de Hermes tiene 19 caracteres).
Cambiado
- El roster de proveedores se normalizó a los siete completos en README (× 17), la ayuda en app (× 17), el dict
config.llmProviderHint(× 17) ydocs/sdd;hermesChatUrlcompleta un host sin ruta; el texto de reserva manual nombra a Hermes.
Notas
- Seguridad sin cambios — sin ruta nueva ni cambio SSRF/CSP; health/doctor gana una fila
HERMES_API_KEY. - Conjunto: 2392 pruebas (+2).
[1.151.0] — 2026-08-12
Hermes ya es un proveedor de LLM conectado (Phase 5) — el spike de alcance de Phase 5 confirmó que Hermes de Nous Research incluye un API Server compatible con OpenAI (hermes gateway → POST /v1/chat/completions), así que career-ops-ui ahora ejecuta evaluaciones en vivo a través de un Hermes local igual que OpenAI/Qwen. Define HERMES_API_KEY en Ajustes de la app y entra en el orden auto (el último). Cierra el último punto abierto del roadmap — Phase 5, Shape A.
Añadido
- Proveedor LLM Hermes (Shape A) —
runHermessobre el cliente compartidorunOpenAICompatible(server/lib/openai.mjs), en ambas cascadas (llm-dispatch.mjs+routes/llm.mjs), en la cola del orden auto + el pinLLM_PROVIDER=hermes,/api/status/providersyllm-pricing.mjs. Alcanza una base URL local configurable (por defectohttp://127.0.0.1:8642/v1) con auth Bearer — es un endpoint de proveedor CONFIGURADO (como OpenRouter/Qwen), no una URL de empleo del usuario, así que no pasa por el guard SSRF. - Campos en
#/config—HERMES_API_KEY(secreto) +HERMES_BASE_URL+HERMES_MODEL(por defectohermes-agent), con 6 nuevas claves i18n × 17 locales (snapshot 1208 → 1214).
Cambiado
- Se resuelve el spike de alcance:
docs/integrations/HERMES.md, la ayuda integrada §30 (× 17), el aviso del README (× 14), la skillhermes-bridgey el roadmap pasan de “planificado / aún sin conectar” a conectado (Shape A). Shape B (un relay de runtime de agente a medida) no fue necesario.
Notas
- Seguridad: el fetch del proveedor es un endpoint configurado, de la misma categoría que los otros proveedores compatibles con OpenAI — sin nueva superficie SSRF, sin cambios de CSP/sanitizador.
HERMES_API_KEYes unaSECRET_KEY(nunca se muestra). - Pruebas (aisladas en CI, transporte simulado):
tests/hermes-provider.test.mjs(+5); el canario “sin rama Hermes” de v1.146.0 se invierte para afirmar que SÍ está conectado; las pruebas de superficie de proveedores actualizadas al orden de 7 proveedores. - Conjunto: 2390 pruebas (+5).
[1.150.0] — 2026-08-12
Estados vacíos consistentes (pulido de Phase 4) — cada panel de “aún no hay nada” ahora se renderiza con el único estilo compartido .empty, en lugar de que algunas vistas redeclaren el aspecto en línea con un 40px mágico. Pequeña corrección de consistencia visual; los estados vacíos de #/activity, #/cv-studio, #/stats y #/usage ahora coinciden con todos los demás (relleno de 48px tokenizado + borde discontinuo).
Cambiado
#/activity,#/cv-studio,#/stats,#/usageeliminaron sustyle: { padding: '40px', textAlign: 'center', color: 'var(--foggy)' }en línea en los paneles vacíos — las tres propiedades ya las aporta la clase compartida.empty(--space-7= 48px, centrado, atenuado, borde discontinuo). Así esos cuatro se renderizan idénticos a los ~25 otros paneles.empty.- Las anulaciones legítimas por vista (
#/dashboardwidth:100%,#/pipelineborder:none) no se tocan — solo se quitaron las redeclaraciones puramente redundantes.
Notas
- Solo limpieza de uso de CSS en el cliente — sin cambios de ruta, servidor, clave i18n ni reglas CSS (la clase
.emptyno cambia); snapshot del diccionario 1208. Verificado en el navegador (el panel vacío de#/usagecalcula 48px de relleno + borde discontinuo, 0 errores de consola). - El nuevo canario
tests/empty-state-consistency.test.mjsmantiene.emptycomo única fuente de verdad. Phase 5 (proveedor Hermes) sigue bloqueada. - Conjunto: 2385 pruebas (+2:
tests/empty-state-consistency.test.mjs).
[1.149.0] — 2026-08-12
Portales movidos a Ajustes (Phase 4) — #/portals ahora vive en el grupo de navegación Setup, junto a Ajustes de la app, en lugar de bajo Sourcing. Desde v1.144.0 es una superficie de configuración (activar/desactivar empresas seguidas + una sonda de salud del ATS), no una acción de sourcing — así que es donde corresponde. Cambio solo de navegación; la página y su ruta no cambian.
Cambiado
- Elemento de navegación
#/portals→ grupo Setup (enpublic/index.html), colocado justo después de Ajustes de la app. Eliminado del grupo Sourcing (que conserva Scan / Pipeline / Auto-pipeline / Empresas financiadas). La ruta#/portals, la vista y la etiquetanav.portalsno cambian — solo se movió la posición en la barra lateral.
Notas
- Solo marcado de navegación — sin ruta, vista, clave i18n ni cambios de servidor. Verificado en el navegador (0 errores de consola); protegido por
tests/portals-nav-placement.test.mjs. - Conjunto: 2383 pruebas (+2:
tests/portals-nav-placement.test.mjs).
[1.148.0] — 2026-08-12
Filtros de búsqueda más claros (Phase 4) — el panel de filtros ahora es una cuadrícula ordenada — el panel de filtros de #/scan pasó de un flex-wrap irregular de cajas rígidas de ancho variable a una cuadrícula responsiva, y las acciones Aplicar / Restablecer ahora ocupan su propia fila separada y alineada a la derecha. Mismos filtros, mismo comportamiento — solo más fáciles de leer. Un retoque de diseño (sin parent-sync).
Cambiado
- Panel de filtros de
#/scan→ cuadrícula responsiva —.scan-filtersahora esdisplay: gridcon columnasrepeat(auto-fill, minmax(180px, 1fr))y espaciado uniforme, de modo que los 11 filtros con etiqueta se alinean en columnas ordenadas a cualquier ancho en lugar de envolverse en una fila irregular. - Acciones Aplicar / Restablecer abarcan toda la cuadrícula en su propia fila, separadas por una línea fina y alineadas a la derecha. Se eliminó el viejo truco de etiqueta oculta + el envoltorio flex interno en
scan.js.
Notas
- Solo CSS + una pequeña limpieza del DOM — cada id de filtro (
#scan-filter-*,#scan-apply) y el cableado deSR.render()no cambian, así que el flujo de Playwright queda intacto. Sin nuevas claves i18n. - Verificado en el navegador (0 errores de consola); protegido por
tests/scan-filters-grid.test.mjs. - Conjunto: 2381 pruebas (+3:
tests/scan-filters-grid.test.mjs).
[1.147.0] — 2026-08-12
Hermes & Telegram — la sección de ayuda integrada + superficie en cvstart.org (Phase 5b, parte 2) — la segunda y última parte del trabajo de documentación de Hermes: el tutorial ahora vive dentro de la propia guía de ayuda de la app, en los 17 idiomas, y el asistente de documentación integrado responde preguntas sobre Hermes a partir de ella. Sigue siendo solo documentación — la vía del proveedor LLM Hermes permanece planificada / aún sin conectar (Phase 5).
Añadido
- Ayuda integrada §30 “Hermes & Telegram” × 17 idiomas — una nueva sección de la guía (qué es Hermes + las dos formas de integración; ejecución en un servidor en la nube; Telegram vía Hermes + la regla “qué NO exponer”), accesible desde
#/help. El grounding dedocs-assistant/DocsFabla recoge automáticamente, ya que ambos leendocs/help/<lang>.md. - cvstart.org — un enlace a la guía de Hermes que apunta al documento en GitHub.
Cambiado
- Umbral del paquete de ayuda elevado 29 → 30 H2 / 105 → 108 H3 (
canonical-docs-coverage,help-ui,help-ru-config-section); §30 añade 3 H3.
Notas
- Todavía nada llama a Hermes. El nuevo canario
tests/help-hermes-section.test.mjsverifica que cada idioma incluye la §30 con sus anclas independientes del idioma (docs/integrations/HERMES.md,hermes-bridge,#/help,127.0.0.1, Telegram). El proveedor sigue bloqueado a la espera del contrato de API de Phase 5. - Esto cierra el entregable de documentación + skill de Phase 5b; la integración del proveedor (Phase 5) sigue siendo un elemento aparte y bloqueado.
- Conjunto: 2378 pruebas (+2:
tests/help-hermes-section.test.mjs).
[1.146.0] — 2026-08-12
Agente Hermes + Telegram — la guía de integración + una skill (Phase 5b, parte 1) — puedes ejecutar career-ops-ui en un servidor en la nube y conectar sus eventos (un escaneo terminado, un informe nuevo, un seguimiento urgente) a Telegram a través de un agente Hermes de Nous Research. Esta versión entrega la documentación de diseño + despliegue y una skill hermes-bridge; la vía del proveedor LLM Hermes sigue siendo planificada / aún no conectada (bloqueada por el spike de contrato de API de la Phase 5). Documentación por delante del código, a propósito.
Añadido
docs/integrations/HERMES.md— el análisis a fondo: las dos formas de integración (endpoint compatible con OpenAI vs. runtime de agente), el despliegue en servidor en la nube (reverse proxy + HTTPS + systemd, el contrato de solo lectura con el parent en una máquina headless), Telegram vía Hermes, y una lista de modelo de amenazas «qué NO exponer» (sin CV / salario / cuerpos de informes / claves hacia el canal).## Hermes agent + Telegramcomo aviso en el README — un enlace breve con puntero, en el README en inglés y reflejado en los README traducidos de cada idioma.- Una skill
hermes-bridge(.claude/skills/hermes-bridge/) que operacionaliza la guía — comprobaciones de prerrequisitos y de alcance (Node ≥ 18, claves presentes, alcanzabilidad del endpoint por la vía segura contra SSRF), nunca escribe secretos en disco/logs, y se niega a inventar un endpoint de Hermes o afirmar que el proveedor está conectado. - Una sección Integrations en
docs/architecture/OVERVIEW.mdenlaza la guía.
Notas
- Nada llama a Hermes todavía. Una prueba canario (
tests/hermes-docs.test.mjs) verifica las marcas de honestidad «planificado / aún no conectado» y quellm-dispatch.mjsno tiene ninguna rama Hermes/Nous — así que conectar el proveedor más adelante deberá actualizar la documentación + el roadmap en el mismo cambio. - Aplazado a v1.147.0 (Phase 5b, parte 2): la sección H2 «Hermes & Telegram» de la ayuda integrada × 17 idiomas y la superficie de marketing de cvstart.org.
- Suite: 2376 pruebas (+4:
tests/hermes-docs.test.mjs).
[1.145.0] — 2026-08-12
Estadísticas útiles (cont.): un gráfico reconstruible — la pestaña “Tendencia de roles objetivo” en #/stats ahora tiene un widget Crear un gráfico: elige una métrica × dimensión y se recompone en vivo. Solicitud de UX del usuario (sin parent-sync).
Añadido
- Gráfico reconstruible métrica × dimensión — elige una métrica (Vacantes / Salario mediano / Salario medio) y una dimensión (Por país / Por rol) y el gráfico de barras se recompone al instante. Las métricas salariales respetan la moneda + el interruptor por año ⇄ por mes; las vacantes son un simple conteo.
- 8 nuevas claves i18n × 17 idiomas; snapshot 1200 → 1208.
Notas
- Verificado en el navegador (0 errores de consola). Suite: 2372 pruebas (+2).
[1.144.0] — 2026-08-12
Ajustes y filtros (Fase 4, parte 1): activar/desactivar portales seguidos — ahora puedes encender o apagar una empresa vigilada desde #/portals, y el escáner lo respeta. Solicitud de UX del usuario (sin parent-sync).
Añadido
- Interruptor Activar/Desactivar por empresa en
#/portals— un clic apaga un portal (el escáner EN ya omite las empresas conenabled: false, así que un portal desactivado desaparece de todos los escaneos futuros) o vuelve a encenderlo, con un aviso optimista. POST /api/portals/toggle— una escritura explícita del usuario que cambia quirúrgicamente y con validación de parseo el flagenabledde una empresa enportals.yml(comentarios, orden y demás campos se conservan). 5 nuevas claves i18n × 17 idiomas; snapshot 1195 → 1200.
Notas
- El cambio en el escáner fue cero —
en-scanner.mjsya filtraenabled !== false. Suite: 2370 pruebas (+3).
[1.143.0] — 2026-08-12
Comprensible (cont.): pistas ? en las vistas de trabajo principales — el ? de ayuda ahora cubre las nueve páginas de acción principales, en todos los idiomas. Ajuste de UX reportado por el usuario (sin parent-sync).
Añadido
- Pista de ayuda
?en 9 títulos de vistas más —#/scan,#/evaluate,#/cv-studio,#/tracker,#/config,#/deep,#/batch,#/auto,#/applyreciben un?en línea (víaHelpHint.title) que abre un popover localizado de «qué hace / cómo usarlo / qué esperar» — el mismo componente seguro para CSP de v1.139.0. - 9 nuevas claves i18n × 17 idiomas (
help.hint.scan/…/apply); snapshot 1186 → 1195.
Notas
- Verificado en el navegador (0 errores de consola). Suite: 2365 pruebas (+1).
[1.142.0] — 2026-08-12
Corrección: se acabó el arquetipo profesional “Unknown” — #/orientation ahora siempre clasifica entre los ocho vectores de carrera nombrados en lugar de responder a veces “Unknown” y recomendarte “redoblar” en él. Corrección reportada por el usuario (sin parent-sync).
Corregido
#/orientation— el prompt de IA ahora prohíbe un arquetipo fuera del conjunto. El modelo DEBE clasificar los tres primeros entre exactamente los ocho vectores nombrados y nunca responder “Unknown”/“N/A”/“datos insuficientes” ni inventar una etiqueta. Con un CV escaso, igual nombra los tres más cercanos con menor confianza y dice qué evidencia falta, en vez de declinar.
Notas
- Solo cambio de prompt del servidor (
buildOrientationPrompt); sin cambios de i18n/esquema. Suite: 2364 pruebas (+1).
[1.141.0] — 2026-08-12
Estadísticas útiles (cont.): enriquecimiento de empresas financiadas — #/funded es ahora más visual: logos de empresa, un gráfico por importe de financiación y tarjetas con ronda / importe / puntuación de descubrimiento / acción sugerida. Ajuste de UX reportado por el usuario (sin parent-sync).
Cambiado
#/funded— tabla plana → cuadrícula de tarjetas. Cada empresa recién financiada es ahora una tarjeta con logo (derivado del nombre víaCompanyLogo, con avatar de letra como respaldo), chips de ronda + importe, la puntuación de descubrimiento y la acción sugerida del proyecto padre, más el enlace y la fecha de la noticia.- Visualización del importe de financiación — un gráfico de barras de las mayores empresas por importe divulgado; los importes de texto libre (“$120M”/“€1.5B”) se analizan a una magnitud con un nuevo
parseAmount. 3 nuevas claves i18n × 17 idiomas.
Notas
- Sigue siendo solo lectura sobre
GET /api/company-funded; descripción y rango salarial no están en la fuente de financiación. Suite: 2363 pruebas (+2).
[1.140.0] — 2026-08-12
Estadísticas útiles: cifras salariales más ricas — el desglose salarial de “Mi pipeline” en #/stats ahora muestra la media (no solo la mediana), un cambio por año ⇄ por mes y una tabla mín · media · mediana · máx por país. Primer tramo de la Fase 3. Ajuste de UX reportado por el usuario (sin parent-sync).
Añadido
- Salario medio (media) —
RoleStats.salaryStatsahora devuelveavgUsdjunto aminUsd/medianUsd/maxUsd. La mediana resiste los valores atípicos; la media revela el sesgo, así que juntas se leen como una distribución. - Cambio por año ⇄ por mes y una tabla mín · media · mediana · máx por país en la sección salarial, con los selectores de moneda y periodo. 8 nuevas claves i18n × 17 idiomas.
Notas
- Las cifras siguen derivándose solo de anuncios con salario legible y normalizadas a USD (indicativas). Suite: 2361 pruebas (+1).
[1.139.0] — 2026-08-12
Comprensible: pistas de ayuda ? — un botón ? reutilizable y seguro para CSP que, al hacer clic, explica «qué hace / cómo funciona / qué esperar» en tu idioma. Ajuste de UX reportado por el usuario (sin parent-sync).
Añadido
- Popover de pista de ayuda
?(window.HelpHint) — un?redondo junto a un encabezado abre un popover ligero, adaptado al tema y reflejado en RTL, que muestra una explicación localizada medianteUI.md(); accesible (role="tooltip",aria-expanded, cierre con Escape o clic fuera, foco restaurado) y seguro para CSP. ?en las 5 pestañas de#/statsy en 8 títulos de vistas de IA/analítica (career-plan, orientación, two-pager, networking, entrevista simulada, memoria, funded, resumen semanal) — 14 nuevas claves i18n × 17 idiomas.
Notas
- Todas las vistas ya tenían un subtítulo de una línea; el
?añade la explicación más detallada bajo demanda y hace que los estados vacíos se expliquen por sí solos. Suite: 2360 pruebas (+4).
[1.138.0] — 2026-08-12
Generación en el idioma de tu interfaz — cada generación con IA ahora responde en el idioma que has elegido en la interfaz, además de refuerzos de pruebas surgidos de la revisión. Ajuste de UX reportado por el usuario (sin parent-sync).
Cambiado
- Las generaciones con IA ahora respetan el idioma de la interfaz. Con la interfaz en ruso, español, japonés, … el texto generado vuelve en ese idioma en lugar de siempre en inglés. La directiva de idioma de salida se propaga por todos los endpoints de generación — plan de carrera, orientación, informe de mercado, entrevista simulada, plan de networking, «pregunta a la documentación», la sugerencia de nota de memoria y el borrador del two-pager. El código y los identificadores siguen en inglés (p. ej. las claves YAML del two-pager); solo se localiza la prosa, los títulos y las viñetas.
Corregido
- Guardia de rol de color CSS (
tests/css-role-tokens.test.mjs) — un canario estático que verifica que los tokens alias del modo oscuro de v1.137.0 nunca invierten su rol: los tokens de texto (--fg/--danger/--ok/…) nunca se usan comobackground, y los de superficie (--card/--panel/--line/…) nunca comocolorde texto, en todo el CSS y los estilos inline de la SPA. - Auto-sonda del cargador XSS de
UI.md()— el test que cargamd()desdeapi.jsahora pruebamd('<script>…')justo tras la extracción y lanza un error si falta el escape, de modo que un futuro corte erróneo falla ruidosamente en vez de dejar verde la suite de seguridad. - Guardia de scroll en
#/career-plan— elscrollIntoViewtras generar solo se ejecuta si la vista previa sigue conectada al documento.
Notas
docs/UX-ROADMAP.mdactualizado: las pistas de ayuda?+ descripciones de página + estados vacíos pasan a v1.139.0; un proveedor Nous Research / Hermes — con una guía de despliegue en servidor cloud + Telegram y un skill de Hermes — se registra como Fase 5 / 5b.- Suite: 2356 pruebas (+5).
[1.137.0] — 2026-08-11
Correcciones de legibilidad y renderizado — contraste en modo oscuro, etiquetas de gráficos y el plan de carrera. Una revisión de UX motivada por un reporte de usuario (sin parent-sync).
Corregido
- Modo oscuro en blanco-sobre-blanco / negro-sobre-negro en muchas pantallas — quince propiedades personalizadas de CSS que varias vistas referenciaban (
--fg,--panel,--panel-2,--ok,--danger,--card, …) nunca se habían declarado, así que recurrían a valores fijos claros/negros: correcto en modo claro, ilegible en modo oscuro (los chips de resumen de#/pipeline, la pestaña activa de#/stats, “Active / Keys” + “✓ set” de#/config, las secciones de#/two-pager, la burbuja de pregunta de#/mock-interview, el texto de error). Ahora son alias de los tokens reales que sí responden al tema, así que lo siguen automáticamente — 0 fallos de contraste WCAG-AA en las 29 vistas, verificado por un auditor automatizado; la pestaña activa de#/configpasó a un estilo de insignia con tinte legible. Una protección de regresión (tests/dark-theme-tokens.test.mjs) mantiene ese alias. - Las etiquetas de los gráficos de
#/statsse cortaban a mitad de palabra («Senior Backend Engineer» → «…Enginee») — ahora se truncan con elipsis y conservan la etiqueta completa como tooltip al pasar el cursor. #/career-planmostraba el plan generado como Markdown sin procesar — ahora se renderiza automáticamente como texto legible y con formato (el Markdown editable permanece en el área de texto; el botón Vista previa lo alterna).
Notas
#/career-plan,#/two-pager,#/statsy el resumen semanal de entrevistas no están rotos — muestran estados vacíos hasta que generas un plan o tienes datos. Se planea para la próxima entrega una guía en pantalla más clara y pistas de ayuda?(docs/UX-ROADMAP.md).
[1.136.0] — 2026-08-11
Paridad con career-ops padre v1.26.x (posterior a v1.26.0 en la rama principal) — una nueva fuente sin autenticación más una ronda de mejoras de calidad y robustez portadas a los espejos de web-ui. El registro ahora son 79 fuentes = 74 en inglés + 5 en ruso (ALL_ADAPTERS 74).
Añadido
eightfold(Eightfold AI, #2684) — boards de captación de talento vía la API sin autenticaciónhttps://<tenant>.eightfold.ai/api/apply/v2/jobs, anclada a host a*.eightfold.ai(el CNAME de marcacareers.<company>.comse rechaza deliberadamente); paginada con un límite de seguridad, lanzamiento de error ante board muerto, y deduplicación de URL. Fuente + adaptador + suite aislada para CI; aparece en el filtro de Fuente de#/scany en la landing.
Corregido
- Deduplicación y claves de rol con reconocimiento Unicode (#2569 / #2587 / #2667) — una nueva
normalizeTextKeycompartida (NFKC, conserva letras/marcas/dígitos de cualquier alfabeto) sustituye a las claves solo-ASCII:detect-repostsahora agrupa variantes de ancho/puntuación del nombre de empresa (“Acme, Inc.” ≡ “Acme Inc”) y nunca colapsa empleadores no latinos distintos, mientras querole-matcherpliega títulos en ancho completo y conserva los tokens de rol no latinos en lugar de borrarlos. fetchJsonWithRetryya no reintenta una redirección rechazada (#2657) — una protecciónredirect:'error'que encuentra un 3xx es determinista, así que ahora es no reintentable y falla rápido en lugar de agotar el presupuesto de reintentos.- Grupos AND de
title_filter.positive(#2552) — un+delimitado por espacios dentro de una entrada positiva ahora exige que todos los términos aparezcan en el título, en cualquier orden. oraclecloudacepta los apex de tenant numeradosoraclecloud1.com … oraclecloud99.com(#2683) — una familia acotada (sin cero inicial, ≤ 2 dígitos), nunca un apex comodín.workablereforzado (#2675) — reintentos, cabeceras similares a un navegador, y serialización de peticiones frente al host tras Cloudflare.personiorecurre a un scraping HTML cuando el feed XML está desactivado, en lugar de devolver nada.- Los alias FALLBACK de
statesresincronizados con el padre (#2615).
Notas
- No portado (no reflejado por web-ui, o exclusivo de CLI): reply-matcher (#2672), jd-similarity (#2661), jd-skill-gap (#2686), el parseo de scan env-path (#2568) /
--flag=value(#2589), y los cambios de cover-letter / plantilla de CV / doctor / ollama / generate-pdf. Los avisos HIGH dejs-yaml/nanoiddel lado web ya se habían corregido en web-ui v1.135.0.
[1.135.0] — 2026-08-11
Paridad con career-ops padre v1.26.0 — cinco nuevas fuentes de escaneo sin autenticación más correcciones de precisión en cuatro boards que web-ui ya incorpora. El registro ahora son 78 fuentes = 73 en inglés + 5 en ruso (ALL_ADAPTERS 73).
Añadido
- Cinco nuevas fuentes de escaneo (cada una con una fuente + adaptador + una suite aislada para CI; aparecen en el filtro de Fuente de
#/scany en la landing de cvstart.org):join(JOIN) — el board JOIN de una empresa a partir del__NEXT_DATA__de Next.js enjoin.com/companies/<slug>(anclado a host, con límite de páginas).getro(Getro) — boards de “red de talento” de portfolios de VC vía la API pública POST deapi.getro.com, paginada de más reciente a más antiguo; cada empleo se atribuye al empleador del portfolio, no al fondo.consider(Consider) — boards de portfolio de VC de getconsider.com vía un POST del mismo origen; el host configurable está anclado por una protección SSRF estructural (solo host HTTPS público).joinup(JOINUP) — el board suizo joinup.ch, que lee la página más reciente renderizada en el servidor (SSR); falla cerrado ante una rotura del scraper.remotli(Remotli) — remotli.ch, puestos remotos en empresas suizas (salarios en CHF); emite la propia URL de aplicación del ATS del empleador para que las publicaciones cruzadas se deduplican.
Corregido
- a16z Speedrun ya no aborta todo el board ante un fallo transitorio — las peticiones de página ahora pasan por un
fetchJsonWithRetrycompartido (reintentos acotados solo ante 429/5xx/timeout transitorios, nunca ante un 4xx permanente), y el presupuesto de páginas se ha redimensionado para la página de 50 empleos. - arbeitsagentur se ha migrado a la API Jobsuche v6 (
/pc/v6/jobs) — el antiguo endpoint v4 devuelve 404; la forma de la respuesta se renombró y el filtrado remoto ahora se acota en el servidor. - thehub se ha migrado a la API v2
jobsandfeatured; las filas no llevan fecha de publicación y están exentas del filtro de antigüedad. - hackernews encuentra el hilo mensual “Who is hiring?” de forma fiable filtrando la búsqueda de Algolia por la etiqueta de cuenta
author_whoishiringen lugar de una consulta de texto libre.
Notas
- No portado (web-ui ya es seguro, absorbido por el relé, o exclusivo de CLI): las claves de deduplicación de roles / coincidencia de empresas en Unicode (la agrupación de repost de web-ui ya usa como clave el nombre de la empresa en minúsculas planas, así que los empleadores distintos no latinos nunca colapsan); la señal de latencia de rechazo de followup + los ajustes de company-funded (relevados de solo lectura, con degradación segura); las rutas de escaneo configurables por entorno y el parseo
--flag=value(web-ui ejecuta los escáneres en proceso); la refactorización de consolidación del User-Agent (web-ui ya la centraliza); y los elementos exclusivos de CLI (el listado de contenido no confiable, oferta/offer-prep, doctor, cambios de plantilla de carta/CV).
[1.134.1] — 2026-08-05
Endurecimiento de validación — correcciones detectadas por una auditoría integral del proyecto.
Corregido
successfactorsya no descarta los empleos extraídos ante un fallo a mitad de escaneo (regresión introducida en el port de “lanzar error en board muerto” de v1.134.0) — su bucle de paginación no teníatry/catch, así que un fallo en la página 2 o posterior (tras el éxito de la página 1) lanzaba un error y descartaba todo lo ya recolectado; y si ese fallo era un404(unstartrowfuera de rango),en-scannerponía en cuarentena durante días a un tenant en realidad activo, como si estuviera muerto. Ahora imita aphenom/radancy: un fallo en la página 0 sigue lanzando error (board muerto), pero un fallo en una página posterior conserva los resultados parciales.- Los chips de filtro de
#/scanya se pueden operar con teclado (WCAG 2.1.1) — los chips de faceta (y el chip “borrar”) eranspancon un manejador de clic pero sintabindexni rol, así que los usuarios de teclado y lector de pantalla no podían alcanzarlos ni activarlos. Ahora llevanrole="button",tabindex="0",aria-pressedy activación con Intro/Espacio. - Tres cadenas en inglés codificadas de forma fija ya están localizadas — el tooltip de la insignia de confianza de
#/scan, la cabecera de columna de reubicación de#/scany la cabecera de puntuación de#/dashboarderan literales sueltos que la puerta de paridad i18n no podía detectar (nunca fueron claves), así que permanecían en inglés en todos los idiomas que no fueran el inglés. Ahora sonscan.trustTip+scan.col.reloc(2 claves nuevas) más una reutilización detrack.col.score, con una comprobación estática de código fuente.
[1.134.0] — 2026-08-05
Paridad con career-ops padre v1.25.0.
Añadido
- Nueva fuente de escaneo: getManfred (
manfred) — un feed de todo el board de puestos tecnológicos españoles/UE con salarios publicados, desdewww.getmanfred.com/api/v2/public/offers(sin autenticación, anclado a host + solo HTTPS, catálogo completo en una sola petición). Fuente + adaptador + una suite aislada para CI (tests/sources-manfred.test.mjs); el registro ahora son 73 fuentes = 68 en inglés + 5 en ruso (ALL_ADAPTERS68). Aparece en el filtro de Fuente de#/scany en la landing de cvstart.org.
Corregido
- El feed de a16z Speedrun truncaba silenciosamente a 50 empleos (#2404) — el feed limita una página a 50, pero la fuente solicitaba
PER_PAGE = 100, así que la paginación se detenía tras la primera página. Corregido a 50. - Los boards muertos ahora lanzan un error en vez de leerse como “en vivo pero vacíos” (#2379) —
cryptocurrencyjobs,phenom,radancy,successfactors: un fallo de descarga en el que ninguna petición llega a resolverse ahora lanza un error (de modo que la salud de#/portalsy el escaneo registran un fallo real), en vez de silenciarlo como una lista vacía; un fallo a mitad de escaneo tras al menos un éxito conserva los resultados parciales. - workable ahora usa la API pública de widget (#5ab8425) — cambiado a
apply.workable.com/api/v1/widget/accounts/<slug>, que devuelve el listado completo de publicaciones de una cuenta grande en una sola petición, de modo que las cuentas grandes ya no se truncan.
Notas
- No portado (exclusivo de CLI o no reflejado en web-ui): la reescritura de rendimiento por agrupación de títulos de detect-reposts #2389; las correcciones de clave de empresa Unicode (la deduplicación del tracker propio de web-ui ya es segura para no-latinos);
scan --since;cv-facts; la pasada de auditoría de plantilla de CV / PDF;doctor; la directiva de contenido no confiable de los modos.
[1.133.1] — 2026-08-02
Corregido
#/funded(Empresas financiadas) ahora renderiza resultados — dos errores hacían que la tabla mostrara siempre “no hay empresas financiadas” incluso cuando elcompany-funded.mjsdel padre devolvía una lista completa. (1) La vista leía los resultados bajores.candidates, pero el padre los emite bajocompanies(cada una{ company, amount, round, funding: { sources: [{ source, url, observed_date }] } }); el cliente ahora lee la clave correcta y mapea la forma real de la evidencia. (2) La tabla de resultados pasaba sus celdas aUI.el('tr', {}, …)como argumentos variádicos, peroUI.el(tag, attrs, children)esperachildrencomo un único nodo o un array, así que solo se renderizaba la primera columna (Empresa) — las celdas ahora se pasan como un array. Verificado en un navegador real: 11 empresas de los cuatro feeds se renderizan con las columnas Empresa / Señal de financiación / Fuente / Fecha y enlaces de evidencia funcionales, cero errores de consola. Un resultado vacío ahora también muestra el diagnóstico por fuente, de modo que un día tranquilo de noticias se distingue de un feed bloqueado.- Guardas de regresión en
tests/parity-routes-v1133.test.mjs: el script simulado del padre ahora emite la forma real de salidacompanies(el fixture original reflejaba la forma incorrectacandidates— la razón exacta por la que el error se publicó con la suite en verde), más comprobaciones estáticas de quefunded.jsleeres.companies(nuncares.candidates) y construye las filas de la tabla con hijos en forma de array (+1 → 2144).
[1.133.0] — 2026-08-01
Añadido
- Descubrimiento de empresas financiadas (
#/funded, paridad con el padre #2117) — una nueva vista de solo lectura que retransmite elcompany-funded.mjsdel career-ops padre víaGET /api/company-funded: una lista de revisión previa de empresas recientemente financiadas, descubiertas a partir de feeds de financiación públicos y anclados a su host (TechCrunch, PR Newswire, The Guardian, Hacker News). El relé ejecuta el script con--json --dry-run(JSON a stdout, sin escrituras en disco), nunca hila la entrada del usuario hacia--sources, lleva limitación de tasa, y se activa por el usuario (un botón Descubrir, nunca al montar). Nuevo módulo de rutaserver/lib/routes/funded.mjs+public/js/views/funded.js, bajo Búsqueda. - Resumen semanal de entrevistas (
#/interview-digest, paridad con el padre #2129/#2130) — una nueva vista de solo lectura que retransmite elweekly-digest.mjsdel padre, de cero-LLM, víaGET /api/interview/weekly-digest: un resumen mecánico de las notas de sesión de entrevista — con qué empresas y rondas entrevistaste esta semana, competencias recurrentes, y brechas abiertas (best-effort). El rango opcional?from=&to=solo se hila cuando AMBOS sonYYYY-MM-DDválidos; un rango vacío es un resumenavailable:trueválido. Añadido aserver/lib/routes/interview.mjs+public/js/views/interview-digest.js, bajo Analítica. - Ambos relés siguen el contrato establecido de degradación segura
available:falsecuando el script del padre está ausente (CI, instalaciones independientes). 26 nuevas claves i18n ×17 idiomas; suite aislada para CItests/parity-routes-v1133.test.mjs(+5 → 2143).
Notas
- El career-ops padre avanzó más allá de v1.24.0 con la página Follow-up Tracker de la app Next.js
web/(#1422) y el renderizado de PDF en el backend (#2182) — no portado: web-ui ya tiene su propio relé de seguimiento y sus propios runners de PDF, y el endurecimiento subyacente defollowup-cadence.mjsllega gratis vía el relé shell-out. Los cambios deset-status.mjs/tracker-utils.mjsson internos de CLI y no se reflejan.
[1.132.0] — 2026-07-31
Cambiado
- El subsistema de renderizado de resultados de
#/scanse extrae apublic/js/lib/scan-results.js(amortización de la deuda del contrato de tamaño de archivo —public/js/views/scan.jshabía crecido hasta ~1254 LOC). El subsistema —renderResults,buildChipRow, los constructores de fila/faceta, los pintores de opciones y el espejo del registroFALLBACK_SOURCES— pasa a una fábricawindow.ScanResults.create(ctx)que encierra un objeto de contexto que le proporciona la vista. Sin cambio de comportamiento — las funciones se movieron literalmente y sus variables de cierre se recablearon actx.*;scan.jsahora tiene ~906 LOC (se prevé una segunda pasada de extracción hacia el objetivo de 800 LOC). - Nueva puerta de regresión en el navegador —
tests/playwright-scan-filters.mjssiembra undata/last-scan.jsonprefabricado y ejercita cada filtro de#/scan, verificando recuentos de fila exactos, de modo que la extracción queda comprobada contra el comportamiento real del navegador. - Banner del README simplificado — el largo muro narrativo de “Última versión” por versión se retira en favor de un resumen de una línea + un enlace al registro de cambios completo (este archivo).
[1.131.2] — 2026-07-31
Cambiado
- División de
app.cssen tres hojas de estilo ordenadas (deuda de contrato de tamaño de archivo — el archivo único había crecido hasta ~1990 LOC, muy por encima del límite estricto de 800 LOC). Ahora esapp.css(~672 — accesibilidad, tokens de diseño/tema, barra lateral, contenido principal, botones, content-shell),components.css(~595 — tarjetas, grids, paginador, badges, tablas, formularios, log/consola, markdown, selector de idioma, filtro de chips, banner de conexión) yoverlays.css(~737 — toast, cajón de notificaciones, modal, misceláneo/responsive, el espejo[dir="rtl"], docs-fab, usage-hud), cada una dentro del límite estricto.- El corte es contiguo y en orden, de modo que la cascada es byte a byte idéntica al archivo previo a la división;
index.htmlcarga las tres como<link>ordenados. Sin cambio de comportamiento, marcado ni i18n. - Los tests que verifican CSS ahora leen la concatenación mediante un helper compartido
tests/helpers/css.mjs::loadAppCss(). El nuevotests/css-modularization.test.mjsfija la división (los archivos existen · cada uno ≤ 800 LOC · el orden de enlaces de index.html) → suite 2138. Verificado en el navegador: las tres hojas de estilo se parsean y sus reglas se aplican.
- El corte es contiguo y en orden, de modo que la cascada es byte a byte idéntica al archivo previo a la división;
[1.131.1] — 2026-07-31
Corregido
- Consistencia del anclaje de host de los adaptadores en las dos fuentes de v1.130.0 (seguimientos de la revisión de código, defensa en profundidad; sin cambio de comportamiento para entradas válidas):
- El adaptador
a16z-speedrun-talentahora revalida la anulaciónapi:/a16z-speedrun-talent:enbuildEndpoint(HTTPS + host exactospeedrun-talent-network.com) y recurre al feed canónico cuando falla — paridad con el adaptadorcryptocurrencyjobs, de modo que un valor de host distinto nunca llega a la ranura de descarga (antes dependía únicamente de la comprobaciónassertSpeedrunUrlen el momento de la descarga). La comprobación de host exacto es ahora una únicaSPEEDRUN_TALENT_HOST_REexportada, compartida por la comprobación y el adaptador. - El analizador de
cryptocurrencyjobs—cleanUrlahora usa la misma comprobación de host por coincidencia exacta queassertCryptocurrencyJobsUrly la anulación del adaptador (antes eraendsWith, que aceptaba subdominios). El analizador nunca es más permisivo que la comprobación SSRF: un enlace de elementosub.cryptocurrencyjobs.cose descarta. - +2 tests → suite 2135.
- El adaptador
[1.131.0] — 2026-07-31
Añadido
- Tablero de pestañas de etapa CRM en
#/tracker(portado desde la vista/pipelinede la app web del padre). La barra de chips de embudo y el desplegable de estado del tracker se sustituyen por una franja de pestañas de etapa: una pestaña All más una por cada estado canónico —Evaluated · Applied · Responded · Interview · Offer · Rejected · Discarded · SKIP · Hired— cada una mostrando un recuento en vivo de todo el historial, incluidas las etapas con recuento cero para que el embudo completo sea siempre visible (el aspecto CRM). La pestaña activa controla el filtro; volver a pulsarla la limpia de vuelta a All. Las filas conservan su tono de puntuación, legitimidad, PDF e informe, y la celda de empresa ahora muestra un logo de marca cuando los logos están activados (desactivado por defecto → cero peticiones adicionales).- Nueva ruta de solo lectura
GET /api/tracker/stagesdevuelve el embudo canónico (etiquetas en orden) + un mapa de plegado de alias, obtenido deserver/lib/states.mjs(templates/states.yml, con el fallback integrado) — así el cliente nunca fija de forma hardcodeada la whitelist de estados. La respuesta legacy deGET /api/trackersin parámetros no cambia ({ rows }solamente). - Nueva librería de cliente pura y con tests unitarios
public/js/lib/tracker-stages.jsagrupa las filas según las etapas del servidor, tolerando negrita markdown perdida y alias localizados (p. ej.aplicado→Applied). Las pestañas son accesibles (role tablist/tab, aria-selected, área táctil ≥44 px, recuentos en el nombre accesible de cada pestaña). Sin nuevas claves i18n. Suite 2133.
- Nueva ruta de solo lectura
[1.130.0] — 2026-07-31
Añadido
- Dos nuevas fuentes de escaneo portadas del career-ops padre v1.24.0 (en proceso, sin nuevas dependencias; ambas aparecen en el filtro de Fuente de
#/scany en la landing de cvstart.org):- a16z Speedrun (
a16z-speedrun-talent, #2231) — el feed JSON de todo el board de la red de talento de a16z Speedrun. Fijado al hostspeedrun-talent-network.com, solo HTTPS, paginación 0-indexada con tope de páginas, hilo deq/config por empresa, fail-soft. - Cryptocurrency Jobs (
cryptocurrencyjobs) — la bolsa de empleo Web3cryptocurrencyjobs.co, ingerida vía su feed público RSS 2.0 (sin autenticación). Decodificación de entidades XML en dos pasadas, ofertas solo remotas, empleador extraído de la cola del título"… at <Company>". - El registro total ahora es 72 fuentes = 67 en inglés + 5 en ruso (
ALL_ADAPTERS= 67 adaptadores de portales en inglés).
- a16z Speedrun (
Corregido
echojobs— los puestos híbridos siguen siendo distinguibles del remoto (refleja el #2258 del padre). Un marcadorhybridinsensible a mayúsculas ahora produce"<Ciudad> · Híbrido"(o un simpleHíbridocuando no hay ciudad) yworkplaceType: 'Hybrid', en vez de colapsarse enRemote.radancy— marcado legacy de TalentBrew + transporte de fragmento de resultados JSON (refleja el a3e6df9 del padre), condicionado a unopts.fetchJsoninyectable.
Notas
- No portado — funcionalidades del padre exclusivas de CLI. La amplia superficie CLI/modos de career-ops v1.24.0 se mantiene fuera de web-ui, que es un visor + escritura ligera, no un host de modos: las tablas de cumplimiento/jurisdicción, la agenda de contactos + vCard, el debrief de transcripción de entrevistas / detección de plataforma de llamada, el cambio de estado del ledger, el registro de resultados, el triaje en dos pasadas, jd-similarity, el esquema versionado de artefacto de CV por postulación, la detección doctor de Playwright-MCP, y
portals/fix-slugs.mjs. Los cambios de orquestación de escaneo que viven en elscan.mjsdel padre —el escáner Playwright de Interamt.de, el barrido completo reverse-ATS de iCIMS, el filtro de elegibilidad por país remoto, el espaciado de búsquedas DNS, la deduplicaciónrltrde StepStone, y la columna de empresa normalizada en el historial de escaneo— no aplican: web-ui ejecuta los escáneres EN/RU en proceso y no invocascan.mjs. - Ya cubierto. La corrección de plegado de acentos de
role-matcher(#2209) se portó en v1.127.0, así que aquí es un no-op.
[1.129.1] — 2026-07-29
Corregido
- Seguimientos de la revisión de IA sobre los ports web de v1.128/v1.129 (todos consultivos, corregidos en origen): precedencia de nivel en
job-facets.js(un modificador explícito ahora gana a una palabra de gestión —Senior Engineering Manager→senior, anteslead); el fallback destates.mjsya no se fija (una lectura correcta se memoiza, el fallback se devuelve sin caché, así un padre momentáneamente no disponible al arranque se relee después) y avisa (console.warn) ante un archivo presente pero malformado;score-tone.js— una fila sin puntuación es neutral (muted), no roja;domainFromName()omite slugs no-ASCII antes de/api/logo; +una comprobación de aislamiento entests/states.test.mjs. +4 tests → 2073.
[1.129.0] — 2026-07-29
Añadido
- Faceta de nivel + columna de antigüedad en
#/scan— la libreríajob-facets.jsde v1.128.0 ya está integrada en la UI de escaneo (antes solo lógica). Un nuevo desplegable Nivel clasifica el título de cada oferta en lead/staff/senior/mid/junior/becario (JobFacets.seniorityFromTitle) y se autocompleta con lo que hay en los resultados —como la faceta de País—; los títulos sin palabra de nivel siempre pasan. Se conserva en búsquedas guardadas, Restablecer y Aplicar. La tabla gana una columna Nivel (badge) y una columna Antigüedad sin tokens (hoy/Nd, deJobFacets.daysSince). 12 claves i18n ×17, +3 tests → 2069.
[1.128.0] — 2026-07-29
Añadido
- Cuatro soluciones portadas de la propia web app del padre (
../web/, Next.js), reimplementadas en JS vanilla/ESM, sin nuevas dependencias: (1)server/lib/states.mjsleetemplates/states.ymlen vivo como vocabulario canónico de estados del tracker (con fallback para CI) — elimina el re-sync manual del whitelist cada release; POST pliega alias (español/legacy) al rótulo canónico, el funnel de GET agrupa por estado canónico; (2) logos de empresa en filas con host ATS víadomainFromName()(~90 marcas→dominio) antes del avatar de letra; (3)score-tone.js— tono de puntuación de 4 niveles (≥4.2/3.8/3.0 + fallback de letra); (4)job-facets.js— facetas seniority/source/days. +21 tests.
Notas
- No portado (solo concepto): la capa de acciones agénticas del padre (
actions/registry.ts+api/assistant/route.ts) — plano para cuandodocs-fabpase de chat de ayuda a copiloto. Sin nuevas fuentes (registro 70), sin cambios i18n/help.
[1.127.0] — 2026-07-29
Añadido
- Tres nuevas fuentes de escaneo (paridad con career-ops v1.23.0) — el registro ahora trae 70 adaptadores (65 EN + 5 RU): Flowxtra (agregador global sin auth), VDAB (API por palabra clave del servicio público de empleo flamenco) e iCIMS (portales
careers-<tenant>.icims.com, distinto dejibeapply). Además Cursor vuelve al roster de CLIs (parent #2115):cli-detectahora detectacursor(10 herramientas) y el roster se restaura en help/README/config ×17.
Corregido
- agenticjobs pasó de scraping HTML a la API REST (#2167); Greenhouse recupera la ciudad del endpoint
/officescuandolocation.namees solo un modelo de trabajo (#2104); paridad de role-matcher (#1933/#2164/#2009: prefijo MTS, baseproduct, plegado de acentos, desacuerdo sub-baseline).
Notas
- No portado. El grueso de v1.23.0 es superficie CLI/dashboard sin uso en web-ui (batch-tailor, discover-ats, modos NL/PT, temas de PDF, dashboard Go, updater/doctor); los scripts relayados no requieren cambios. VERSION del padre → 1.23.0.
[1.126.1] — 2026-07-25
Corregido
- Dos puntos de deriva del roster de CLI que el resync de v1.126.0 omitió — (1) el intro de la pestaña API keys de
#/config(config.providerModelNote, i18n ×17) listaba solo 7 CLIs — ahora Antigravity y Grok Build se insertan tras OpenCode; (2) una segunda fila de tabla comparativa en la guía de ayuda (×17) y el help del sitio (construido en CI) aún decíanInside Claude Code / Codex / Cursor / Gemini CLI— el conjunto obsoleto con Cursor — ahora el roster completo. Ambos usaban separadores barra/punto medio que los patrones del barrido v1.126.0 no cubrían. Snapshot i18n regenerado; suite sigue en 1969.
[1.126.0] — 2026-07-25
Añadido
- La pestaña de herramientas AI CLI ahora detecta las 8 CLIs de primera clase de career-ops — el roster de
#/configse sincronizó condocs/SUPPORTED_CLIS.mddel padre:server/lib/routes/cli-detect.mjsgana Grok Build CLI (grok) y Kimi CLI (kimi), y Antigravity ahora prueba primero su binario canónicoagy. El escaneo de PATH de solo lectura reporta ahora 9 herramientas; sigue sin ejecutar ningún binario encontrado.
Cambiado
- Resincronización de la documentación con career-ops.org/docs — se reconcilió cada superficie de docs con las páginas vivas del padre (leídas las 31). El roster canónico de asistentes IA (help ×17 + README ×17) lista ahora las 8 CLIs de primera clase — Claude Code, Codex, OpenCode, Antigravity CLI, Grok Build CLI, Qwen Code, Kimi, GitHub Copilot CLI — más Gemini CLI (wrapper heredado). Los paquetes de ayuda mantienen su estructura de 29 H2 / 105 H3.
[1.125.4] — 2026-07-23
Cambiado
- dependencias de site (dependabot #151–#153) —
sharp0.34.5→0.35.3,svgo4.0.1→4.0.2,fast-uri(dev) 3.1.3→3.1.4 ensite/; build de Astro verde, sin impacto en SPA/servidor.
Notas
- Barrido de paridad con el padre (career-ops
37d17ec..254764a, post-v1.22.0) — nada que portar: el guard de fila errónea deset-status(#2108) es solo CLI (en web-ui las filas del tracker se seleccionan explícitamente y ninguna ruta invocaset-status.mjs), el Risk Summary de modos localizados (#2109) toca archivosmodes/<lang>/que web-ui nunca lee (solomodes/*.mdde nivel superior), la verificación de manifiesto deupdate-system(#2111) es solo del actualizador, y el resto son docs del padre (README turco, SIGNATURES ×4, SCRIPTS.md, acentos es). El VERSION del padre sigue en 1.22.0 —parentVersionsin cambios.
[1.125.3] — 2026-07-23
Corregido
- Los prompts LLM en danés e hindi respondían en inglés (reportado por un usuario) —
LOCALE_NAMESy los cinco bloques deSCAFFOLD_STRINGSenserver/lib/prompts.mjsnunca se ampliaron paradanihi, por lo queresolveLocale()caía aeny cada prompt de IA — deep research (en vivo y manual), modos, evaluación, entrevista, networking, CV Studio — perdía su directiva# Output languageen esos dos idiomas. Ahora ambos son de primera clase: directiva de idioma + andamiaje localizado. La barrera de regresión entests/locale-scaffold.test.mjsahora recorre la lista canónica de 17 locales en lugar de una lista fija de 12, y una nueva verificación estructural falla si cualquier clave de andamiaje cae al inglés — un futuro locale que omitaprompts.mjsya no puede publicarse (+12 pruebas, la suite ahora 1969).
[1.125.2] — 2026-07-22
Corregido
- Deep research con Gemini: HTTP 502 (
MALFORMED_FUNCTION_CALL) (#145, contribución de @Alien10140) — el prompt en vivo de/api/deeppedía al modelo «Use WebFetch / WebSearch» y guardar el informe en un archivo, pero los proveedores de API sin herramientas no tienen canal de tools; Gemini respondía con una llamada a función en lugar de texto, lo que aparecía como un 502 vacío.buildDeepPromptybundleProjectContextaceptan ahora un flagheadless: las ejecuciones en vivo (Anthropic/Gemini/cascada de respaldo) reciben un prompt sin herramientas que redacta el informe desde el contexto insertado, mientras que el prompt para copiar en Claude Code conserva sus instrucciones de herramientas. +1 test entests/critical-fixes.test.mjs.
Cambiado
- Modelos por defecto de Gemini actualizados más allá del obsoleto
gemini-2.0-flash(#144, contribución de @Alien10140) — el desplegable de Configuración, el fallback del servidor engemini.mjs(que discrepaba en silencio con la pista), la cadena de respaldo de OpenRouter,config.geminiModelHint×17 y la guía de ayuda ×17 nombran ahoragemini-3.6-flash. La nueva puerta anti-derivatests/gemini-default-model.test.mjs(+5 tests) fija todas las superficies al mismo literal — la suite llega a 1957 tests.
[1.125.1] — 2026-07-21
Corregido
- SuccessFactors: los tenants RMK multimarca conservan su ruta de marca (padre #2099, tras v1.22.0) — las empresas holding que gestionan varias marcas adquiridas desde una única instancia RMK compartida las distinguen mediante un segmento de ruta (
careers.nemetschek.com/Bluebeam/frente a…/Vectorworks/); el adaptador solía colapsar la URL configurada a su origen, escaneando en silencio las ofertas de la marca matriz. El endpoint ahora conserva el prefijo de marca, eliminando solo un segmento final/search/o/tile-search-results/para que nada se duplique nunca sobre sí mismo; los tenants de dominio único quedan intactos byte a byte. Nuevo helper exportadoresolveTenantBase+ 1 bloque de test portado entests/sources-successfactors.test.mjs.
[1.125.0] — 2026-07-21
Añadido
- cvstart.org: sección “Fuentes de empleo” en la landing — una nueva sección entre las capturas de pantalla y la comparativa lista las 67 fuentes de escaneo como chips pulsables (62 portales/ATS en inglés + los 5 portales rusos bajo su propio subtítulo), cada uno enlazado al sitio público de la fuente. La lista se sincroniza con el registro de adaptadores en vivo durante el build (
sync-assets.mjs→facts.sources), de modo que nunca puede desincronizarse de la app; un mapa de enlaces seleccionado enSources.astroqueda protegido por la nuevatests/site-sources.test.mjs. La navegación de cabecera ganó un ancla Fuentes; 4 nuevas claves i18n del sitio ×17. También se corrigió la listainLanguagedel JSON-LD de la landing, a la que todavía le faltabahi.
[1.124.0] — 2026-07-21
Añadido
- Cinco fuentes de escaneo (paridad con el padre v1.22.0, #1808/#1572/#2024/#2055) — Welcome to the Jungle (API JSON de todo el board), Agentic Engineering Jobs (portal de ingeniería agéntica/IA), Jobvite (ATS por tenant sin autenticación), Gem (ATS por tenant) y Alibaba Group (API JSON de carreras, patrón Meituan/Tencent). Cada una es un par fuente + adaptador anclado a su host, aislado para CI; el registro ahora ofrece 67 adaptadores (62 EN + 5 RU); el desplegable de origen de
#/scany su comprobación de desincronización quedan actualizados; cinco nuevas suitestests/sources-{wttj,agenticjobs,jobvite,gem,alibaba}.test.mjs.
Corregido
- Arbeitsagentur: remoto a nivel nacional solo cuando
homeofficetypesVOLLSTAENDIG(padre #1981) — la consultahomeoffice=nv_truetambién devuelve puestos híbridos, así que ahora el filtro de remoto confirma cada resultado contra el endpoint de detalle de la oferta en lotes pequeños y falla de forma segura (un error de consulta conserva la ciudad real de la oferta, de modo que los filtros de ubicación siguen aplicándose). - SmartRecruiters: las URL públicas de las ofertas se construían sin
/postings/(padre #2047) — los enlaces ahora llevan a la página pública de la oferta en vez de un 404, para los tenants cuyo sitio público omite el segmento.
Notas
- El padre v1.22.0 también incorporó cambios del lado del CLI que la interfaz web no invoca o ya cubre: la plantilla de CV en chino simplificado (zh-CN) + la tipografía del PDF, el modo
/expand, los ajustes de caché de prompts de los proveedores (Gemini/OpenAI/Ollama), el desglose de tokens por paso (la interfaz web tiene su propio medidor de uso), la serialización con bloqueo de escritura del tracker (la interfaz web enruta las escrituras a través dewithFileLockdesde la v1.21), los indicadoresvisa_filter+ fecha de publicación absoluta del CLI de escaneo (la interfaz web tiene su propio filtro “Publicado hace” por antigüedad), y la siembra de deduplicación de fuentes vistas (el escáner de la interfaz web mantiene su propia deduplicación de historial de escaneo).
[1.123.0] — 2026-07-17
Añadido
- Fuente de escaneo Oracle Recruiting Cloud (paridad con el padre v1.21.0, #1929) — la API REST
recruitingCEJobRequisitionssin autenticación de los portales de empleo de Oracle Fusion/ORC (JPMorgan Chase, Oracle, BNY Mellon, American Express, Honeywell…): anclada al host*.fa[.<región>][.ocs].oraclecloud.com, el número de sitio se resuelve a partir delcareers_urlde cada empresa seguida, paginación por offset con un tope duro de páginas, y cabeceras tipo navegador conscientes del WAF. El registro ahora ofrece 62 adaptadores (57 EN + 5 RU); el desplegable de origen de#/scany su comprobación de desincronización quedan actualizados; nueva suite aislada para CItests/sources-oraclecloud.test.mjs.
Corregido
- Detector de reposts: los títulos base se mantienen distintos de sus variantes con sufijo especializado (padre #1922) — «Senior Analytics Engineer» ya no se agrupa con «Senior Analytics Engineer, People Analytics»: cuando los tokens de un título son un subconjunto estricto de los del otro y el token adicional es una especialización real (no una palabra genérica), ambos se tratan como vacantes publicables por separado. Las anotaciones de repost («(Repost)», «relisted») ahora se filtran como ruido léxico. +2 aserciones en
tests/detect-reposts.test.mjs.
Notas
- El padre v1.21.0 también incorporó cambios del lado del CLI que la interfaz web no invoca o ya cubre: el aviso de reaplicación a empresas repetidas (la interfaz web tiene su propio período de enfriamiento de reaplicación desde la v1.84.0), las opciones
--format/--reportde la carta de presentación, los modos de prompt de entrevista de señal de alerta / inteligencia del panel / no-show, la persistencia de señales de confianza y de salud de portales del escaneo (la interfaz web ejecuta su propio escáner en proceso contrust-validatory la página de salud de Portales), y las extensiones de stats/salary-gap (retransmitidas de solo lectura y con fallback silencioso).
[1.122.0] — 2026-07-16
Añadido
- Hindi (हिन्दी) — el 17.º idioma — diccionario completo de la interfaz (~1110 claves), la guía de ayuda integrada completa (paridad de 29 H2 / 105 H3),
README.hi.md, un nuevoCHANGELOG.hi.md(que empieza en la v1.122.0, siguiendo el precedente de de/it/tr), las páginas de landing + Metodología/Licencia/Historial de cambios/Ayuda de cvstart.org, el selector de idioma (🇮🇳), la autodetección del idioma del navegador, y una captura de pantalla del dashboard localizada. Todas las comprobaciones de paridad ×16 ahora se ejecutan ×17: paridad del diccionario i18n + snapshot, comprobaciones de paridad H2/H3 de la ayuda, paridad del CHANGELOG,check-i18ndel sitio, y el barrido de locales de Playwright.
[1.121.0] — 2026-07-16
Añadido
- cvstart.org: páginas de Metodología, Licencia e Historial de cambios — la landing suma tres secciones nuevas en los 16 idiomas, junto al bloque de Comparativa ya existente: /methodology/ (la rúbrica de puntuación de seis dimensiones de 0,0 a 5,0, el umbral de 4,0 para postular y las reglas que nunca se rompen — un resumen localizado de career-ops.org/methodology), /license/ (el texto canónico de la licencia MIT con la referencia a NOTICE.md), y /changelog/ (este mismo archivo, renderizado por locale a partir de los 16 CHANGELOG traducidos del repositorio). Nueva entrada Metodología en la cabecera y enlaces de Recursos en el pie;
sync-assets.mjsahora sincroniza el CHANGELOG ×16 y la LICENSE dentro del sitio en tiempo de compilación, de forma que las páginas nunca puedan desincronizarse del repositorio. - Enlaces a la metodología en toda la documentación — el README (los 16 idiomas), la lista canónica del §1 de la guía de ayuda integrada (los 16 idiomas) y la wiki ahora enlazan career-ops.org/methodology (además de la FAQ y el glosario) junto a las guías ya existentes de career-ops.org/docs.
Cambiado
- Se refrescaron el banner de versión y los badges del README (1850 tests, versión v1.121.0) — el banner seguía anunciando la v1.119.5.
[1.120.0] — 2026-07-16
Añadido
- El manifiesto de CareerOps (paridad con el padre v1.20.0) — el proyecto padre publicó el Manifiesto de CareerOps (
MANIFESTO.md· career-ops.org/manifesto) y ahora lo muestra desde su README, el actualizador y el dashboard de Go. La interfaz web hace lo propio: un nuevo enlace en el pie de la barra lateral abre la página del manifiesto (nueva clave i18nfooter.manifestoen los 16 locales), la guía de ayuda integrada suma el §29 «El manifiesto de CareerOps» en los 16 idiomas, el README explica qué es el manifiesto y cómo firmarlo, y el pie de la landing de cvstart.org también lo enlaza.
Notas
- El padre v1.20.0 también corrigió la supresión de habilidades ya conocidas en el modo dirigido de
upskill, silenció dotenv para que la salida stdout descan --jsonsiga siendo parseable, y arregló la plantilla HTML del CV para que la cabecera de un puesto se mantenga junto a sus puntos — superficies del lado del CLI en las que la interfaz web no interviene; no fue necesario ningún cambio de código en web-ui.
[1.119.5] — 2026-07-13
Corregido
- El botón de idioma de la landing ya no se parte — con las banderas de v1.119.2 la etiqueta del selector en la cabecera (p. ej. «🇷🇺 Русский») podía romperse hasta en tres líneas en anchos de escritorio estrechos; la etiqueta del selector y todas las opciones del desplegable llevan ahora
whitespace-nowrap— bandera + endónimo siempre en una línea. La lista de idiomas del pie pasó de una cuadrícula rígida de dos columnas a una fila envolvente de elementos de una línea — «🇧🇷 Português (Brasil)» tampoco se parte ya a mitad de nombre.
[1.119.4] — 2026-07-13
Cambiado
- LICENSE nombra al autor — la línea de copyright ahora dice: Sergei Emelianov (Fighter90) https://sergey-cv.com and career-ops-ui contributors (texto MIT canónico intacto). Un nuevo NOTICE.md detalla el licenciamiento: quién posee el copyright, qué cubre exactamente la concesión MIT (código, docs, traducciones, la landing, la wiki), qué NO cubre (tus datos en runtime, el proyecto padre, el contenido de las bolsas de empleo, las marcas), la tabla de componentes de terceros (express/js-yaml — MIT; Astro/Tailwind — MIT; las fuentes Figtree y JetBrains Mono — SIL OFL 1.1; sharp — Apache-2.0) y una línea de atribución opcional.
[1.119.3] — 2026-07-13
Añadido
- SECURITY.md — la política de seguridad a la que apuntaba CONTRIBUTING ahora existe: versiones soportadas, flujo de reporte privado (el repo tiene habilitado el private vulnerability reporting de GitHub — pestaña Security → «Report a vulnerability»), el modelo de amenazas para una app de un solo usuario en localhost (en alcance: XSS vía ofertas hostiles / SSRF / path traversal / fuga de secretos / debilitamiento de CSP; fuera de alcance: DoS a tu propio localhost y problemas del proyecto padre) y la línea base de hardening para revisores.
[1.119.2] — 2026-07-13
Añadido
- CONTRIBUTING.md — la guía del contribuidor que la landing y el README enlazaban desde siempre ahora existe: instalación, mapa del proyecto, las reglas duras de seguridad/no-build, niveles de testing, el walkthrough de los «dos registros» para añadir una fuente de escaneo, el contrato i18n ×16, convenciones de commits/PR y el proceso de release.
- Banderas de idiomas en la landing — el selector de idiomas de cvstart.org, la cuadrícula de idiomas del pie y el banner «lee en tu idioma» ahora muestran la bandera de cada locale junto a su endónimo (el mismo set de indicadores regionales que el
<select>de idioma de la app; degrada a letras de región donde faltan los glifos de bandera). - Arreglos del pie de la landing — el enlace muerto a Discussions (la función no está habilitada en el repo) ahora apunta a la wiki del proyecto, y el pie acredita al autor: Sergei Emelianov (sergey-cv.com · LinkedIn).
[1.119.1] — 2026-07-13
Corregido
- El filtro de fuentes de
#/scanalcanzó al registro — la lista estáticaFALLBACK_SOURCEStras el desplegable Source (usada solo cuandoGET /api/scan/sourcesno responde) llevaba rezagada en silencio desde v1.87.0: faltaban 20 proveedores en el fallback offline (Amazon, Avature, SAP SuccessFactors, Get on Board, Dassault Systèmes, beesite, HigherEdJobs, JibeApply (iCIMS), softgarden, Cornerstone, Phenom, Radancy, Deutsche Bahn, EchoJobs, TKMS, Heckler & Koch, Rheinmetall, LaraJobs y los nuevos Meituan / Tencent). Sincronizada con los 61 y ahora protegida por un test de deriva que rompe la CI si la lista del cliente diverge del registro del servidor (valores Y etiquetas). +1 test (1845).
[1.119.0] — 2026-07-13
Paridad con el career-ops padre v1.19.0 + refresco de la landing cvstart.org.
Añadido
- 2 nuevos proveedores de escaneo — Meituan (
zhaopin.meituan.com) y Tencent (careers.tencent.com): las APIs JSON públicas sin autenticación de las bolsas tech chinas, detectadas por host o seleccionadas con unprovider:explícito, con búsqueda del lado servidor por palabra clave, paginación y deduplicación por URL — ahora 61 adapters (56 EN + 5 RU). +20 tests (1844). - Bloque de contribuidores en la landing — cvstart.org muestra los avatares de todas las personas que aportaron código (API
/contributorsde GitHub en tiempo de build, bots filtrados), localizado en los 16 idiomas, con enlace al grafo completo de contribuidores. - Contador de estrellas de GitHub en vivo en la landing — la insignia de la cabecera se refresca ahora del lado cliente desde la API de GitHub en cada visita (la instantánea de build queda como fallback), y una reconstrucción semanal programada de Pages mantiene frescos la instantánea y la lista de contribuidores; las llamadas a la API en CI van autenticadas con token.
Corregido
- Las peticiones Workday CXS llevan cabeceras de navegador (padre #1813) — los tenants tras Cloudflare (visto en vivo: geico) responden 500 a peticiones sin UA/
accept-language/origin/referernormales; el fetcher deriva ahora el origin y el slug del site desde la propia URL CXS. Las peticiones de Glints ganaron el mismo UA de navegador + origin/referer, ambos desde la constante compartidaBROWSER_LIKE_USER_AGENTdehttp-json.mjs.
[1.118.4] — 2026-07-10
Corregido
- Los escaneos de hh.ru devolvían 0 resultados desde una IP rusa (enlaces de subdominio regional) — desde una IP residencial rusa, hh.ru redirige la búsqueda (302) a un subdominio regional (
sochi.hh.ru,spb.hh.ru, …) y devuelve los enlaces de vacantes en ese subdominio. El parser buscaba el enlace del título con el host fijohttps://hh.ru/vacancy/y no coincidía con ninguno de los regionales, así que un escaneo plenamente funcional registraba 0 en silencio. Ahora acepta cualquier host*.hh.ru(los anuncios enadsrv.hh.ru/click?…siguen excluidos — no tienen ruta/vacancy/<id>) y canonicaliza cada URL de resultado ahttps://hh.ru/vacancy/<id>. Verificado en vivo: 17 vacantes reales se parsean desde una páginasochi.hh.ruque antes daba 0. +1 test (1824).
[1.118.3] — 2026-07-10
Corregido
- hh.ru devolvía 0 resultados en silencio (interstitial de verificación de VPN) — hh.ru ahora redirige con 302 a las redes que marca como VPN/proxy (IPs de datacenter) a un interstitial
/vpncheeck(“VPN мешает работе сайта”) que responde HTTP 200 sin ninguna tarjeta de vacante, por lo que el escaneo reportaba 0 sin error alguno. El escáner ahora detecta la redirección por la URL final de la respuesta, desactiva hh.ru durante el resto de la ejecución e imprime una pista honesta: el tráfico debe salir realmente por una IP residencial — un VPN/proxy a nivel de sistema puede seguir activo aunque el interruptor del navegador esté apagado. +1 test (1823).
[1.118.2] — 2026-07-10
Mantenimiento
- Seguimiento del landing (#118) —
site/README.mdreconciliado con Astro 7 (la mejora de seguridad de #116), import sin uso eliminado y +4 guardas ejecutables para los scripts de build del landing: la puerta de paridad i18n falla de forma demostrable con un diccionario roto ysync-assetsnunca escribe fuera desite/— suite 1822. Dos alertas de CodeQL resueltas (una corregida en el código, otra descartada como comportamiento previsto del build).
[1.118.1] — 2026-07-10
Corregido
- Escaneo de hh.ru desde fuera de Rusia — hh.ru ahora devuelve HTTP 451 (bloqueo legal regional) a las IP no rusas en sus páginas públicas de búsqueda. El escáner trata el 451 como el 403: tras el primer bloqueo hh.ru se desactiva para el resto de la pasada con un mensaje honesto en el log que apunta a una IP rusa / salida VPN, sin desperdiciar las consultas restantes ni las demás fuentes RU. Ayuda §7 actualizada en los 16 idiomas. +1 prueba (1818).
[1.118.0] — 2026-07-09
Paquete de paridad con el career-ops padre v1.18.0.
Añadido
- 9 nuevos proveedores de escaneo — Cornerstone OnDemand (
csod), Phenom (phenom), Radancy (radancy), Deutsche Bahn (deutschebahn), EchoJobs (echojobs), TKMS (tkms), Heckler & Koch (hecklerkoch), Rheinmetall (rheinmetall), LaraJobs (larajobs) — ahora 54 adaptadores. El adaptador de Lever además detecta tableros del tenant EU (jobs.eu.lever.co). - Estado
Hireden el tracker (paridad constates.ymldel padre): las ofertas aceptadas tienen su propio estado canónico, una insignia celebratoria y un banner de «trabajo conseguido» en#/tracker; el embudo y las conversiones lo cuentan como avanzado por todas las etapas. - Pestaña Histórico en
#/stats— retransmisión de solo lectura delstats.mjsdel padre (resumen histórico del tracker, tasas del embudo acumulado, totales del escáner, cobertura de portales) más observaciones de compensación desalary-gap.mjs(deseado vs anunciado vs real, por candidatura). Nuevas rutasGET /api/stats/lifetimeyGET /api/stats/salary-gap— shell-outs de cero tokens, degradación segura{available:false}sin el proyecto padre. - 28 nuevas claves i18n en los 16 idiomas; guía de ayuda §14/§26 actualizada en todos.
Pruebas
- +38 pruebas unitarias (tres suites de paridad de proveedores + rutas de relé/estado) — 1817 en total.
[1.117.2] — 2026-07-06
Arreglo de tracker vacío para los shell-outs de paridad. Los scripts del padre salen con código 1 y un JSON {error} estructurado cuando el tracker aún no tiene candidaturas; el tablero de seguimiento y la pestaña de patrones lo mostraban como «script-error». Ambas rutas ahora lo transmiten como un estado vacío sano (available:true, empty:true) y la UI muestra su mensaje honesto de «nada todavía». Verificado en vivo contra un padre real.
Nuevo: ninguno.
[1.117.1] — 2026-07-06
Endurecimiento de v1.117.0 (triaje de CodeQL). Los tres endpoints shell-out (GET /api/followup, POST /api/followup/seed, GET /api/stats/patterns) llevan ahora el limitador por IP compartido (generan un proceso hijo por petición; no-op en loopback). La extracción de texto por URL de Añadir al CV elimina etiquetas hasta punto fijo y luego borra todo </> restante — saneamiento demostrablemente completo para texto de prompt LLM. Sin cambios para entradas válidas.
Nuevo: ninguno.
[1.117.0] — 2026-07-06
Paquete de paridad con el padre — seis capacidades del career-ops padre llevadas a la UI. (1) Tablero de cadencia de seguimiento en #/followup con urgencia por candidatura (🔴/🟠/🟡/🔵) desde followup-cadence.mjs, más el botón Sembrar fechas (followup-seed.mjs --backfill). (2) Patrones de rechazo: cuarta pestaña de Estadísticas que ejecuta analyze-patterns.mjs (solo lectura) — mezcla de resultados, recomendaciones y tasa de avance por proveedor ATS. (3) Añadir al CV: una tarjeta de CV Studio convierte una URL o texto pegado en viñetas ATS basadas SOLO en esa fuente (solo sugerencias, sin escrituras; el fetch de URL está protegido contra SSRF). (4) 4 nuevos proveedores de escaneo — beesite, HigherEdJobs (RSS), JibeApply (iCIMS), softgarden — el registro ahora incluye 50 adaptadores (45 EN + 5 RU), todos en el desplegable de Scan. (5) Paso de pre-escaneo de descalificadores en el checklist de Apply. (6) Runner reconcile (/api/run/reconcile). Las rutas shell-out degradan con honestidad sin los scripts del padre.
- Nuevo módulo
server/lib/routes/followup.mjs(31.º) + nuevas rutas + 8 archivos source/adapter. Pruebas: 6 + 7 nuevas; suite 1737 → 1750. 41 claves i18n ×16. Ayuda §13/§17/§24/§26 ampliada ×16.
Nuevo: GET /api/followup, POST /api/followup/seed, GET /api/stats/patterns, POST /api/cv-studio/add-entry, POST /api/run/reconcile.
[1.116.0] — 2026-07-06
Medidor de uso rehecho + primer test end-to-end de widgets. El medidor de uso de IA (v1.114.0) queda fijado correctamente: ahora está fijado al fondo de la barra lateral izquierda (todo el ancho de la barra, con su misma superficie) y reserva abajo un espacio igual a su altura para que el menú nunca quede tapado — la navegación y el pie de versión siempre se desplazan libres por encima. Se actualiza en vivo (cada 15 s, y al enfocar la pestaña y al cambiar de ruta), y cada fila de ventana muestra ahora <tokens> · <coste estimado> real (las barras se escalan contra la ventana de 30 días) en vez de un “porcentaje” siempre al 100%. Además: una barrera typeof duradera en el importador de CV cierra en origen el falso positivo recurrente de type-confusion de CodeQL, y un nuevo test end-to-end de Playwright ejercita ambos widgets persistentes en un navegador real.
public/js/lib/usage-hud.js+app.css,server/lib/cv-import.mjs. Pruebas:tests/playwright-widgets.mjs(2 E2E) +tests/usage-hud.test.mjs(10). Ayuda §6 ampliada ×16.
Nuevo: ninguno.
[1.115.0] — 2026-07-06
Retoque de diseño (conservador, se mantiene la marca coral). Una pasada ligera de refinamiento sobre el sistema de diseño compartido — sin reestructurar, sin cambiar la paleta. Las tarjetas de métricas del panel ahora se elevan y toman un borde coral al pasar el ratón (como los mosaicos de acción rápida); las tarjetas de contenido se elevan un poco; los botones primary / dark / danger ganan una sombra en reposo y una ligera elevación al pasar el ratón; los números grandes se alinean con tabular-nums; y los controles interactivos reciben un halo coral suave tras el anillo de teclado de 2px. Todo el movimiento respeta prefers-reduced-motion, y el halo se limita a los controles — nunca un *:focus-visible global.
- Solo CSS (
public/css/app.css); sin cambios de marcado, i18n, rutas ni CSP. Pruebas:tests/design-polish-v1115.test.mjs(5). Verificado en vivo con Playwright.
Nuevo: ninguno.
[1.114.0] — 2026-07-06
Medidor de uso y coste de IA en la barra lateral (abajo a la izquierda). Una sección USO compacta ahora aparece al final de la barra lateral (una tarjeta fija abajo a la izquierda si no hay barra lateral; abajo a la derecha en RTL) en cada página. Muestra tu uso de tokens LLM en ventanas de 24h / 7d / 30d — cada una como <tokens> · <cuota%> con una barra verde (cuota del total histórico) — más un pie con el coste estimado de 24h. Los datos son el resumen de solo lectura GET /api/usage de data/llm-usage.jsonl (solo local), la misma fuente que la página #/usage; el coste es una estimación y las ejecuciones en modo manual son gratis y no se cuentan. Plegable — la cabecera alterna y el estado se conserva.
- Nuevo widget cliente
public/js/lib/usage-hud.jscargado desdeindex.html, montado en la barra lateral sobre el pie de versión (alternativa de esquina fija). Seguro para CSP; con tema y espejo RTL. Sin nueva ruta de servidor. Pruebas:tests/usage-hud.test.mjs(8). 3 nuevas claves i18n ×16.
Nuevo: ninguno.
[1.113.0] — 2026-07-06
Asistente flotante “Preguntar a la guía” en cada página. Un botón de chat con un robot y degradado ahora flota en la esquina inferior derecha (inferior izquierda en RTL) de cada página. Púlsalo para abrir un chat compacto que responde preguntas de uso basándose SOLO en la guía de ayuda en tu idioma — el mismo endpoint que la página #/docs-assistant (POST /api/docs-assistant/ask), así que nunca lee tu CV, perfil ni seguimiento. En vivo con una clave LLM; sin clave → un prompt listo para usar. La cabecera muestra un avatar de robot + estado en línea; los chips inician preguntas comunes; Escape o clic fuera lo cierra; se oculta en la página #/docs-assistant.
- Nuevo widget cliente
public/js/lib/docs-fab.jsmontado globalmente desdeindex.html; seguro para CSP; estilos con tema y espejo RTL enapp.css. Sin nueva ruta de servidor. Pruebas:tests/docs-fab.test.mjs(8). 6 nuevas claves i18n ×16. Ayuda §1 ampliada en su sitio.
Nuevo: ninguno.
[1.112.0] — 2026-07-06
Consolidación de docs y QA. Sin cambios de código visibles. El documento de convenciones SDD (docs/sdd/CONVENTIONS.md) se actualiza a los 30 módulos de ruta actuales (antes 24) y a la base de pruebas actual; el prompt QA definitivo de todo el proyecto (qa/QA-REGRESSION-PROMPT.md) se consolida — mecánica de release actualizada (v1.111, parentVersion 1.17.0, publicación disparada por el release), la tabla de adiciones §14 corregida (Excluir de Scan reetiquetado v1.109.0) y ampliada con el cierre de CodeQL de v1.111 — para valer por sí solo como único prompt de regresión de toda la funcionalidad. Añade una prueba de cobertura para la rama de carga sobredimensionada.
Nuevo: ninguno.
[1.111.0] — 2026-07-06
Seguridad — cierre del backlog de CodeQL. Tres refuerzos de defensa en profundidad que cierran los hallazgos de análisis estático restantes en el origen en lugar de descartarlos. stripDangerousMarkdown ahora escapa el < de cualquier apertura de etiqueta peligrosa truncada (una carga que termina en <script/<iframe/…), de modo que su salida no contiene ninguna etiqueta peligrosa viva. La importación de CV lee el tamaño del búfer subido mediante una coerción explícita con Number() — una barrera contra la confusión de tipos. Las líneas de rol de los modos ahora son cadenas de plantilla interpoladas con String.replace en vez de funciones almacenadas, eliminando por completo la llamada de despacho dinámico. Sin cambios visibles para el usuario.
server/lib/security.mjs,server/lib/cv-import.mjs,server/lib/prompts.mjs. Pruebas:tests/security-hardening-v1111.test.mjs(7) + prueba de guarda v1108 actualizada. Sin cambios de i18n/ayuda/rutas.
Nuevo: ninguno.
[1.110.0] — 2026-07-06
Actualización de docs y QA (todos los idiomas). Sin cambios de código. El prompt de QA de todo el proyecto se actualiza a v1.109.0 con un nuevo §14 (v1.98→v1.109), y los prompts perennes de UX-audit y design-export ganan la superficie actual. Cada párrafo de ayuda añadido en v1.100–v1.109 está ahora traducido a los 16 idiomas.
Nuevo: ninguno.
[1.109.0] — 2026-07-06
Filtro Excluir en Scan + resumen del pipeline (paridad de diseño con la web principal). En #/scan, el cuadro Buscar trata las comas como O (“roles a encontrar”) y un nuevo campo Excluir oculta cualquier fila cuya empresa/rol/ubicación contenga alguna palabra separada por comas (p. ej. senior, staff); ambos se recuerdan en tus búsquedas guardadas. En #/pipeline, una tira de resumen compacta muestra tu pipeline de un vistazo — N en la bandeja, N en seguimiento y los recuentos de Applied / Responded / Interview / Offer del tracker, cada chip enlaza a #/tracker.
- Solo cliente (sin nueva ruta/escrituras).
public/js/views/scan.js+public/js/views/pipeline.js. Pruebas:tests/scan-pipeline-ui-v1109.test.mjs(2). 4 nuevas claves i18n ×16. Ayuda §7 + §8 ampliadas en su sitio.
Nuevo: ninguno.
[1.108.0] — 2026-07-06
Refuerzo de seguridad (triaje de CodeQL, ronda 2). Tres hallazgos más de baja severidad corregidos: el constructor de prompts resuelve la línea de rol del idioma por clave propia + typeof === function para que un idioma manipulado no despache a un método de prototipo (unvalidated-dynamic-method-call); el slug del nombre de archivo PDF se limita a 200 caracteres antes del regex para que una entrada de solo guiones no retroceda (ReDoS polinómico); y la importación de documentos coacciona un filename de tipo array (encabezado repetido) a cadena (type-confusion). Sin cambios de comportamiento para entrada válida.
server/lib/prompts.mjs,server/lib/routes/runners.mjs,server/lib/cv-import.mjs+tests/security-hardening-v1108.test.mjs(3). En v1.106–v1.108 el backlog de análisis estático pasó de 167 a ~14, con cada hallazgo realmente relevante para la seguridad corregido y el resto (falsos positivos protegidos/saneados + lint de nivel nota) descartado con justificación.
Nuevo: ninguno.
[1.107.0] — 2026-07-06
Refuerzo del saneador (defensa en profundidad XSS en reposo). stripDangerousMarkdown — que neutraliza HTML peligroso en el markdown de CV/oferta almacenado para que cualquier consumidor que evite el cliente con escape-al-renderizar siga seguro — ahora ejecuta su limpieza de etiquetas hasta un punto fijo (repetir hasta estabilizar) para que una eliminación que reforme una carga (p. ej. <scr<script></script>ipt>) se detecte, coincide con etiquetas de cierre de script/style/etc. con basura final (</script foo>) y elimina un abridor ejecutable sin cerrar. El comportamiento para markdown válido no cambia — solo elimina más.
server/lib/security.mjs: bucle de punto fijo (limitado a 8 pasadas) + patrones de cierre[^>]*>+ eliminación de abridor sin cerrar. +3 casos de regresión entests/cv-xss-bypasses.test.mjs. El límite XSS autoritativo sigue siendo el escape en salida (UI.md); esto refuerza la garantía en reposo y cierra los hallazgos de CodeQL correspondientes.
Nuevo: ninguno.
[1.106.0] — 2026-07-06
Refuerzo de seguridad (triaje de CodeQL). Se corrigieron tres hallazgos reales (aunque de baja severidad): la ruta de error de renderizado ahora escapa el mensaje de error antes de que llegue al DOM (un error del servidor puede reflejar entrada del usuario, así que se trata como no confiable — límite XSS), y las escrituras de propiedades de perfil/config rechazan las claves __proto__ / constructor / prototype (protecciones contra contaminación de prototipos por si acaso — las claves vienen de specs de campos fijos, no de entrada bruta). La mayoría de las alertas restantes son falsos positivos sobre lecturas/escrituras legítimas del escáner en data/* y sobre rutas que ya llevan el limitador propio; se descartaron con justificación.
public/js/router.jsescapaerr.messageconUI.escapeHtmlantes deinnerHTML;server/lib/routes/content.mjsyserver/lib/routes/config.mjsprotegen las claves de prototipo. Sin cambios de comportamiento para entrada válida. Pruebas:tests/security-hardening-v1106.test.mjs(3). Sin nuevas claves i18n.
Nuevo: ninguno.
[1.105.0] — 2026-07-06
Página de uso y coste de IA. Una nueva página Uso de IA (barra lateral, junto a Salud) muestra cuántos tokens has gastado en generaciones de IA en vivo — evaluaciones, informes, chats — desglosado por proveedor en las últimas 24 horas, 7 días, 30 días y todo el tiempo, con un coste estimado en USD. Cada llamada en vivo añade un pequeño registro {provider, in, out} a data/llm-usage.jsonl (no se envía nada a ningún sitio); las ejecuciones sin clave (modo manual) no cuestan nada y no se registran.
- Nuevo módulo de ruta (30.º)
server/lib/routes/usage.mjs—GET /api/usage(agregados de solo lectura) +server/lib/llm-usage.mjs(recordUsagenormaliza las formas de uso de Anthropic/OpenAI/Gemini y añade en modo best-effort;readUsage/aggregateagregan por ventana 24h/7d/30d/todo × proveedor) +server/lib/llm-pricing.mjs(una tabla editable de precios por proveedor$/1Mtokens — los tokens son exactos, los dólares son precios de lista aproximados que puedes corregir; nunca se facturan). El registro se engancha en los puntos de despacho (runActiveProvider+routes/llm.mjs). - Nueva vista
public/js/views/usage.js(#/usage, pestañas de ventana). Pruebas:tests/usage-routes.test.mjs. 17 nuevas claves i18n ×16 (usage.*+nav.usage). Ayuda §6 ampliada en su sitio.
Nuevo: server/lib/routes/usage.mjs; server/lib/llm-usage.mjs; server/lib/llm-pricing.mjs; public/js/views/usage.js.
[1.104.0] — 2026-07-06
Logos de empresa en la tabla de escaneo (respetando la privacidad). Un nuevo interruptor de Apariencia en Ajustes — Mostrar logos de empresa en la tabla de escaneo (desactivado por defecto) — dibuja el logo de cada empresa junto a su nombre en #/scan. El logo es el favicon de la empresa obtenido desde su propio dominio y con proxy en el servidor (GET /api/logo), de modo que ningún servicio de logos de terceros sabe qué empleadores estás viendo. Las ofertas en un portal de empleo compartido (Greenhouse, Lever, Ashby, …) muestran una insignia con una letra en lugar del icono del portal, y cualquier logo que no cargue recae en la misma insignia.
- Nuevo módulo de ruta (29.º)
server/lib/routes/logos.mjs—GET /api/logo?domain=. Valida el dominio (sin esquema/ruta/loopback), obtiene/favicon.icoa través delsafeGetseguro contra SSRF (un nuevo modobinarydevuelve los bytes crudos + content-type; el fijado de DNS, la validación de redirecciones y el límite de tamaño no cambian), hace sniffing de firmas de imagen para no servir nunca una página HTML de error como imagen, cachea aciertos y fallos en un LRU en memoria y no escribe nada en disco. - Nueva librería cliente
public/js/lib/company-logo.js(window.CompanyLogo): desactivada por defecto vía flag en localStorage; omite hosts ATS compartidos en favor de un avatar-letra determinista; respaldoimg.onerrorseguro para CSP. Pruebas:tests/logo-routes.test.mjs. 5 nuevas claves i18n ×16 (appear.*). Ayuda §2 ampliada en su sitio.
Nuevo: server/lib/routes/logos.mjs; public/js/lib/company-logo.js.
[1.103.0] — 2026-07-06
Ajustes: “Herramientas CLI de IA” — cuáles están instaladas. career-ops se basa en Claude Code pero funciona con cualquier CLI de agente del estándar abierto de skills. Una nueva pestaña Herramientas CLI de IA en Ajustes (#/config) muestra cuáles — Claude Code, Codex, Gemini CLI, OpenCode, GitHub Copilot CLI, Qwen, Antigravity — están instaladas en la máquina que ejecuta el servidor, y sus rutas. Es un escaneo de solo lectura del PATH: solo comprueba si cada binario existe y nunca lo ejecuta (sin --version, sin ejecución), no escribe nada y no toca datos del usuario.
- Nuevo módulo de ruta (28.º)
server/lib/routes/cli-detect.mjs—GET /api/cli-detect. La detección resuelve la ruta de un binario desde una lista blanca fija de 7 entradas enprocess.env.PATH(shims.cmd/.exe/.baten Windows; bit de ejecución en POSIX); un archivo hostil en PATH nunca puede ser ejecutado por esta ruta. - Nueva pestaña “Herramientas CLI de IA” en
public/js/views/config.js(carga diferida, enlazable con#/config?tab=cli). Pruebas:tests/cli-detect-routes.test.mjs. 8 nuevas claves i18n ×16 (cli.*+config.tabCli). Ayuda §2 ampliada en su sitio.
Nuevo: server/lib/routes/cli-detect.mjs.
[1.102.0] — 2026-07-05
“Preguntar a la guía” — un chat fundamentado sobre la guía de ayuda integrada. Nueva página Preguntar a la guía 💬 (barra lateral, bajo Ayuda): escribe una pregunta como “¿Cómo escaneo portales de empleo?” y obtén una respuesta extraída solo de la guía de ayuda de la app en tu idioma — muestra qué secciones usó y nunca lee tu CV, perfil ni tu búsqueda de empleo. Es sobre cómo usar la app, no sobre ti. Con clave LLM responde en vivo; sin clave te entrega un prompt listo, ya relleno con las secciones de ayuda relevantes.
- Nuevo módulo de ruta (27.º)
server/lib/routes/docs-assistant.mjs—POST /api/docs-assistant/ask. Recuperación sin dependencias: la guía de tu idioma se divide en sus secciones##y se puntúan por coincidencia de palabras clave con tu pregunta; las mejores se incluyen y el modelo debe responder desde ellas o decir que la guía no lo cubre (sin funciones/rutas inventadas). Cascada de proveedores compartida, respaldo manual, con límite de tasa, sin escrituras, no lee datos del usuario. - Nueva vista
public/js/views/docs-assistant.js. Pruebas:tests/docs-assistant-routes.test.mjs. 14 nuevas claves i18n ×16 (docs.*+nav.docsAssistant). Ayuda §1 ampliada en su sitio.
Nuevo: server/lib/routes/docs-assistant.mjs; public/js/views/docs-assistant.js.
[1.101.0] — 2026-07-05
CV Studio: adapta tu CV + escribe una carta de presentación para un empleo concreto, con verificación tipo reclutador. Nueva tarjeta Adaptar a un empleo en #/cv-studio: pega una descripción de empleo (y, opcionalmente, un rol/titular objetivo) y CV Studio produce un CV adaptado a esa oferta más una carta de presentación a juego, y luego los pasa por una verificación antes de entregarlos — los error bloquean (se corrigen antes de que veas el resultado), los warn aconsejan. La mecánica está destilada de la práctica de coaching de carrera en reglas genéricas — el reclutador lee en segundos, así que lo relevante va arriba, el titular coincide con el rol de la vacante, los resultados llevan cifras concretas y la carta es un teaser breve con un único puente “requisito ↔ tu hecho que encaja”. Se basa solo en tu CV, perfil y two-pager y nunca inventa — sin empresas, roles ni historia codificados.
- Nuevo endpoint
POST /api/cv-studio/tailor(amplía el módulo cv-studio existente — sin 27.º módulo):buildTailorPrompt+ una verificación genéricaTAILOR_INSTRUCTIONS, basada enbundleProjectContext(CV + perfil + two-pager), cascada de proveedores compartida, respaldo manual sin clave, con límite de tasa, sin escrituras. El resultado se exporta a Markdown / PDF / DOCX con la barra compartidareport-export.js. - Pruebas: +3 en
tests/cv-studio-routes.test.mjs. 10 nuevas claves i18n ×16 (cvs.tailor*). Referencia genéricadocs/prompts/resume-cover.md. Ayuda §24 ampliada en su sitio.
Nuevo: docs/prompts/resume-cover.md.
[1.100.0] — 2026-07-05
Two-pager: autorrelleno con IA desde tu CV + Vista previa + exportar a PDF/DOCX/Markdown. El two-pager (#/two-pager) recoge lo que de verdad quieres de tu próximo puesto, pero antes había que redactar cada campo a mano o copiar un prompt en otra herramienta. Ahora el ✨ asistente de relleno con IA se ejecuta en vivo con tu proveedor configurado: lee solo tu CV + perfil (vía bundleProjectContext, sin inventar nada), redacta todos los campos (quién soy / me gusta / imprescindibles / detesto / líneas rojas / innegociables / entorno objetivo) y rellena el formulario para que lo revises, edites y guardes. Sin clave de API vuelve al modal de copiar-el-prompt, igual que antes. Un nuevo botón 👁 Vista previa y exportar renderiza el two-pager como documento con una barra Descargar .md / Guardar como PDF / Guardar como DOCX / Copiar.
- Exportación
.docxsin dependencias. Nuevoserver/lib/docx.mjsque genera un.docxOffice Open XML mínimo pero válido (un ZIP DEFLATE de las cuatro partes OOXML, con CRC-32 por entrada) — sin nueva dependencia (las deps siguen siendoexpress+js-yaml). Nueva rutaPOST /api/export/docx(server/lib/routes/export.mjs, el 26.º módulo de rutas; sin estado, limitado a 200 KB, sin escrituras / sin LLM / sin fetch de URL). Integrado en elpublic/js/lib/report-export.jscompartido, así que el informe de mercado, el plan de carrera y la orientación profesional también ganan exportación a DOCX. - El autorrelleno en vivo usa la cascada de proveedores compartida (
runActiveProvider/providerAvailable); el YAML devuelto se parsea y se ajusta de vuelta a la forma acotada del two-pager (parseYamlFields+normalizeTwoPager) — claves desconocidas descartadas, arrays/cadenas limitados. Se conserva el modo manual. - Pruebas:
tests/export-routes.test.mjs. 4 claves i18n nuevas ×16 (export.saveDocx,twoPager.preview,twoPager.aiFilling,twoPager.aiFilled).
Nuevo: server/lib/docx.mjs; server/lib/routes/export.mjs.
[1.99.0] — 2026-07-05
Página de estado de portales (#/portals). El escáner vigila un conjunto de empresas en portals.yml; un slug de ATS puede romperse silenciosamente y ese empleador desaparece de todos los escaneos futuros. La nueva página Portals lista cada empresa vigilada y, al pulsar Check portal health, sondea cada careers_url a través del safeGet con DNS fijado (a prueba de SSRF) y marca las caídas (un 404 = descartada en silencio) — solo lectura. También refuerza el reportador de errores de la v1.98.0 tras la revisión: el búfer de errores ahora captura fallos de red del fetch, y el limpiador redacta claves de proveedor sin etiqueta.
Nuevo: server/lib/routes/portals.mjs; public/js/views/portals.js.
[1.98.0] — 2026-07-05
Reportador de errores integrado (paridad con el web web-v0.2.0 del proyecto padre). Un botón 🐞 Report a bug en el cajón de notificaciones reúne una instantánea de diagnóstico con piso de privacidad — versiones, tu pantalla, navegador, un resumen de comprobaciones de /api/health y los últimos 20 errores de un nuevo búfer circular del cliente — más una huella de deduplicación determinista (co-web-<base36>), te deja revisar el Markdown exacto y luego abre una incidencia de GitHub pre-rellenada. Nada se envía automáticamente; nunca lleva tu CV, perfil, respuestas, URLs de empleo ni claves. Nuevas libs logbuf.js + bug-report.js; 11 claves i18n ×16; tests/bug-report.test.mjs.
Nuevo: public/js/lib/logbuf.js; public/js/lib/bug-report.js.
[1.97.1] — 2026-07-05
Endurecimiento guiado por revisión y paridad de documentación (seguimiento de v1.97.0). Un barrido de los registros de revisión de IA sacó a la luz correcciones reales:
fit-score.js(insignia de encaje◎del escáner).salaryFloor()ya no asciende una tarifa inferior a la anual a un suelo anual falso — «at least 500 EUR/day», «$80/hr», «6000 monthly» ahora devuelvennullen lugar de un factor descalificador de 500k/80k. La coincidencia de países ahora es por palabra completa (\b…\b), de modo que «Germany» ya no coincide con el adjetivo «German» (ni «Nigeria» dentro de «Nigerian») ni dispara una falsa violación de imprescindible-en-otro-sitio. +3 pruebas entests/fit-score.test.mjs.- Paridad de documentación. Cada README localizado anuncia ahora 16 idiomas de forma coherente — el recuento/la lista de la fila de Ayuda (×13) y la prosa de la sección de Localización más la nota «añade la clave a los N archivos» (×8) seguían con los recuentos previos a v1.85 (8/9). El recuento de adaptadores de la ayuda integrada §17 se corrige a 46 adaptadores — 41 en inglés + 5 en ruso en los 16 paquetes.
Sin cambios de comportamiento más allá de la heurística de la insignia de encaje; sin nuevas rutas, claves ni adiciones de i18n.
[1.97.0] — 2026-07-05
Fuente de escáner Dassault Systèmes + un barrido de calidad en tres frentes.
- Nueva fuente de escaneo — Dassault Systèmes (paridad con el career-ops principal, #1498).
server/lib/sources/dassault.mjs+server/lib/portals/adapters/dassault.mjsreplican el proveedor de «búsqueda de tarjetas» Exalead de coste cero en tokens del proyecto principal (el feed público tras3ds.com/careers/jobs). Es un único endpoint global, así que se selecciona por proveedor (provider: dassault) o se detecta automáticamente a partir de un host3ds.com, con el host anclado contra SSRF awww.3ds.commedianteredirect:'error'. El XML se analiza sin DOM (mapas<Meta>por cada<Hit>), la ciudad/país se extraen de la cadena de categoría localizada, y las ofertas solo se conservan cuando su URL pública está en*.3ds.com. El registro ahora incluye 46 adaptadores (41 EN + 5 RU); el recuento deALL_ADAPTERS, las aserciones de id ordenado y del conjunto EN de/api/scan/sourcespasan de 40 → 41. Suitetests/sources-dassault.test.mjs(10 casos). - Correcciones de robustez portadas del proyecto principal. El analizador de Avature ahora tolera dos variantes de marcado de inquilinos en producción (
article--resultcon un sufijo de índice de posición + un ancla de título de JobDetail sin clase, #1541); Get on Board protege contra unpublished_at0/negativo (se acabaron las fechas espurias de 1970); SuccessFactors limita la última página para que no pueda sobrepasarMAX_JOBS(#1528). - Correcciones de auditoría del servidor.
safe-fetchya no se cuelga ante una respuesta que supera el límite — la ruta de límite de tamaño ahora resuelve la promesa directamente en lugar de esperar un evento'end'que un stream destruido nunca emite (corrige las obtenciones de páginas grandes en/api/pipeline/preview+ auto-pipeline). El registro de actividad SSEstream.*vuelve a ser alcanzable (la comprobación de/api/stream/se movió por encima de la guarda general de «omitir GET»). - Correcciones de auditoría de la SPA. El conmutador de pestañas de
#/statsse protege contra una carrera de renderizado asíncrono — el resultado de una pestaña lenta ya no puede pisar una pestaña más nueva a la que el usuario ya cambió. Las confirmaciones de eliminación de mock interview y de networking ahora pasan un título + cuerpo adecuados (se acabó el diálogo con cuerpo vacío). - Correcciones de traducción. Se corrigieron valores del diccionario sin traducir — ucraniano
config.modes*(Adaptive Framing / Exit Narrative / Location Policy), rusoeval.jdLbl(«Job Description»), italianodash.quick.contactoSub(«referral» → «segnalazione») — además de la localización del texto fijo inglés**16 locales**en los CHANGELOG de ru/uk/ja/ko/zh-CN/zh-TW.
Nuevo: server/lib/sources/dassault.mjs; server/lib/portals/adapters/dassault.mjs.
[1.96.0] — 2026-07-04
Orientación profesional (Epic 27). Una nueva página #/orientation responde a la pregunta «¿qué direcciones encajan realmente conmigo?» — la lectura que obtendrías de un test vocacional, pero inferida a partir de tu propio CV y perfil en lugar de un cuestionario. Haz clic en Generar perfil y el modelo devuelve tus vectores de carrera con mejor ajuste (cuáles de los ocho arquetipos — Funcionalista, Administrador, Comunicador, Especialista, Analista, Innovador, Gestor, Emprendedor — encajan, con evidencia), una inclinación de tipo profesional, roles recomendados, fortalezas profesionales ligadas a tu CV, tendencias de estilo de trabajo y recomendaciones de desarrollo. Es una reflexión de IA sobre cómo se lee tu CV — no un test psicométrico: nunca inventa logros ni informa puntuaciones numéricas como si fueran medidas. Expórtalo a Markdown o PDF; no se escribe nada en el disco.
- Nueva ruta
server/lib/routes/orientation.mjs(24.º módulo de rutas) —POST /api/orientation/generateconstruye el prompt del perfil a partir de CV+perfil+two-pager+memoria mediante la cascada de proveedores compartida, con un fallback manual de copiar y pegar y sin escritura de archivos. - Reutiliza
report-export.jspara Markdown/PDF/copia, dentro del grupo de navegación Desarrollo. - Tests:
tests/orientation-routes.test.mjs(enfoque de reflexión / sin puntuaciones inventadas, modo manual sembrado con CV/perfil). 7 nuevas claves i18n ×16 idiomas, Ayuda §28 ×16.
Nuevo: #/orientation; server/lib/routes/orientation.mjs.
[1.95.0] — 2026-07-04
Plan de carrera (Epic 26). Una nueva página #/career-plan convierte tu CV y tu perfil en un plan de desarrollo concreto y personalizado. Elige un horizonte (6/12/24 meses) y un enfoque opcional, y el modelo — leyendo tu CV, tu perfil, tu two-pager y tu nota de memoria — redacta una instantánea del punto de partida, un DAFO de fortalezas/crecimiento, objetivos como SMART / OKR / WOOP, trayectorias alternativas, un plan de competencias técnicas y blandas, una hoja de ruta mes a mes, métodos de seguimiento del progreso, escollos y palancas de apoyo. Planifica hacia adelante a partir de lo que tus materiales realmente muestran y nunca inventa datos sobre tu historial. Edítalo en línea, Guárdalo en la capa de usuario (config/career-plan.md) y expórtalo a Markdown o PDF.
- Nueva ruta
server/lib/routes/career-plan.mjs(23.º módulo de rutas) —GET/PUT /api/career-plan(escribeconfig/career-plan.md) +POST /api/career-plan/generate(cascada compartida de proveedores, modo manual como respaldo, sin fabricación).PATHS.careerPlan. - Reutiliza el helper compartido
report-export.js(v1.94.0) para Markdown/PDF/copia, y un nuevo grupo de navegación Crecimiento. - Pruebas:
tests/career-plan-routes.test.mjs(acotación, ida y vuelta GET/PUT, prompt sembrado con CV/perfil según el horizonte). 20 nuevas claves i18n en los 16 locales, Ayuda §27 en los 16.
Nuevo: #/career-plan; server/lib/routes/career-plan.mjs; PATHS.careerPlan.
[1.94.0] — 2026-07-04
Estadísticas, rediseñadas (Epic 25). La página #/stats es ahora una sección de Estadísticas con tres pestañas, con gráficas reales y muchos más datos. Una nueva pestaña de Informe de mercado le pide al modelo un análisis salarial y del mercado laboral de tus roles objetivo en la región y la moneda que elijas — resumen ejecutivo, salario por nivel con percentiles P10/P25/P75/P90, principales empleadores, una tabla de competencias demandadas, frecuencia de beneficios, el reparto presencial/híbrido/remoto, tendencias a 12–24 meses y orientación de negociación. Cada cifra se etiqueta como una estimación orientativa a partir del conocimiento del modelo, nunca presentada como datos extraídos. Una nueva pestaña de Mi pipeline grafica tu propio tracker: distribución de puntuaciones, embudo de estados, principales empresas y roles, postulaciones a lo largo del tiempo y tasas de conversión. La vista original de rol objetivo (vacantes/salario por país + tendencia de instantáneas guardadas) pasa a una tercera pestaña, ahora con un selector de moneda y un resumen de publicaciones por rol.
- Exporta cualquier informe a Markdown o PDF, o cópialo — mediante el helper compartido
report-export.js(descarga de blob Markdown; PDF a través del runner de PDF en línea existente). - Nueva ruta
server/lib/routes/market.mjs(22.º módulo de rutas) —POST /api/stats/marketconstruye un prompt de análisis de mercado a partir de tu CV/perfil (de modo que conoce tus roles objetivo), la región y la moneda, lo pasa por la cascada compartida de proveedores y recurre a un prompt para copiar y pegar cuando no hay clave. Sin escrituras en disco. - Pruebas:
tests/market-routes.test.mjs(acotación de región/moneda, prompt con etiquetado de honestidad, modo manual sembrado con CV/perfil). 36 nuevas claves i18n en los 16 locales, Ayuda §26 en los 16.
Nuevo: #/stats rediseñada en pestañas; server/lib/routes/market.mjs; public/js/lib/report-export.js.
[1.93.0] — 2026-07-04
Capa de memoria (Epic 24). Una nueva página #/memory guarda una nota breve y editable de «recuerda esto sobre mí» que el asistente tiene en cuenta en cada tarea:
- Una nota, en todas partes — al incrustarse en
bundleProjectContext, la nota llega automáticamente a cada petición de IA (evaluar, mock interview, networking, CV Studio) en todos los proveedores. Escríbela una vez; dirige todo. - Orientación, no hechos — captura tus preferencias y cómo te gusta trabajar (tono, formato, deal-breakers, cadencia), nunca nuevas afirmaciones factuales sobre tu experiencia — esas siguen viviendo solo en tu CV, tu perfil y tu two-pager. Se guarda en la capa de usuario en
config/memory.md, nunca sobrescrita por las actualizaciones. - Sugerir a partir de tus datos —
POST /api/memory/suggestexplora tu propio tracker de postulaciones en busca de patrones de comportamiento y redacta viñetas para que las revises y edites. Lee tu tracker; nunca inventa hechos, y no hace ninguna llamada en vivo.
Nuevo: server/lib/routes/memory.mjs (21.º módulo de rutas — GET/PUT /api/memory + POST /api/memory/suggest), public/js/views/memory.js, PATHS.memory, y un bloque config/memory.md añadido a bundleProjectContext. 11 nuevas claves i18n en los 16 locales. Pruebas: tests/memory-routes.test.mjs.
[1.92.0] — 2026-07-04
CV Studio (Epic 21). Una nueva página #/cv-studio dota a tu CV de tres herramientas honestas y mayormente locales:
- Diagnóstico de currículum — una puntuación determinista de 0–100 con explicaciones por comprobación (impacto cuantificado, verbos débiles, palabras de moda, longitud, secciones esenciales, información de contacto). Puramente del lado del cliente (
window.CvDiagnostics) — sin LLM, nada inventado, cada hallazgo explicado para que tú decidas qué cambiar. - Máscara de privacidad — redacta datos personales (email, teléfono, enlaces/handles, dirección postal y, opcionalmente, tu nombre → iniciales) antes de compartir tu CV como muestra o captura. Se ejecuta enteramente en el navegador (
window.CvPrivacy); informa exactamente qué redactó y nunca almacena el original. - Hazlo humano / coincidir con tu voz — pega una línea o un párrafo rígido y reescríbelo con tu voz, anclado en el servidor en
voice-dna.mdywriting-samples/. Barandilla estricta: puede reordenar, condensar y re-vocalizar, pero nunca introduce un hecho, una métrica ni un logro que no esté ya en el texto. Se ejecuta en vivo a través de la cascada compartida de proveedores, o te devuelve un prompt para copiar y pegar sin clave.
Nuevo: server/lib/routes/cv-studio.mjs (20.º módulo de rutas — POST /api/cv-studio/humanize), public/js/views/cv-studio.js, public/js/lib/cv-diagnostics.js, public/js/lib/cv-privacy.js, PATHS.voiceDna + PATHS.writingSamplesDir. 29 nuevas claves i18n en los 16 locales. Pruebas: tests/cv-diagnostics.test.mjs, tests/cv-studio-routes.test.mjs. (La galería de plantillas, la exportación a Word y el archivo de PDF de la oferta quedan como trabajo de seguimiento de CV Studio.)
[1.91.0] — 2026-07-04
Networking e investigación profunda de empresas (Epic 16). Una nueva página #/networking convierte una empresa en un plan accionable para conseguir una entrevista, anclado en tu CV, perfil y two-pager:
- Dossier de empresa — un resumen conciso de qué hace la empresa, señales recientes dignas de citar y ganchos de “por qué encajo” extraídos de tu trayectoria real.
- A quién contactar — 3–5 personas objetivo (hiring manager, reclutador interno, un IC sénior del equipo, un contacto cálido/de la misma alma máter) con una cadena de búsqueda de LinkedIn concreta para encontrar a cada uno. Nunca inventa nombres reales.
- La vía de presentación más cálida — la ruta cálida más realista para tu trayectoria (empleador/escuela/comunidad en común, un camino de segundo grado o un mensaje directo en frío de alta señal) y por qué.
- Borradores de contacto — mensajes cortos y específicos para las personas principales, anclados en tus puntos de prueba reales.
- En vivo o manual — se ejecuta en vivo a través de la cascada compartida de proveedores con cualquier clave, o te devuelve un prompt listo para copiar y pegar (fallback honesto, nada inventado). Guardar plan persiste un plan terminado en la capa de usuario (
networking/net-{company}-{role}-{date}.md); la página lista, abre y elimina los planes guardados.
Nuevo: server/lib/routes/networking.mjs (19.º módulo de rutas), public/js/views/networking.js, PATHS.networkingDir. Reutiliza la cascada server/lib/llm-dispatch.mjs de v1.90.0. 24 nuevas claves i18n en los 16 locales. Pruebas: tests/networking-routes.test.mjs.
[1.90.0] — 2026-07-04
Mock Interview 2.0 (Epic 15). Una nueva página #/mock-interview convierte tu CV, perfil, two-pager y banco de historias en un ensayo de entrevista turno por turno:
- Práctica conversacional — fija un puesto objetivo (+ empresa / JD opcionales) y el entrevistador abre con una pregunta enfocada. Cada respuesta que envías recibe una réplica estructurada: Feedback (fortalezas + la brecha STAR+R), una Puntuación (
N/5) y una Siguiente pregunta que indaga en la parte más débil de tu última respuesta. Anclada en el servidor a tus materiales reales — nunca inventa experiencia que no tienes. - Consciente del banco de historias —
interview-prep/story-bank.mdse incrusta en el prompt (mismo nivel de confianza quecv.md), de modo que el feedback puede señalarte tus propias mejores historias. - En vivo o manual — con una clave de proveedor, el turno se ejecuta en vivo a través de la cascada compartida de proveedores (Anthropic → Gemini → OpenAI → Qwen → OpenRouter → GitHub Models); sin clave obtienes un prompt listo para copiar y pegar (fallback honesto, sin respuestas inventadas).
- Sesiones guardadas — pulsa Save transcript para persistir una entrevista terminada en la capa de usuario (
interview-prep/mock-{company}-{role}-{date}.md); la página lista, abre y elimina las sesiones guardadas.
Nuevo: server/lib/routes/interview.mjs (18.º módulo de rutas), public/js/views/mock-interview.js, server/lib/llm-dispatch.mjs (cascada compartida de proveedores), PATHS.storyBank, bundleProjectContext({ extraFiles }). 30 nuevas claves i18n en los 16 locales. Pruebas: tests/interview-routes.test.mjs.
[1.89.0] — 2026-07-04
Ajuste candidato-mercado — el two-pager (Epic 14). Una nueva página #/two-pager te permite capturar lo que tú realmente quieres de tu próximo puesto, inspirada en el «Mnookin two-pager» de Never Search Alone:
- Constructor guiado — una narrativa en primera persona «Quién soy», una nota «Entorno objetivo» y cinco editores de listas de chips: loves, must-haves, hates, deal-breakers y non-negotiables. Se guarda en la capa de usuario del proyecto padre (
config/two-pager.yml) víaPUT /api/two-pager— nunca sobrescrita por actualizaciones del sistema. - Asistente de relleno con IA (
POST /api/two-pager/draft) — construye un prompt Mnookin listo para ejecutar con tu CV + perfil incrustados, para que lo corras en cualquier LLM y pegues el resultado de vuelta. Solo usa tus propios materiales; nada se inventa. - Insignia de encaje-con-lo-que-quieres — cada oferta en
#/scanmuestra ahora una puntuación de encaje◎ N(del lado del cliente, víawindow.FitScore) que contrasta el tipo de trabajo, país, salario mínimo y reubicación de la oferta con tu two-pager. Honesta por diseño: cuando una oferta no aporta señal comparable, no se muestra insignia (nunca un número inventado). Las violaciones de deal-breakers pesan más que los disgustos leves. - Alimenta cada evaluación — el two-pager guardado se incrusta en
bundleProjectContext, de modo que todas las evaluaciones LLM posteriores combinan tus preferencias declaradas con el encaje CV-vs-oferta.
Nuevo: server/lib/routes/two-pager.mjs, public/js/views/two-pager.js, public/js/lib/fit-score.js, PATHS.twoPager. 27 nuevas claves i18n en los 16 locales. Pruebas: tests/two-pager-routes.test.mjs, tests/fit-score.test.mjs.
[1.88.0] — 2026-07-04
Pulido del issue #29 — huecos de i18n en Escaneo + higiene de la API.
- Localizadas las últimas cadenas de Escaneo codificadas (hoja de ruta v1.69.4): las píldoras de resumen por fuente (
N nuevas / M coincidentes), los avisosN nuevas ofertasy la insigniarelocahora pasan port()— 4 nuevas claves (scan.pillNew,scan.pillMatching,scan.newOffers,scan.relocBadge) en los 16 locales. Los usuarios que no usan inglés ya no ven texto suelto en inglés en el flujo principal de escaneo. - Deshabilitada la cabecera
X-Powered-By(hoja de ruta v1.69.5):app.disable('x-powered-by')encreateApp()— el servidor ya no anuncia Express. (El resto de ese conjunto ya se había entregado:parentVersionelimina su comentario de release-please, el interruptor de tema en modo claro, el cierre de modales al cambiar de ruta y la localización de «Score» (rep.score) en Informes.)
Pruebas: tests/scan-i18n-gaps.test.mjs + una aserción de ausencia de X-Powered-By en tests/security-headers.test.mjs.
[1.87.0] — 2026-07-04
4 nuevos proveedores de escaneo sin autenticación (paridad con career-ops padre v1.16.0). El registro del escáner crece de 41 → 45 adaptadores (40 EN + 5 RU) — todos públicos, sin autenticación, con host fijado, redirect:'error' (a salvo de SSRF), cada uno con una prueba aislada para CI:
- Get on Board (
getonbrd) — JSON:API público de todo el portal (tecnología LATAM/remoto), seleccionado por proveedor, paginado.server/lib/sources/getonbrd.mjs. - Amazon (
amazon) — JSON de búsqueda público deamazon.jobs, detectado por host oprovider: amazon, paginado por desplazamiento.server/lib/sources/amazon.mjs. - Avature (
avature) — ATS*.avature.netpor inquilino, analizado desde HTML, detectado por host oprovider: avature.server/lib/sources/avature.mjs. - SAP SuccessFactors (
successfactors) — lista de mosaicos RMK por inquilino (*.successfactors.eu/.com,jobs2web.com), analizada desde HTML.server/lib/sources/successfactors.mjs.
Cada uno incluye un sources/<slug>.mjs (con meta autodescubierto → menú desplegable #/scan) y un portals/adapters/<slug>.mjs en ALL_ADAPTERS (la regla de los dos registros) + tests/sources-<slug>.test.mjs. El recuento de ALL_ADAPTERS y las aserciones de id ordenado y del conjunto EN de /api/scan/sources subieron de 36→40; GET /api/scan/sources ahora lista 45.
[1.86.0] — 2026-07-03
Estadísticas por roles objetivo (#/stats) — estadísticas de vacantes y salarios del mercado para TUS roles objetivo. Una nueva página de Analítica lee tus roles objetivo del perfil (config/profile.yml → no codificados) y las ofertas del último escaneo, y luego muestra, por rol y país:
- Vacantes por país y salario mediano por país (USD) — agregados en el cliente (
public/js/lib/role-stats.js, reutilizandowindow.Countries) a partir de los datos dispersos que los escáneres ya recopilan. Los salarios en cualquier moneda se normalizan a USD mediante una tabla FX explícitamente aproximada, con una advertencia sobre el tamaño de la muestra — nunca se inventan. - Filtros de rol y país y gráficos de barras y tendencia con SVG en línea hechos a mano (sin nuevas dependencias, seguros para CSP — solo
addEventListener). - Guardar instantánea (
POST /api/stats/snapshot) persiste el agregado actual endata/role-stats.jsonl; el gráfico de tendencia (GET /api/stats/trend) sigue el recuento de vacantes a lo largo del tiempo — la vista de «dinámica». Híbrido honesto: las instantáneas provienen de datos de escaneo locales, actualizados bajo demanda. - Totalmente localizado en los 16 locales (26 nuevas claves i18n).
Nuevo: server/lib/routes/stats.mjs (16.º módulo de rutas), public/js/lib/role-stats.js, public/js/views/stats.js, PATHS.roleStats; pruebas role-stats.test.mjs (7) + stats-routes.test.mjs (5).
[1.85.0] — 2026-07-03
Locales alemán, italiano y turco (paridad de locales con career-ops padre v1.16.0). La interfaz ahora se distribuye en 16 idiomas — de 🇩🇪, it 🇮🇹 y tr 🇹🇷 se suman a los 13 existentes.
- Traducción completa de la interfaz — las 730 claves i18n traducidas en
public/js/lib/locales/i18n-dict.{de,it,tr}.js; el selector de idioma lista Deutsch / Italiano / Türkçe y la detección automática del idioma del navegador reconocede/it/tr(public/js/lib/i18n.js). - Guía de ayuda integrada —
docs/help/{de,it,tr}.mdtraducidos (estructura completa de 19 H2 / 75 H3), servidos porGET /api/help/:lang. - Documentación — se añaden
README.{de,it,tr}.mdyCHANGELOG.{de,it,tr}.md; la comprobación de paridad de locales del CHANGELOG ahora cubre 15 locales no EN. - Andamiaje de prompts —
server/lib/prompts.mjs(LOCALE_NAMES+SCAFFOLD_STRINGS) localizado para los tres nuevos locales, de modo que la salida del LLM sigue el idioma de la interfaz.
Todas las comprobaciones de paridad (i18n-locale-files, i18n-coverage, check-changelog-parity, lang-switcher-rtl) extendidas al conjunto de 16 locales.
[1.84.0] — 2026-06-30
Cooldown de repostulación + compensación en pipeline.md (paridad con career-ops padre v1.15.0). Dos mejoras del escáner:
- Cooldown de repostulación (#1201): el escaneo EN ahora omite roles en empresas a las que postulaste recientemente, para que los resultados se centren en NUEVAS ofertas. Configura ventanas por empresa en
config/profile.ymlbajore_apply_windows:(last_apply_date,same_role_days,applied_to: [roles],cross_role_bucketopcional); la coincidencia de empresa es insensible a puntuación y basada en límites de palabra (server/lib/cooldown.mjs). Desactivado cuando la clave está ausente; el registro de escaneo muestraCooldown skipped: N. - Compensación en pipeline.md (#1017): las ofertas escaneadas ahora persisten su salario como columna opcional al final (
url | <salary>) endata/pipeline.md. La URL sigue siendo la clave de deduplicación (la columna| compse elimina en la lectura), la celda se sanea (sin inyección de filas/columnas, fórmulas iniciales neutralizadas) y los pipelines con solo URL son retrocompatibles.
Incluye tests/cooldown.test.mjs + tests de compensación de pipeline. El número de fuentes se mantiene en 41 (ambas son mejoras de lógica de escaneo, no nuevos boards).
[1.83.0] — 2026-06-30
Detector de reposteos / empleos fantasma (paridad con career-ops padre v1.15.0). Un nuevo panel 🔁 Reposteos / empleos fantasma en #/scan señala los clústeres empresa+rol que fueron publicados de nuevo bajo distintas URLs en una ventana móvil de 90 días — señal de pipelines obsoletas y ofertas fantasma. Respaldado por un comparador difuso de títulos de rol (server/lib/role-matcher.mjs) y un detector de solo lectura (server/lib/detect-reposts.mjs) sobre data/scan-history.tsv, expuesto vía GET /api/scan/reposts. Además: parentVersion en /api/health ahora reporta solo el semver (se elimina el comentario # x-release-please-version de release-please). Incluye tests/detect-reposts.test.mjs. El número de fuentes se mantiene en 41 — los reposteos son una función de análisis, no un nuevo board.
[1.82.0] — 2026-06-30
Fuente de escaneo NoDesk (paridad con career-ops v1.15.0). El feed RSS de empleo remoto de NoDesk ahora es una fuente de primera clase — añade una entrada provider: nodesk y aparece en el desplegable Source de #/scan (41 adaptadores en total: 36 EN + 5 RU). Host fijado a nodesk.co con redirect:'error' (anti-SSRF); los títulos se dividen por Role at Company (NoDesk no tiene etiqueta de ubicación, por lo que la ubicación queda vacía); todas las filas son remotas. Incluye una suite CI aislada tests/sources-nodesk.test.mjs; suite de tests unitarios completa en verde con 1523.
[1.81.0] — 2026-06-29
Paridad con el career-ops padre — 13 nuevas fuentes de escaneo de bolsas de empleo. Incorpora el último lote de proveedores desde el main de Fighter90/career-ops al escáner en proceso. APIs públicas universales (seleccionadas por proveedor): Arbeitnow, Himalayas, Jobicy, Landing.jobs, 4 Day Week, The Muse, The Hub, Jobspresso (RSS) y Hacker News “Who is hiring?” (Algolia en dos pasos). Bolsas polacas (detectadas por host o provider:): JustJoin.it y NoFluffJobs (búsqueda POST). ATS por tenant (autodetectados desde careers_url): Pinpoint (<slug>.pinpointhq.com/postings.json) y Rippling (ats.rippling.com/<slug> → api.rippling.com). Cada fuente está fijada por host con redirect:'error' (anti-SSRF) y es seleccionable en el desplegable Source de #/scan — el registro incluye ahora 40 adaptadores de escáner (35 EN + 5 RU). Añade 13 conjuntos de tests CI aislados por fuente; suite de tests unitarios completa en verde con 1513 tests.
[1.80.0] — 2026-06-28
Cinco mejoras de escaneo (ideas de job-crawler, reimplementadas). (1) Fuente Teamtailor — sitios <slug>.teamtailor.com vía su feed público /jobs.rss, autodetectado desde careers_url (host fijado + redirect:'error'); el registro trae ahora 27 adaptadores. (2) Cuarentena de fuentes — una fuente con 404/410 permanente se guarda en data/scan-quarantine.json y se omite en escaneos posteriores (autorreparable: se reintenta a los 14 días). (3) Máx. por fuente — campo opcional en #/scan que limita los empleos por board (∞ por defecto). (4) Publicado en — filtro de antigüedad del lado del cliente (24h / 7d / 30d). (5) Búsquedas guardadas + ★ favoritos — nombra y reutiliza conjuntos de filtros y marca empleos, en localStorage con validación defensiva (una caché corrupta se reinicia limpia); la caché de resultados se reinicia antes de cada escaneo y se rellena en vivo.
[1.79.0] — 2026-06-28
Fuente de escaneo WeWorkRemotely (paridad con career-ops v1.14.0). El feed RSS de empleo remoto de We Work Remotely ahora es una fuente de primera clase — añade una entrada provider: weworkremotely y aparece en el desplegable Source de #/scan (26 adaptadores en total). Host fijado a weworkremotely.com con redirect:'error' (anti-SSRF); los títulos se dividen por Company: Role. Además: las palabras clave de title_filter ahora se recortan antes de la comprobación de longitud (parent #1261).
[1.78.2] — 2026-06-27
Refuerzo de i18n y UX (correcciones tras v1.78.1). El nombre accesible del logo ahora está localizado en los 13 idiomas (nav.logoHome). Enter en la búsqueda global estando ya en #/scan fuerza un re-render para no perder el término precargado (guard de misma ruta). health.title ahora está traducido al polaco (Kondycja) y al danés (Systemtilstand) — antes quedaba en inglés. Tests 1235 → 1238.
[1.78.1] — 2026-06-27
Correcciones de UX en Scan. La tabla de resultados de #/scan ahora se actualiza automáticamente durante el escaneo y una vez más al terminar, sin recargar. La búsqueda global de la barra superior muestra una pista Enter y, con una consulta que no es URL, salta a #/scan con el campo pre-rellenado (antes #/tracker). El logo ahora enlaza al panel (inicio).
[1.78.0] — 2026-06-27
Filtro geográfico en la página de Scan — filtra resultados por país, con banderas. Un nuevo desplegable País en #/scan lista cada país detectado en tus resultados (emoji de bandera + recuento), para quedarte solo con roles ligados a un país — junto al filtro Remote/Hybrid/Onsite, de modo que puedes buscar trabajo ligado a un país y remoto. Respaldado por un nuevo helper countries.js que mapea la ubicación de texto libre (nombres de país, alias y ~100 ciudades clave) a un país ISO + bandera; la detección es conservadora y nunca adivina.
[1.77.0] — 2026-06-27
Danés (Dansk) añadido como el 13.º idioma de la interfaz. Traducción completa de la UI, la guía de ayuda integrada (19 H2 / 75 H3), README y CHANGELOG. El danés se suma al selector de idiomas con banderas; la maquinaria i18n (ensamblador, auditoría, comprobaciones de paridad, snapshot) abarca ahora 13 locales.
[1.76.0] — 2026-06-26
Paridad con career-ops v1.13.0 — seis nuevas fuentes, refuerzo del escáner y tabla de resultados sin tope.
Añadido
- Seis fuentes ATS por inquilino — BambooHR, Breezy HR, Comeet, Personio, Recruitee, SolidJobs. Se autodetectan por el host de
careers_url(Comeet requiere elapi:completo); cada host se fija con regex anclado +redirect:'error'(anti-SSRF). Seleccionables en el desplegable Source de#/scan— el registro ahora trae 25 adaptadores (20 EN + 5 RU). AñadefetchTextpara el feed XML de Personio. trust_filter— puntuación de confianza opcional (0–100, nivel high/medium/low, flags), solo anota. Las filas bajohighmuestran una insignia ⚠ neutral en#/scan; nunca se descarta nada.- Arbeitsagentur
remoteMatch+remoteMaxPages— detección de remoto por configuración:title,filter(homeoffice=nv_truedel lado servidor + paginación) uoff.
Cambiado
- Sin tope de resultados. Se eliminó
MAX_STORED_RESULTS(2000) — se almacenan todas las coincidencias y la tabla#/scanlas pagina (200/pág.). - Robustez del filtro de título — los acrónimos cortos (COO, SDR…) coinciden por límites de palabra; una config
title_filtermal formada ya no rompe el escaneo. Ambos escáneres.
Pruebas
- +32 casos (1190 → 1222):
sources-ats-providers,title-filter,arbeitsagentur-remote,trust-validatory un guardiánscan-result-capreescrito («sin tope»).
[1.75.2] — 2026-06-19
docs: paridad documental completa para los agregadores del escáner de la v1.75.0 en los 12 idiomas. Sin cambio de código — alinea la documentación de cara al usuario con las siete fuentes que aterrizaron en la v1.75.0:
- Guía de ayuda (12 idiomas). §5 gana un bloque
content_filter(gating por palabras clave de descripción/fragmento, hermano delocation_filter) y una nota sobre agregadores; §7 enumera las siete nuevas fuentes en el barrido de escaneo de un clic y en la enumeración completa del desplegable Source; el recuento de adaptadores de §17 se corrige del obsoleto “11 adapters” a “19 adapters — 14 English + 5 Russian”. No se añadió ningún encabezado##/###, por lo que la estructura cerrada de 19 H2 / 75 H3 queda intacta. - README (9 idiomas completos). Nueva viñeta “Aggregator boards (v1.75.0)” bajo las fuentes de escaneo, más la insignia de versión actualizada a v1.75.2. (Los README abreviados pl/uk/ar no tienen lista por fuente y quedan intencionadamente intactos ahí.)
- Documentación de referencia.
docs/portals-examples.mdgana una sección “Aggregator boards” de copiar y pegar con bloques de configuraciónprovider:/<provider>:precisos para las siete;docs/PROJECT.mdactualizado a 19 adapters;docs/sdd/CONVENTIONS.mddocumenta la distinción de los dos registros (sources/registry.mjspara el desplegable frente aportals/registry.mjspara el fetching), la selección de agregador basada enprovider:enhebrada comoopts.company, el saneador de escritura de escaneo (scan-sanitize.mjs) y el recuento de tests de la v1.75.1 (1190). - QA. Añadido
qa/QA-REGRESSION-PROMPT-v1.75.2-FULL.md— el driver de compuerta de lanzamiento de superficie completa, renovado para el ciclo de agregadores de escaneo de la v1.75.x.
[1.75.1] — 2026-06-19
fix(scan): pulido de robustez sobre las fuentes basadas en configuración de la v1.75.0. Tres pequeñas correcciones de endurecimiento surgidas de la revisión posterior al lanzamiento (sin cambio de comportamiento para un escaneo sano):
- Pausas de paginación conscientes del abort. Las pausas de cortesía entre páginas de Glints (300 ms) y Jobstreet/SEEK (200 ms) ahora se resuelven de inmediato cuando se dispara el
AbortSignaldel escaneo, mediante un nuevo helperdelay(ms, signal)enserver/lib/http-json.mjs, de modo que un cliente desconectado no pueda mantener abierto un escaneo paginado durante una pausa extra. - Error descriptivo para respuestas no JSON.
fetchJsonahora envuelve un cuerpo2xxno JSON (p. ej. una página HTML de mantenimiento servida con estado 200) comonon-JSON 2xx response from <url>en lugar de exponer unSyntaxErrordesnudo, de modo que el registro de errores por fuente del escáner nombre el endpoint que se comporta mal. - Normalización de escritura de escaneo más fuerte.
normalizeScanScalarahora colapsa la tabulación vertical, el avance de página y los separadores Unicode de línea/párrafo (\v \f U+2028 U+2029) además de\r \n \t— un superconjunto estricto, de modo que ningún separador de registro/línea que una hoja de cálculo o un visor pudiera respetar sobreviva enscan-history.tsv.
[1.75.0] — 2026-06-19
feat(scan): incorpora la paridad con el career-ops padre v1.12.0 — siete nuevas fuentes de empleo, filtrado de contenido y correcciones de seguridad/calidad. La web-ui ejecuta sus propios escáneres en proceso (no delega en el scan.mjs del padre), por lo que los cambios de proveedor y de escaneo de la v1.12.0 del padre no se propagan automáticamente — esta versión reimplementa los aplicables conforme al contrato de adaptadores de la web-ui.
- Siete nuevas fuentes de escáner. Tres agregadores remotos de alcance global — RemoteOK, Remotive, Working Nomads — encajan en el patrón autodescubierto
server/lib/sources/*.mjs(se seleccionan conprovider: remoteok/remotive/workingnomads). Cuatro agregadores regionales basados en configuración — careers de IBM, Arbeitsagentur (Agencia Federal de Empleo alemana), Glints (Sudeste Asiático), Jobstreet / SEEK — leen un bloque de configuración<provider>:por entrada; el en-scanner ahora propaga la entrada de empresa resuelta hasta cada fetcher para que puedan leerla. Las siete aparecen automáticamente en el desplegable de fuentes de#/scan. content_filter(padre #974). Bloque opcional deportals.yml(listas de palabras clavepositive/negative) que filtra una oferta según el texto de su descripción/fragmento — refleja la semántica delocation_filter; las ofertas sin descripción siempre pasan. Integrado en los escáneres EN y RU.- Endurecimiento de la escritura de escaneo (padre #1098). Los metadatos de feeds externos se sanean ahora antes de aterrizar en
data/scan-history.tsvydata/pipeline.md: los caracteres de control se colapsan (un salto de línea en empresa/título ya no puede inyectar una fila TSV) y un= + - @inicial se neutraliza frente a la inyección de fórmulas de hoja de cálculo. secondaryLocationsde Ashby (padre #1073). La fuente Ashby ahora pliega la etiqueta de región de cada ubicación secundaria másaddressLocality/addressCountrypostal dentro de la cadena de ubicación (deduplicada), de modo que un puesto elegible para la UE cuya etiqueta principal diga p. ej. “Canada” aflore para ellocation_filter.- Validación de la forma del informe de evaluación (padre #819). Los proveedores en proceso de
/api/evaluate(Anthropic / OpenAI / Qwen / OpenRouter / GitHub Models) ahora marcan un informe A–G /SCORE_SUMMARYmalformado como un arraywarningsno fatal; la ruta de evaluación de Gemini ya hereda la salvaguarda delgemini-eval.mjsdel padre. - docs: Antigravity CLI añadido a las listas de asistentes compatibles en los 12 READMEs (se corresponde con el proveedor Gemini).
Heredado gratis del git pull del padre (la web-ui delega en estos): repliegue de fuentes CJK para PDF en japonés (#1053), fuentes PDF compatibles con ATS (#1074), salvaguarda CJK para LaTeX (#1054), correcciones de tracker/merge/followup/dashboard, y los modos chinos modes/zh (la web-ui lista los modos dinámicamente).
[1.74.3] — 2026-06-18
docs(parent-source): apunta el repo padre career-ops al fork Fighter90/career-ops. La web-ui ahora referencia el fork del mantenedor como proyecto padre en todos los puntos donde es una fuente real: el valor por defecto CAREER_OPS_REPO del instalador bin/setup.sh, cada enlace de git clone / “encima de” / onboarding en los 12 READMEs, y la documentación de agentes (CLAUDE.md, AGENTS.md, GEMINI.md, .github/copilot-instructions.md, docs/). El crédito al autor santifer (y el aviso de UI no oficial) no cambia — solo se movieron las URLs de origen/clonado. tests/sh-files.test.mjs ahora verifica que el instalador clona el fork.
[1.74.2] — 2026-06-17
fix(health): mostrar GITHUB_MODELS_API_KEY como verificación opcional en #/health y en /api/status/providers. El proveedor GitHub Models de la v1.74.0 era configurable en #/config, pero no tenía fila en la página de Salud y faltaba en la superficie de proveedores keysConfigured. Se añadió la verificación opcional (con la misma redacción “set / unset (manual mode)” que los otros cinco proveedores de evaluación en vivo) y github (+ su GITHUB_MODELS_MODEL) a /api/status/providers, de modo que el enrutamiento del proveedor activo y la página de Salud ahora reflejan los seis. La prueba de fila de salud de tests/api.test.mjs se amplió a los seis proveedores.
[1.74.1] — 2026-06-17
docs + test: sección “Instalar un asistente de IA” del README; cobertura completa de ramas para el conector de Gemini. Se añadió una tabla de instalación/inicio de sesión al README — enlaces de instalación para Claude Code / Gemini CLI / Codex / Qwen Code / OpenCode / GitHub Copilot CLI + la correspondencia de proveedor #/config de cada uno + “inicia sesión antes de continuar” (refleja el inicio rápido de career-ops.org/docs; aclara que la web-ui es la alternativa autónoma que no requiere CLI). El nuevo tests/gemini-connector.test.mjs (8 casos) cubre cada rama de runGemini — sin clave, éxito, error de API, finalización vacía/bloqueada, cuerpo malformado, tiempo de espera agotado, error de red, hasGeminiKey — llevando server/lib/gemini.mjs al 100% de sentencias. Cobertura global: 96% líneas / 88% ramas / 96% funciones. Suite 1126 → 1134.
[1.74.0] — 2026-06-17
feat(llm): GitHub Models (Copilot) como el 6.º proveedor + alineación canónica de 6 asistentes. career-ops.org/docs lista seis asistentes de codificación con IA — Claude Code, Gemini CLI, Codex, Qwen Code, OpenCode, GitHub Copilot CLI. La web-ui ahora admite los seis: cinco se corresponden con proveedores activos existentes (Anthropic / Gemini / OpenAI / Qwen / OpenRouter), y GitHub Copilot CLI obtiene un conector dedicado a GitHub Models — runGitHubModels (compatible con OpenAI-compatible; un PAT de GitHub con el alcance models), configurable en #/config (GITHUB_MODELS_API_KEY + GITHUB_MODELS_MODEL) y seleccionable mediante LLM_PROVIDER=github; 6.º en el orden auto. Los paquetes de ayuda y los READMEs ahora listan los seis canónicos (se renombró Qwen CLI→Qwen Code; se añadieron Gemini CLI + GitHub Copilot CLI), y el README incorpora una tabla completa de referencia de modos y enlaces a adaptadores de portales en career-ops.org/docs para que cada funcionalidad se pueda rastrear hasta el proyecto principal. tests/llm-provider-context.test.mjs extiende la matriz de límites de recuperación a los seis proveedores (cv.md + profile.yml integrados + artefacto devuelto); se añaden las nuevas claves GITHUB_MODELS_* a los 12 diccionarios de idiomas. Suite 1125 → 1126.
[1.73.0] — 2026-06-17
feat(llm): conector Gemini genérico + contexto CV/perfil verificado en todos los proveedores. Se añadió server/lib/gemini.mjs (runGemini) — un cliente Gemini generateContent sin dependencias externas que devuelve la misma forma {markdown, usage, error} que los clientes compatibles con Anthropic / OpenAI. Corrección: /api/mode/:slug y /api/deep anteriormente enrutaban sus prompts a través de gemini-eval.mjs, diseñado exclusivamente para ofertas, por lo que Gemini Run live devolvía una evaluación en lugar del artefacto solicitado (carta de presentación, alcance, resumen). Ahora llaman a runGemini con bundleProjectContext, de modo que cv.md + config/profile.yml se insertan en línea para Gemini exactamente igual que con cualquier otro proveedor — las cartas y resúmenes son detallados y personalizados. El nuevo tests/llm-provider-context.test.mjs simula el límite HTTP de cada proveedor y verifica que los cinco (Anthropic / Gemini / OpenAI / Qwen / OpenRouter) incluyan en línea cv.md + profile.yml y devuelvan el artefacto (matriz mode + deep + evaluate, 9 casos). /api/evaluate conserva su gemini-eval.mjs ajustado para ofertas. Suite 1116 → 1125.
[1.72.0] — 2026-06-17
feat(modes): Run live ahora devuelve el artefacto final directamente (contrato de salida de un solo disparo). Las plantillas padre modes/<slug>.md están escritas para sesiones interactivas de Claude Code — varias (cover, contacto, …) hacen una pausa para formular preguntas aclaratorias antes de producir el resultado, lo que hacía que el Run live de la interfaz web emitiera un cuestionario en lugar del artefacto. buildModePrompt ahora envuelve cada modo en un contrato de salida no interactivo: realiza el análisis (desglose de la oferta, notas sobre la empresa, palabras clave ATS, diferencias perfil↔oferta, elecciones de tono/enfoque) de forma silenciosa, elige valores predeterminados razonables de cv.md / config/profile.yml para todo lo que la plantilla normalmente preguntaría, y genera únicamente el artefacto final — cerrado con un recordatorio por modo «output ONLY {the cover letter / outreach message / …}». Así, al hacer clic en Run live sobre #/cover ahora se devuelve la carta de presentación en sí; la misma corrección se aplica a todos los modos genéricos (cover, contacto, interview-prep, project, training, followup, patterns) en los 12 idiomas (el artefacto se redacta en el idioma de la interfaz mediante la directiva de configuración regional). Suite 1103 → 1116.
[1.71.2] — 2026-06-17
docs(i18n): publica el pase de consistencia de documentación. El bloque “Translations of this guide” de cada README ahora lista los 11 idiomas hermanos (anteriormente algunos omitían English/Français o se enlazaban a sí mismos), con la línea en blanco antes del salto de sección restaurada. El prompt completo de regresión QA se renombra a la versión actual, y la documentación (CLAUDE.md, CONVENTIONS, LOCALIZATION, PROJECT-CONTEXT) se sincroniza con la versión actual y el recuento de pruebas (1103). Sin cambios en el código ni en el comportamiento — solo documentación, por lo que las traducciones de ayuda/UI y todas las funciones de 1.70.0–1.71.1 permanecen sin cambios.
[1.71.1] — 2026-06-17
fix(i18n): la guía de ayuda integrada está ahora completamente traducida en los 12 idiomas. Se añadieron docs/help/{pl,uk,ar}.md (cada uno con la estructura validada de 19 H2 / 75 H3) para que #/help sirva un paquete nativo en polaco, ucraniano y árabe en lugar de recurrir al inglés — GET /api/help/{pl,uk,ar} ahora devuelven su propia configuración regional. Integrado en todas las comprobaciones de ayuda (help-ui, help.test, help-ru-config-section, canonical-docs-coverage). También se completaron todas las listas de traducción en 12 idiomas: el bloque «Translations of this guide» del README (9 READMEs), las cabeceras «Translations:» de los CHANGELOG localizados (8 archivos) y se actualizaron los recuentos de documentación obsoletos. Suite 1100 → 1103.
[1.71.0] — 2026-06-16
feat(cover): genera un PDF de carta de presentación directamente desde #/cover. El modo cover (añadido en v1.70.0) produce el texto de la carta; el resultado ofrece ahora un botón Generate PDF que lo renderiza a través del pipeline compartido de markdown→PDF en línea (POST /api/stream/pdf/inline → generate-pdf.mjs), la misma ruta que usa interview-prep. Ahora puedes generar la carta y producir un PDF sin salir del SPA.
test/docs: endurecimiento de revisión de v1.70.0. Se añadió cobertura CI-aislada para el modo cover (lista de permisos + ensamblado de prompt), el selector <select> de banderas + RTL árabe (dirFor/<html dir>), top.langLabel en cada locale, el cableado del PDF de carta de presentación, y la directiva de locale de prompts.mjs + scaffolding para fr/pl/uk/ar. Se actualizaron las referencias obsoletas «todos los 8» → 12 locales en docs/sdd/CONVENTIONS.md y el prompt de regresión QA del proyecto completo.
[1.70.0] — 2026-06-16
feat(i18n): tres nuevos idiomas de interfaz — polaco (pl), ucraniano (uk) y árabe (ar, con RTL completo) — llevando la SPA a 12 idiomas, que coinciden con todos los idiomas del README del proyecto padre career-ops. Cada nuevo idioma incluye un diccionario completo de 697 claves (public/js/lib/locales/i18n-dict.{pl,uk,ar}.js), validado por las suites existentes de paridad / cobertura / sin-fugas-latinas / sin-datos-personales. El árabe añade soporte genuino de derecha a izquierda: i18n.js establece <html dir="rtl"> para idiomas RTL y un bloque [dir="rtl"] con alcance limitado en app.css refleja la interfaz (barra lateral, cajón de notificaciones, tablas y citas en markdown, espaciado en línea) — los idiomas LTR permanecen byte a byte sin cambios. Nueva clave top.langLabel (×12) nombra el selector para lectores de pantalla.
feat(ui): el selector de idioma <select> con icono de bandera reemplaza la fila de botones envolventes. Con 12 idiomas, la antigua fila .lang-btn se desbordaba en tres líneas en la barra lateral; un <select> nativo (cada opción precedida por un emoji de bandera) escala limpiamente, es compatible con teclado y lectores de pantalla de serie, y se mantiene seguro con CSP (manejador de cambio mediante addEventListener, sin JS en línea). Las banderas degradan a letras de región donde la plataforma carece de glifos de bandera, por lo que la etiqueta del idioma es siempre el identificador principal.
feat(cover): se porta el modo de carta de presentación del proyecto padre (career-ops v1.10.0 + saludo de v1.11.0) a la SPA. Nueva página #/cover dentro del grupo de navegación de Solicitud, construida sobre el ejecutor de modos genérico: descripción del puesto + empresa/rol + un saludo opcional → una carta personalizada generada a partir de cv.md / modes/_profile.md. Se añadió cover a la lista MODE_ALLOWLIST del servidor y un bloque i18n cover.* (×12 idiomas).
chore(compat): seguimiento del proyecto padre career-ops v1.11.0. Se verificó que el contrato de lectura/escritura está intacto — data/applications.md sigue siendo la fuente de verdad en markdown (el índice de seguimiento SQLite de v1.11.0 es una caché derivada), las columnas del seguimiento siguen siendo mapeadas por encabezado. parentVersion ahora indica 1.11.0.
fix(i18n): se cierra una brecha latente donde el francés (añadido en v1.61.0) faltaba en server/lib/prompts.mjs en LOCALE_NAMES y SCAFFOLD_STRINGS — las llamadas LLM en francés caían silenciosamente a la salida en inglés y andamiaje en inglés. fr/pl/uk/ar están ahora todos conectados al camino de configuración regional del prompt.
Seguimientos conocidos: la guía de ayuda integrada (
docs/help/) vuelve al inglés para pl/uk/ar (la interfaz en sí está completamente localizada); la entrevista de incorporación interactiva del proyecto padre, el descubrimiento ATS inverso y los nuevos proveedores de escaneo aún no están disponibles en la SPA.
[1.69.2] — 2026-06-12
fix(test): corrige una fuga de aislamiento de tests que permitía que npm test sobrescribiera tus config/profile.yml y data/scan-history.tsv reales. tests/critical-fixes.test.mjs importaba prompts.mjs (→ paths.mjs) en la parte superior del archivo, así que PROJECT_ROOT se resolvía al directorio padre real antes de que before() fijara CAREER_OPS_ROOT a un directorio temporal — y PUT /api/profile filtraba la fixture «Acceptance Test» a tu perfil real en cada ejecución. Solución: cargar prompts.mjs mediante import() dinámico dentro de before(). Nuevo tests/test-root-isolation.test.mjs (2 casos) protege toda la suite frente a ese patrón. Sin cambios de código de producción. Suite 1084 → 1086.
[1.69.1] — 2026-06-12
fix(scan): #/scan ya no trunca silenciosamente los barridos regionales grandes. El conjunto mostrado por región estaba limitado a 500 (un escaneo RU real de 1352 ofertas coincidentes mostraba solo 500; 852 ocultas — el síntoma «2000 escaneadas, ~600 mostradas»). Ambos escáneres usan ahora una constante compartida y configurable por entorno MAX_STORED_RESULTS (por defecto 2000, anulable con SCAN_MAX_RESULTS). Solo afecta a la visualización: las adiciones a pipeline.md / scan-history.tsv ya usaban el conjunto sin recortar. fix(health/ui): las tarjetas de comprobación de #/health ya no se desbordan. Un nombre/valor largo chocaba con el botón Fix → y la insignia de estado; la fila ahora se encoge y se ajusta mediante .health-check-row. Nuevas pruebas scan-result-cap + health-card-overflow. Suite 1079 → 1084.
[1.69.0] — 2026-06-12
feat(scan): autodescubrimiento de adaptadores del escáner (P-14) — basta con dejar un .mjs en server/lib/sources/ para registrar una nueva fuente. Antes de v1.69, la lista de fuentes en server/lib/sources/registry.mjs era un arreglo estático mantenido a mano: añadir un adaptador exigía editar tanto <id>.mjs como registry.mjs. Cierra la mitad pendiente del ítem P-14 de la hoja de ruta (docs/ROADMAP.md). Ahora cada *.mjs de server/lib/sources/ se carga dinámicamente al arrancar el módulo; cada adaptador declara su identidad mediante un bloque autodescriptivo export const meta = { value, label, region, configKey? }. Los 12 adaptadores incluidos (ashby / greenhouse / lever / rss / smartrecruiters / workable / workday + geekjob / getmatch / habr / hh / trudvsem) reciben un meta; registry.mjs usa readdirSync + import() dinámico resuelto vía top-level await (estándar ESM Node 18+). La API pública (SOURCES, SOURCES_BY_REGION, RU_CONFIG_KEYS, getRegionalSources) no cambia: todos los imports existentes siguen funcionando. La validación rechaza meta mal formados y registra un console.warn por archivo problemático. Nuevo tests/sources-registry-discovery.test.mjs con 14 casos. Suite 1065 → 1079.
[1.68.2] — 2026-06-07
fix(bin): los verbos de la CLI vía npx / npm link estaban rotos — la ruta del bin ahora se resuelve a través de enlaces simbólicos. npm y npx exponen career-ops-ui como un enlace simbólico bajo node_modules/.bin/, donde el antiguo dirname "${BASH_SOURCE[0]}" apuntaba a .bin en lugar de a la raíz del paquete, por lo que npx career-ops-ui init ejecutaba node node_modules/scripts/init.mjs y fallaba con MODULE_NOT_FOUND (las ejecuciones locales tras npm install no se veían afectadas, lo que ocultaba el fallo). Ahora bin/career-ops-ui.sh y bin/start.sh canonizan SCRIPT_DIR a través de la cadena de enlaces (bucle readlink + cd -P), de modo que cada verbo funciona desde el repo, vía npm link y vía npx. Añade un bloqueo de regresión en tests/sh-files.test.mjs que ejecuta un verbo a través de un enlace simbólico estilo .bin. Suite 1065/1065.
[1.68.1] — 2026-05-29
fix(scan): timeout de fetch por fuente 10s → 60s. El fail-fast de 10s de v1.67.1 también cortaba tableros Ashby lentos pero vivos que solo necesitaban más tiempo. Sube el valor por defecto a un minuto para que esos respondan. Compensación: una fuente realmente muerta/colgada ahora ocupa una ranura de concurrencia durante los 60s completos (escaneo peor-caso más lento), y los que se cuelgan crónicamente (Perplexity, Supabase, Resend, …) probablemente sigan caducando — un arreglo por fuente / menor concurrencia de Ashby los resolvería bien. Override con SCAN_FETCH_TIMEOUT_MS. Suite 1063/1063.
[1.68.0] — 2026-05-29
feat(scan): panel de filtros de resultados rediseñado — campos con etiqueta, botón Aplicar, opción Presencial y un filtro de salario que funciona. Cada filtro de #/scan es ahora un campo con etiqueta (etiqueta encima del control, no un placeholder): Buscar · Tipo · Salario desde · Salario hasta · Fuente · Alcance. Un botón Aplicar explícito (más Restablecer, y Enter en cualquier campo) reejecuta el filtro; una pista en la página explica cómo funciona. El rango salarial ahora filtra de verdad — al fijar un valor desde/hasta, los empleos cuya remuneración queda fuera del rango y los empleos sin salario indicado se descartan (solape de rangos; se ignora la divisa). El filtro de Tipo gana una opción Presencial junto a Remoto / Híbrido / Reubicación. Nuevas claves i18n ×9; salaryInRange ahora estricto; suite 1063/1063.
[1.67.1] — 2026-05-29
fix(scan): timeout de fetch por fuente 30s → 10s (fail-fast). La subida a 30s de v1.67.0 solo recuperó ~la mitad de los tableros Ashby lentos; el resto (Perplexity, Supabase, Resend, DeepL, Ramp, …) se cuelgan sin importar el deadline, así que un timeout mayor solo estancaba cada escaneo esperando ranuras muertas. 10s falla rápido en los que se cuelgan crónicamente y mantiene los escaneos ágiles. Override con SCAN_FETCH_TIMEOUT_MS. Suite 1060/1060.
[1.67.0] — 2026-05-29
feat(scan): filtro de rango salarial (desde / hasta) en #/scan, y un timeout de fetch por fuente más largo. La tabla de resultados gana dos campos numéricos — salario desde / hasta — junto a los filtros de texto y remoto. El salario en texto libre de cada fila (от 100 000 до 200 000 ₽, 120000-150000 USD, $120K–$150K, …) se parsea a un rango numérico y se compara con semántica de rangos solapados; las filas sin salario publicado se conservan, así el filtro acota la lista en vez de vaciarla (la comparación ignora la divisa — sin conversión de cambio). También sube el timeout de fetch por fuente de 15s → 30s (override: SCAN_FETCH_TIMEOUT_MS): los payloads includeCompensation de Ashby tardaban habitualmente >15s con concurrencia ×8, así que ~30 tableros Ashby caían por timeout en cada escaneo. Nuevos window.Skills.parseSalaryRange/salaryInRange + i18n ×9; 13 pruebas nuevas; suite 1060/1060.
[1.66.0] — 2026-05-28
feat(scan): las fuentes RU ahora recorren TODAS las páginas, no solo la primera. hh.ru, Habr Career y Trudvsem solo paginaban los primeros ~50 resultados por consulta; ahora siguen la paginación hasta el final — &page=N para hh.ru/Habr, offset/meta.total para Trudvsem — deduplicando entre páginas y parando cuando una página no aporta nada nuevo (o en un tope de seguridad de 50 páginas). Una consulta como “Backend разработчик” devuelve ahora el conjunto completo (p. ej. hh.ru PHP 17 → 55+ en 3 páginas; Trudvsem devuelve los 72). Cada página conserva el timeout + AbortSignal. 4 pruebas nuevas; suite 1045/1045.
[1.65.0] — 2026-05-28
feat(scan): hh.ru ahora se scrapea desde su web pública en vez de la API JSON — funciona desde cualquier IP, sin proxy. api.hh.ru empezó a devolver un 403 forbidden a cualquier cliente programático sin importar la IP ni el User-Agent (un bloqueo anti-bot de borde). La web (hh.ru/search/vacancy) sí sirve resultados completos a cualquier cliente tipo navegador, así que el adaptador ahora parsea ese HTML (como Habr Career). Elimina la variable HH_PROXY de 1.64.0 y la dependencia undici — sin proxy, clave ni User-Agent. Tests reescritos para el parser HTML; suite 1041/1041.
[1.64.0] — 2026-05-27
feat(scan): enruta la petición a hh.ru por un proxy ruso mediante HH_PROXY. hh.ru bloquea su API por IP, no por User-Agent — por eso HH_USER_AGENT por sí solo nunca levantaba un 403 desde un nodo de salida no ruso. Define HH_PROXY con la URL de un proxy ruso HTTP/HTTPS (p. ej. http://user:pass@ru-host:port) y solo la petición a hh.ru pasa por él; las demás fuentes mantienen su conexión directa. Construido sobre el ProxyAgent de undici (nueva dependencia de runtime); el dispatcher se omite por completo cuando HH_PROXY no está definido. 3 pruebas nuevas; suite 1041/1041.
[1.63.2] — 2026-05-27
feat(scan): % de progreso en vivo + detalle por fuente en la consola de #/scan. La barra ahora es determinada — los escáneres emiten eventos de progreso (EN: por empresa; RU: por consulta) vía SSE, y la barra se llena con una etiqueta «Scanning… NN%» (la franja animada solo hasta el primer evento). El primer fallo de cada fuente (timeout / 403 / red) se registra en detalle en la consola; los repetidos se suprimen. 1 prueba nueva; suite 1040/1040.
[1.63.1] — 2026-05-27
style(scan): barra de progreso de #/scan más visible. El indicador ahora lleva un rótulo visible «Scanning…» y la barra pasó a 8px (antes 4px finos), claramente perceptible durante el escaneo. Sin cambios de comportamiento.
[1.63.0] — 2026-05-27
feat(scan): timeout por petición + barra de progreso en #/scan. Las peticiones de fuentes no tenían límite de tiempo, así que una fuente atascada (p. ej. api.hh.ru desde una IP bloqueada) podía colgar todo el escaneo. El nuevo server/lib/fetch-timeout.mjs envuelve el fetchImpl de los escáneres (makeTimeoutFetch, por defecto 15s, configurable con SCAN_FETCH_TIMEOUT_MS); una fuente expirada se registra como error no fatal y el escaneo continúa. #/scan muestra una barra de progreso durante el escaneo (scan.progress en las 9 localizaciones). 7 pruebas nuevas; suite 1039/1039.
[1.62.3] — 2026-05-27
docs: instalación aclarada (career-ops-ui corre dentro de career-ops/web-ui/) + solución de problemas de init, en las 9 localizaciones. Sección de instalación reescrita en Option 1 (un curl) / Option 2 (clonar la UI dentro de un proyecto career-ops existente como web-ui) + verbos CLI + configuración del proveedor + bloque Troubleshooting init. Nota de estructura anidada añadida a /help §1 Setup; resumen de toda la línea v1.62.* en el README. Solo documentación; sin cambios de código.
[1.62.2] — 2026-05-27
fix(help): el filtro de #/help ahora es de texto completo (encuentra subsecciones H3 como RSS). El filtro de búsqueda/TOC de la página de ayuda solo coincidía con títulos de sección H2, por lo que la documentación RSS de v1.62.x (un H3 bajo §5 Portals & sources) no se encontraba. Ahora el cuerpo de cada sección se indexa en el filtro, así que buscar p. ej. «RSS» muestra §5. Solo del lado del cliente; sin cambios de API.
[1.62.1] — 2026-05-27
feat(scan): RSS en el filtro de fuentes + corrección de ubicación RSS. El desplegable de filtro de fuentes en #/scan ahora incluye RSS (añadido a server/lib/sources/registry.mjs + la lista de respaldo del SPA), por lo que los resultados de portales RSS (LaraJobs, WeWorkRemotely, …) se filtran como cualquier fuente ATS. El adaptador RSS ya no asigna la etiqueta <category> del feed a location — esas etiquetas hacían que location_filter descartara erróneamente puestos remotos; ahora location queda vacío y los feeds pasan el filtro de ubicación. Tooltips/etiquetas del botón de escaneo y la cadena de lista de fuentes actualizadas en las 9 localizaciones (Workable / SmartRecruiters / Workday / RSS). Snapshot i18n y prueba del endpoint de fuentes (6 → 7 EN) actualizados.
[1.62.0] — 2026-05-27
feat(scan): adaptador RSS genérico para portales de empleo no-ATS. Un nuevo adaptador rss (server/lib/portals/adapters/rss.mjs + server/lib/sources/rss.mjs) permite al escáner extraer ofertas de cualquier feed RSS — LaraJobs, WeWorkRemotely, RemoteOK, golangprojects y otros portales fuera de Greenhouse/Ashby/Lever. Sin nuevas dependencias: el análisis del feed usa regex con soporte de CDATA y entidades HTML (títulos/empresas sin etiquetas, code points astrales decodificados de forma segura). Se activa por empresa con provider: rss / rss: / feed_url: en portals.yml, sin interceptar empresas ya emparejadas con ATS. ALL_ADAPTERS crece 6 → 7. 29 nuevas pruebas; documentado en las 9 localizaciones del README.
[1.61.1] — 2026-05-22
fix(i18n): localiza el title + aria-label del conmutador de tema en los 9 idiomas (MINOR-001). El botón de tema claro/oscuro (#theme-toggle) tenía title="Toggle theme" y aria-label="Toggle theme" codificados en index.html — el tooltip y el texto para lectores de pantalla nunca se traducían, en ningún idioma. Nueva clave top.themeToggle + un manejador data-i18n-title en applyI18n() (espejo del arreglo de aria-label de búsqueda de v1.58.15) localizan ambos atributos al arrancar y en cada cambio de idioma. Bloqueado por tests/playwright-theme-toggle-i18n.mjs (9 idiomas + cambio en runtime) y dos guardas estáticas. Único hallazgo LOW del sign-off de v1.61.0. (MINOR-001)
[1.61.0] — 2026-05-22
feat(i18n): añade el francés como 9.º idioma de la interfaz. Nuevo diccionario por idioma public/js/lib/locales/i18n-dict.fr.js (window.__I18N_DICT_FR), con paridad completa de 668 claves con el inglés; nuevo paquete de ayuda docs/help/fr.md (19 H2 / 73 H3, paridad estructural exacta con en). fr queda registrado en el conmutador de idioma y la autodetección del navegador (i18n.js), en el ensamblador (i18n-dict.js), en index.html (etiqueta <script> antes del ensamblador), en el snapshot de test y en todas las listas de locales de los tests. La tabla de traducción inicial proviene de la PR #9 (contribución de la comunidad). Sin cambios de lógica: t() y todas las vistas no cambian. 1001 / 1001 pruebas unitarias; el barrido de locales de Playwright crece a 9 subtests. (FR-LOCALE)
[1.60.0] — 2026-05-22
refactor(i18n): divide el megaarchivo de 8 columnas en archivos por idioma (I18N-SPLIT). El diccionario de traducciones vivía en un único public/js/lib/i18n-dict.js; ahora hay un archivo por idioma en public/js/lib/locales/ más i18n-dict.aliases.js, para que un traductor edite un solo idioma de forma aislada (estilo i18next / OpenWA). i18n-dict.js es ahora un ensamblador que reconstruye exactamente el mismo window.__I18N_DICT, así que t() y todas las vistas no cambian. Se carga de forma síncrona vía <script src> — sin paso de compilación ni fetch. Un snapshot demuestra que la migración no pierde nada (678 claves). Herramientas y ~25 tests adaptados; nuevos tests/i18n-locale-files.test.mjs y tests/playwright-locale-sweep.mjs (cada página × 8 idiomas en Chromium real). 994 → 1000 unitarios · 62 → 70 Playwright. Sin cambios de comportamiento. (I18N-SPLIT)
[1.59.13] — 2026-05-21
fix(i18n): colapsar claves duplicadas reales con @alias + purga final de datos personales. Nombre real del maintainer eliminado de fixtures de test y reportes QA (→ Jane Doe); LICENSE/package.json → handle Fighter90. Mecanismo @alias colapsa las 10 claves idénticas en los 8 locales; nav.config/config.title NO se fusionan (divergen en español). 991 → 994 tests. (I18N-CL3)
[1.59.12] — 2026-05-21
fix(i18n): limpieza de i18n-dict.js — pre-fr (I18N-CL1, I18N-CL2, I18N-CL4). Eliminado dato personal en training.coursePh (→ placeholder genérico), restaurado followup.lastPh como pista de formato (no fecha fija), añadido npm run audit:i18n. Los grupos de valores duplicados son intencionales (roles de UI distintos) — ver cabecera del diccionario. (I18N-CL1, I18N-CL2, I18N-CL4)
[1.59.11] — 2026-05-21
fix(test): v1.59.11 — la suite e2e-comprehensive ahora pasa 23/23 (era 11/23). Causa raíz: page.goto(baseUrl + '/#/X') es un no-op para cambios solo de hash en Playwright. El nuevo helper goRoute(hash) rebota por about:blank antes de cada goto y fuerza una navegación real. (e2e-harness-r1)
[1.59.10] — 2026-05-21
fix(api): NEW-F1-sub-r1 (v1.59.10) — middleware de .. crudo movido arriba de todas las rutas /api. El de v1.59.8 estaba después de app.all y nunca disparaba. Ahora corre antes de la normalización de Express. (NEW-F1-sub-r1)
[1.59.9] — 2026-05-21
fix(ux): UX-A5-r4 (v1.59.9) — marcador de debug data-toc-spy="active" + lock-test conductual del scroll-spy del Help TOC. Sexto ciclo: las 5 cerraduras anteriores pasaron las pruebas estáticas pero el bug persistía. v1.59.9 añade marcador, paint inicial síncrono, re-compute con doble rAF, listener de resize, y limpieza completa en hashchange. (UX-A5-r4)
[1.59.8] — 2026-05-21
fix(ux+api): v1.59.8 — UX-A5-r3 + NEW-F1-sub (HIGH + LOW agrupados). Excepción de doctrina autorizada por el reporte FINAL-REGRESSION-v1.59.7. UX-A5-r3: #/help cambia el IntersectionObserver por un listener scroll con rAF throttling — tras 4 ciclos de fallos del IO, ahora el scroll-spy es robusto en todos los modos de scroll. NEW-F1-sub: middleware que rechaza .. crudos en /api/* con 404 JSON. (UX-A5-r3 · NEW-F1-sub)
[1.59.7] — 2026-05-20
fix(api): NEW-D3-cache (v1.59.7) — GET /api/cv envía Cache-Control: no-store. El CV es el artefacto principal del usuario; siempre revalidar. (NEW-D3-cache)
[1.59.6] — 2026-05-20
feat(a11y): NEW-D2-motion (v1.59.6) — respeto a prefers-reduced-motion: reduce. Nuevo bloque @media neutraliza animaciones, transiciones y scroll-behavior. (NEW-D2-motion)
[1.59.5] — 2026-05-20
fix(api): NEW-F1 (v1.59.5) — /api/* desconocido devuelve 404 JSON en cada verbo. app.get → app.all. (NEW-F1)
[1.59.4] — 2026-05-20
fix(ui): NEW-OR1 (v1.59.4) — chip Active/Keys en #/config ahora libre de races. Construye los nodos antes del swap, token de in-flight, caché del último estado bueno. (NEW-OR1)
[1.59.3] — 2026-05-20
fix(ux): UX-A5-r2 (v1.59.3) — scroll-spy en #/help reforzado. rootMargin ampliado de 10 % a 25 % de banda visible + estado inicial calculado al montar. (UX-A5-r2)
[1.59.2] — 2026-05-20
fix(ui): v1.59.2 — chip Active/Keys: cuenta correcta, nombre del proveedor capitalizado, sin solapamiento. (post-v1.59.1 hotfix)
[1.59.1] — 2026-05-20
fix(test): v1.59.1 — NEW-D1 guard acepta el copy ES pulido en UX-A11. Relajada la regex que bloqueaba pipe.title[es] a vacantes. Ahora también acepta candidaturas. (v1.59.1)
[1.59.0] — 2026-05-20
feat(ui): UX-A14 (v1.59.0) — Pase de auditoría mobile (≤ 420 px). Cinco correcciones en un nuevo bloque @media (max-width: 420px): card-row 1-up, hero CTAs apilados, page-header apilado, qa-grid floor 160 px, api-keys summary compacto. (UX-A14)
[1.58.65] — 2026-05-20
test(ui): UX-A2 (v1.58.65) — test de bloqueo del field-form estructurado de Modes. Nuevo test que protege la implementación v1.54.3 contra regresiones. (UX-A2)
[1.58.64] — 2026-05-20
fix(i18n): UX-A11 (v1.58.64) — pulido del copy en español. eval.subtitle ahora usa ajuste del CV, Puntaje, cabecera, informe. pipe.title ahora es Pipeline de candidaturas. (UX-A11)
[1.58.63] — 2026-05-20
fix(ui): UX-A15 (v1.58.63) — tile Pipeline del Dashboard con peso visual primario. El tile Pipeline ahora destaca con borde de acento, ícono más grande y etiqueta en negrita. (UX-A15)
[1.58.62] — 2026-05-20
feat(ui): UX-A9 (v1.58.62) — chip sticky de resumen en la pestaña API keys. #/config → API keys ahora muestra arriba un chip sticky con el proveedor activo y el conteo de claves configuradas. (UX-A9)
[1.58.61] — 2026-05-20
docs(readme): UX-A8 (v1.58.61) — sección de limpieza para primera ejecución añadida en los 8 READMEs. Ahora se documenta el paso make clean-test-fixtures para purgar las dos URLs fixture QA antes del primer scan. (UX-A8)
[1.58.60] — 2026-05-20
feat(ui): UX-A12 (v1.58.60) — Cajón de notificaciones con Borrar todo + descartar por entrada. Nuevo botón global y × por entrada en el panel de notificaciones. (UX-A12)
[1.58.59] — 2026-05-20
feat(ui): UX-A13 (v1.58.59) — CTA accionable «Fix →» en filas de salud que fallan. Las filas con FAIL/OPTIONAL ahora muestran un botón ghost que enlaza directamente con la pestaña de configuración correspondiente. (UX-A13)
[1.58.58] — 2026-05-20
fix(ux): UX-A10 (v1.58.58) — protección contra perder edición no guardada en #/cv. Ahora beforeunload (cierre del navegador) y hashchange (navegación SPA) muestran confirmación localizada antes de abandonar la página con buffer sucio. (UX-A10)
[1.58.57] — 2026-05-20
test(ui): UX-A7 (v1.58.57) — bloqueo de regresión sobre el contrato de refresco automático del cost-line. Nueva prueba estática asegura que el evento providers-changed se despacha, se suscribe y que todas las vistas de asesor llaman a UI.providerCostHint. (UX-A7)
[1.58.56] — 2026-05-20
fix(a11y): UX-A4 (v1.58.56) — .lang-btn cumple el tamaño mínimo de objetivo táctil WCAG 2.5.8. Antes los botones de idioma medían 23–25 px de alto, por debajo del piso de 24×24 px. Ahora min-height: 28px + min-width: 28px garantizan el cumplimiento WCAG 2.2 AA. (UX-A4)
[1.58.55] — 2026-05-20
feat(ui): UX-A3 (v1.58.55) — chip de proveedor activo en el Dashboard. El hero de #/dashboard ahora muestra qué proveedor LLM está activo (⚡ Live evals: Anthropic claude-sonnet-4-6 o 📋 Manual prompt mode). Se actualiza automáticamente al cambiar LLM_PROVIDER en #/config y al recuperar foco en la pestaña. (UX-A3)
[1.58.54] — 2026-05-20
fix(ux): UX-A1 (v1.58.54) — aviso defensivo de estructura para el brief de Deep. Cuando el brief guardado no incluye al menos 3 de las 6 secciones canónicas (Company snapshot / Engineering culture / Recent news / Glassdoor / Interview process / Negotiation leverage), public/js/views/deep.js antepone un aviso no bloqueante con enlace a la referencia. Solución a nivel de UI; la corrección en el prompt vive en el proyecto padre. (UX-A1)
[1.58.53] — 2026-05-20
fix(ux): UX-A6 — todo saved-card pasa por un único helper renderSavedCard(). Garantiza la estructura <span>+<time> en cada path de renderizado. 948 → 949 unitarios. (UX-A6)
[1.58.52] — 2026-05-20
fix(ux): UX-A5 — scroll-spy del TOC en #/help ahora dispara correctamente. Regresión de v1.58.45 (setTimeout(0) era demasiado temprano). Fix: refs directas a headings + doble requestAnimationFrame. 947 → 948 unitarios. (UX-A5)
[1.58.51] — 2026-05-20
chore(docs): v1.58.51 — limpieza final del ciclo v1.58.37 → v1.58.50 (14 releases). Sin cambios de código. qa/ reorganizado (todo lo version-locked en archive/v158-cycle/); 6 perennials en raíz. REGRESSION-FINAL §13 documenta cada invariante v1.58.37→.50. UX-AUDIT-PROMPT.md extendido. Baseline sin cambios (947/947). (housekeeping)
[1.58.50] — 2026-05-20
docs: DOC-1 — qa/REGRESSION-FINAL.md añade §5a documentando que los cuerpos de error del servidor son inglés-por-política. Cierra NEW-D4 como not-a-finding. Recomendación A del spec. Cierra la cola v1.58.37 → v1.58.50 de FIX-PROMPT-FINAL-EXHAUSTIVE.md (14 releases). 946 → 947 unitarios. (DOC-1)
[1.58.49] — 2026-05-20
chore(tooling): TOOL-1 — make clean-test-fixtures + script para eliminar líneas example.com de data/pipeline.md del proyecto padre. Soporta --dry-run. 4 tests CI-isolated. 942 → 946 unitarios. (TOOL-1)
[1.58.48] — 2026-05-20
fix(ux/onboarding): UX-D-B — banner global en #/dashboard cuando el perfil sigue en el template por defecto. Nuevo profileFixtureBanner() que muestra .hero-banner--warning al detectar Profile customized: false en /api/health. Nuevas claves i18n onboarding.fixtureWarning + onboarding.fixProfile × 8. 941 → 942 unitarios. (UX-D-B)
[1.58.47] — 2026-05-20
fix(ux/naming): UX-D-C — el botón “Quick scan” de la barra superior pasa a llamarse Abrir Scan para reflejar que solo navega (no inicia un scan). Actualizado en las 8 locales. 940 → 941 unitarios. (UX-D-C)
[1.58.46] — 2026-05-20
fix(ux): UX-D-D — checklist de #/apply sustituye {company}-{role} por slugs derivados del URL/JD. Antes los placeholders se mostraban literalmente. Nuevas funciones extractSlugs + substitutePlaceholders reconocen Greenhouse/Lever/Ashby/Workable/SmartRecruiters/Workday. Fallback [company]/[role]. 939 → 940 unitarios. (UX-D-D)
[1.58.45] — 2026-05-20
fix(ux): UX-D-K — scroll-spy en el TOC de #/help resalta la sección actual. IntersectionObserver aplica .toc-current al enlace cuyo H2 está en la banda de lectura. 938 → 939 unitarios. (UX-D-K)
[1.58.44] — 2026-05-20
fix(ux): UX-D-L — el brief abierto en Saved-research de #/deep tiene un botón × para cerrar. Antes no había forma de cerrar el brief sin desplazarse o navegar. Nueva clave deep.closeBrief × 8. 937 → 938 unitarios. (UX-D-L)
[1.58.43] — 2026-05-20
fix(ux): UX-D-F — submit vacío en #/evaluate muestra toast localizado distinto. Antes confundía vacío con “demasiado corto”. Nueva clave eval.emptyJd × 8. 936 → 937 unitarios. (UX-D-F)
[1.58.42] — 2026-05-20
fix(ux): UX-D-J — paridad del chip de ETA en todas las páginas de advisor. Antes solo #/auto mostraba “⏱ ~1–2 min”. Ahora también #/evaluate, #/deep, y las 5 mode-pages muestran ⏱ ~30s (clave advisor.eta × 8). 935 → 936 unitarios. (UX-D-J)
[1.58.41] — 2026-05-20
fix(ux/truthfulness): UX-D-I — la cost-hint vuelve a consultar al cambiar de pestaña + en evento providers-changed. Antes solo se cargaba una vez; ahora se re-fetch via visibilitychange + un CustomEvent que dispara #/config al guardar. 934 → 935 unitarios. (UX-D-I)
[1.58.40] — 2026-05-20
fix(ux/docs): UX-D-H — regression-lock para que toda URL career-ops.org/docs/... visible siga siendo clickeable. Nueva tests/external-doc-links.test.mjs valida views/.js y docs/help/.md. 932 → 934 unitarios. (UX-D-H)
[1.58.39] — 2026-05-20
fix(ux): NEW-D2 — botón Refresh en el header del panel con feedback explícito. Distinto del Refresh del banner de conexión; este re-fetch in-place sin recargar la página. 2 nuevas claves i18n. 931 → 932 unitarios. (NEW-D2)
[1.58.38] — 2026-05-20
fix(a11y): NEW-D3 (WCAG 4.1.2) — input de búsqueda de #/tracker con aria-label localizado distinto del placeholder. Antes solo había placeholder; SR no escuchaban el propósito. Nueva clave track.searchAria × 8 idiomas, distinta del placeholder. 930 → 931 unitarios. (NEW-D3)
[1.58.37] — 2026-05-20
fix(i18n): NEW-D1 — H1 de #/pipeline localizado en es/pt-BR/ru + 2 fugas RU corregidas. pipe.title en es ahora Pipeline de vacantes; nuevo tests/i18n-no-latin-leaks.test.mjs que también atrapó ru.contacto.title y ru.health.title. 928 → 930 unitarios. (NEW-D1)
[1.58.36] — 2026-05-20
chore(docs): v1.58.36 — limpieza completa al cerrar el ciclo v1.58.x. Sin cambios de código. (1) qa/: 3 snapshots versionados (REGRESSION-END-TO-END-v1.58.16/33/35.md) archivados en qa/archive/v158-cycle/. (2) REGRESSION-FINAL.md añade §12 con todos los invariantes v1.58.4 → v1.58.35. (3) UX-AUDIT-PROMPT.md extendido con 30 filas cerradas. (4) docs/architecture/ actualizado (FRONTEND drawer, TESTING totales 928/62/20/23). (5) CLAUDE.md gana sección “Lecciones difíciles del ciclo v1.58.x”. (6) README ×8 con nueva fila “Notificaciones 🔔” + conteo de tests corregido. Baseline sin cambios. (housekeeping)
[1.58.35] — 2026-05-20
fix(ui): v1.58.35 — el drawer de notificaciones ya no se auto-abre + nueva §18 “Notificaciones” en la ayuda. Bug v1.58.34: .notif-drawer { display: flex } ganaba contra UA [hidden] { display: none }. Fix con .notif-drawer[hidden] { display: none } explícito + sólo se abre al hacer clic en el sino. Nueva §18 en las 8 traducciones de la ayuda con la tabla de categorías. 927 → 928 unitarios. (reporte de usuario)
[1.58.34] — 2026-05-20
feat(ui): v1.58.34 — Drawer de notificaciones (cierra U-13 por completo). Sobre la captura de v1.58.33: nueva API UI.onToast(fn) (pub/sub), campana 🔔 en la top-bar con badge de no leídos, drawer derecho con título/vacío/items localizados (notif.* × 8). Esc + cerrar + click en la campana cierran. 926 → 927 unitarios. (U-13 follow-up)
[1.58.33] — 2026-05-20
fix(ux): U-13 + U-14 + U-15 — diario de toasts (cap 50 + UI.getToastHistory()) + selector de seguridad para .page-header h1 + p + indicador de cambios sin guardar en #/cv. Cierra el ciclo v1.58.x. Nueva clave i18n cv.unsaved × 8 idiomas. 925 → 926 unitarios. (U-13/U-14/U-15)
[1.58.32] — 2026-05-20
fix(ux): U-12 — la barra de filtro del TOC de ayuda gana min-width: 16ch para que los placeholders KO/JA no se recorten. Clase .help-toc__filter añadida. 924 → 925 unitarios. (U-12)
[1.58.31] — 2026-05-20
fix(ux): U-11 — el encabezado Legitimacy del Tracker ahora muestra un chip de info ⓘ con tooltip explicando la escala (Alta/Precaución/Sospechosa). Nueva clave i18n track.col.legitimacy.help × 8 idiomas. 923 → 924 unitarios. (U-11)
[1.58.30] — 2026-05-20
fix(ux): U-10 — los botones Normalizar / Dedup / Merge del Tracker se desactivan cuando data/applications.md está vacío. Tooltip localizado (track.fixEmpty × 8 idiomas) explica que hay que añadir filas primero. 922 → 923 unitarios. (U-10)
[1.58.29] — 2026-05-20
fix(ux): U-9 — la fila contador ↔ filtro de #/pipeline se apila verticalmente en ventanas estrechas. Nueva clase .pipeline-controls + @media (max-width: 720px) que estira el filtro a 100% del ancho. 921 → 922 unitarios. (U-9)
[1.58.28] — 2026-05-20
fix(ux): U-8 — el bloque del prompt generado ahora está colapsado por defecto en las 7 páginas de modo. Envuelto en <details class="prompt-block">; el resumen muestra “Show prompt (N lines)” localizado (prompt.show / prompt.lines × 8). Copy + Run-live se mantienen visibles. 920 → 921 unitarios. (U-8)
[1.58.27] — 2026-05-20
fix(ux): U-7 — los divisores ASCII === de verify-pipeline.mjs se eliminan del modal de resultado. Regex ^={10,}$ aplicada en el handler antes de renderizar. 919 → 920 unitarios. (U-7)
[1.58.26] — 2026-05-20
fix(ux): U-6 — el chip de #/scan “Active companies N/M” ahora explica N vs M con tooltip + aria-label. Nueva clave scan.activeCo.help × 8 idiomas. 918 → 919 unitarios. (U-6)
[1.58.25] — 2026-05-20
fix(ux/ia): U-5 — Dashboard deduplica CTAs (eliminados el botón Open Pipeline del header y la tarjeta Scan all sources). El sidebar y el héroe ya cubren ambas rutas; las 4× Pipeline / 4× Scan que detectó la QA v1.58.3 quedan reducidas a 2× cada una. 917 → 918 unitarios. (U-5)
[1.58.24] — 2026-05-20
fix(ux): U-4 — los toasts de error tucan el postfijo “(MÉTODO /ruta · HTTP NNN)” dentro de un <details> colapsado. El postfijo técnico sigue presente en el DOM (invariante de BUG-006), pero el titular humano queda limpio. Nueva clave i18n toast.details × 8 idiomas. 916 → 917 unitarios. (U-4)
[1.58.23] — 2026-05-20
fix(ux): U-3 — el placeholder de lastContact en #/followup ahora se calcula como hoy − 14 días. Antes el placeholder era el ISO congelado 2026-04-21 y envejecía mal; ahora se computa en render via new Date() con setDate(getDate() - 14). 915 → 916 unitarios. (U-3)
[1.58.22] — 2026-05-20
fix(ux): U-2 — el H1 de #/auto ya no se rompe a 2 líneas por el ✨ inicial. Antes el auto.title contenía ✨ Auto-pipeline para una URL; el emoji participaba en el wrap. Se separa el emoji en un <span class="page-icon" aria-hidden="true"> y .page-header--icon usa display: grid. 914 → 915 unitarios. (U-2)
[1.58.21] — 2026-05-20
fix(ux): U-1 — H1 + subtítulo de #/cv ahora coinciden con el resto de páginas (supersede UX-9 v1.56.0 por diseño). El chip .cv-breadcrumb se eliminó; el header de #/cv ahora usa <h1 class="page-title"> + <p class="page-subtitle">. El invariante de UN solo <h1> se mantiene. 913 → 914 unitarios. (U-1)
[1.58.20] — 2026-05-20
fix(i18n/platform): I-6 — el atajo en el pie de barra muestra ⌘K en Mac y Ctrl+K en otros sistemas, con el verbo localizado. Antes era el literal CTRL+K — search en todas las plataformas/idiomas. top.langhint ahora usa {hotkey} — buscar; applyFooterHotkey() sustituye {hotkey} por la combinación nativa según navigator.platform. 915 → 916 unitarios. (I-6)
[1.58.19] — 2026-05-20
fix(i18n): I-4 — #/followup ruso ya no filtra cadence / follow-up. Las cadenas RU del modo followup (H1, hints) usaban cadence, follow-up, scope, timeline. Reemplazadas por equivalentes nativos rusos. 914 → 915 unitarios. (I-4)
[1.58.18] — 2026-05-20
fix(i18n): I-3 — ítems 2/5/13/14 del TOC de ayuda sin restos de inglés en locales no latinos. Algunos bundles de ayuda aún mostraban ## 2. App settings & API keys, ## 5. Portals & Sources, ## 13. Mode prompts, ## 14. Apply checklist (ru/ja/ko/zh-CN/zh-TW). Ahora totalmente localizados en los 8 idiomas. 913 → 914 unitarios. (I-3)
[1.58.17] — 2026-05-20
fix(i18n): I-2 — fechas de Saved-research ahora usan Intl.RelativeTimeFormat por locale. El helper formatRelative() en public/js/views/deep.js devolvía today / 1d ago / Nd ago en inglés en todos los idiomas. Sustituido por Intl.RelativeTimeFormat(I18n.getLang(), { numeric: 'auto' }) — la cadena nativa del navegador (hoy/ayer, сегодня/вчера, 今日/昨日, etc.). Fechas > 7 días caen a Intl.DateTimeFormat(locale, { dateStyle: 'medium' }). 912 → 913 unitarios. (I-2)
[1.58.16] — 2026-05-20
fix(ui): parpadeo de hover en botones de marca (reportado por usuario). Causa: el fondo por defecto de .btn-primary / .btn-danger era un linear-gradient(...) y el :hover lo reemplazaba por un color sólido — CSS no puede interpolar gradiente↔sólido, así que la transición de 180 ms chasqueaba y se veía un parpadeo blanco/rosa. Solución en public/css/app.css: mantener el gradiente en hover y oscurecer con filter: brightness(0.92) — filter se interpola limpiamente. La lista de transition de .btn añade filter var(--transition) para que el oscurecimiento se anime. 911 → 912 unitarios. (reporte de usuario)
[1.58.15] — 2026-05-20
fix(a11y/i18n): I-1 — aria-label y <label> oculto del buscador superior ahora se localizan. Antes los lectores de pantalla en cualquier idioma escuchaban el aria-label inglés. Nuevo hook genérico data-i18n-aria-label en public/js/app.js — applyI18n() actualiza aria-label en cada cambio de idioma como ya lo hace para data-i18n y data-i18n-placeholder. Dos nuevas claves i18n (top.search.aria, top.search.label) en los 8 idiomas. El hook es reutilizable para cualquier control futuro. 910 → 911 unitarios. (I-1)
[1.58.14] — 2026-05-20
fix(ux): M-9 — el botón Actualizar del banner de conexión ahora da feedback (antes era una recarga silenciosa). Hasta v1.58.13 el handler llamaba directamente a location.reload(). Ahora muestra un toast Actualizando…, marca sessionStorage['refreshedToast'], desactiva el botón para evitar dobles clics, y difiere la recarga 200 ms para que el toast pinte. En el arranque siguiente, app.js detecta la marca y emite un toast de éxito Actualizado. 2 nuevas claves i18n (common.refreshing, common.refreshed) en los 8 idiomas. 909 → 910 unitarios. (M-9)
[1.58.13] — 2026-05-20
fix(ux): M-8 — el checklist de #/apply ahora es interactivo. Antes de v1.58.13, ▶ Generar checklist mostraba los ítems 0…7 como un bloque monoespaciado <pre> no editable. Ahora cada ítem se renderiza como <input type="checkbox"> real, envuelto en <label> (área de clic ≥44 px, WCAG 2.5.5). El estado se persiste por URL en localStorage['applyChecklist:'+slug] — marca 3 ítems, recarga, los 3 siguen marcados. Botones: Copiar sin marcar (copia los ítems abiertos como bullets - markdown) y Restablecer. 5 nuevas claves i18n (apply.checklist.copyUnchecked, resetBtn, copied, copyFailed, reset) en los 8 idiomas. Fallback defensivo si el parser no encuentra ítems. 908 → 909 unitarios. (M-8)
[1.58.12] — 2026-05-20
fix(ux): M-7 — la línea de coste ahora sigue al proveedor activo (OpenRouter ya no cae en un número fabricado). UI.providerCostHint() ya consultaba /api/status/providers, pero los mapas por proveedor en public/js/api.js sólo listaban anthropic/gemini/openai/qwen. Tras v1.57.0 con OpenRouter como 5º proveedor, éste caía al genérico 0,03 y mostraba el literal openrouter en minúsculas. Ahora EST incluye openrouter: null (el router elige el modelo — el coste varía), y la rama === null emite cost varies (router picks) localizado en lugar del falso ~$0.03/eval. NAME añade openrouter: 'OpenRouter'. Nueva clave i18n cost.varies en los 8 idiomas. 907 → 908 unitarios. (M-7)
[1.58.11] — 2026-05-20
fix(ux): M-4 — el espaciado entre título y fecha en la tarjeta de investigación guardada ahora es CSS estructural (antes margen inline). La regresión MASTER de v1.58.3 verificó que algunas tarjetas mostraban software-engineer-generaltoday (sin espacio entre título y fecha), mientras otras estaban bien — el código previo dependía de style="margin-left: 8px" entre dos <span> sueltos, que colapsaba en ciertos casos. Corrección en public/js/views/deep.js — sustituye los dos <span> por .saved-card__title + un <time class="saved-card__date" datetime="…"> semántico, envueltos en un contenedor flex .saved-card. El espaciado ahora lo controla gap: var(--space-2, 8px), no puede colapsar (y se gana semántica a11y/SEO con <time>). 906 → 907 unitarios. (M-4)
[1.58.10] — 2026-05-20
fix(ux): M-2 — descartar el toast de progreso antes de abrir cualquier modal de resultado. Hacer clic en sync-check en #/cv dejaba el toast “Running cv-sync-check.mjs…” abajo a la derecha mientras se abría el modal de resultado — ambos compitiendo por la atención y, en pantallas estrechas, solapándose visualmente. Los botones Doctor / verify-pipeline de la página Health ya llamaban a UI.dismissToast() explícitamente antes de UI.modal(); el sync-check de cv.js era el único punto de entrada que lo omitía. Corrección en public/js/api.js — UI.modal() ahora invoca dismissToast() como primera sentencia, cubriendo cualquier futuro punto de entrada (defensa en profundidad). Además, las cadenas de cv.js se localizaron mediante t('cv.syncCheckRunning') y t('cv.syncCheck') (invariante BUG-008: título del modal == etiqueta localizada del botón). Dos nuevas claves i18n añadidas en los 8 idiomas. 905 → 906 unitarios. (M-2)
[1.58.9] — 2026-05-20
fix(a11y): M-1 — restablecer un anillo visible de :focus-visible en los campos de formulario (WCAG 2.4.7 Nivel AA). La regresión MASTER de v1.58.3 confirmó que getComputedStyle(focusedInput) devolvía outline: rgb(255,255,255) none 1.5px — la palabra clave none colapsaba el anillo a 0 px en cada campo. Causa raíz: las reglas base .input, .textarea, .select { outline: none } y .searchbar input { outline: none } tenían mayor especificidad que el *:focus-visible global y anulaban silenciosamente el anillo de teclado en 88 elementos por página. Corrección en public/css/app.css — añadidas reglas explícitas .input:focus-visible/.textarea:focus-visible/.select:focus-visible y .searchbar input:focus-visible con outline: 2px solid var(--rausch) + sombra translúcida; el foco de ratón sigue limpio (usa :focus, no :focus-visible). 904 → 905 unitarios (tests/qa-report-fixes.test.mjs guarda estática); Playwright 60 → 61 (tests/playwright-smoke.mjs Tab-traversal). (M-1)
[1.58.8] — 2026-05-20
feat(health): mostrar OPENAI_API_KEY, QWEN_API_KEY, OPENROUTER_API_KEY en #/health (igual que GEMINI_API_KEY). v1.57.0 sumó OpenRouter como 5º proveedor live-eval; v1.55.3 (UX-2) añadió el onboarding de 4 proveedores. La página #/health solo reportaba GEMINI_API_KEY y ANTHROPIC_API_KEY — los otros tres quedaban invisibles aunque /api/status/providers ya los enrutaba. Petición del usuario: extender el mismo patrón “set / unset (manual mode)” a cada proveedor headless. server/lib/routes/health.mjs ahora añade tres filas adicionales de checks opcionales, conectadas al mismo isUsableKey (hasOpenAIKey(), hasQwenKey(), hasOpenRouterKey() ya estaban importadas pero sin usar). El texto “manual mode” coincide con la fila GEMINI en los 8 idiomas — la vista Health itera sobre body.checks por lo que no se requiere cadena por locale. 903 → 904 unitarios. (Solicitud del usuario)
[1.58.7] — 2026-05-20
fix(security): NEW-2 — isValidJobUrl ahora rechaza las sintaxis emparejadas de plantillas (${…}, {{…}}) para coincidir con el mensaje de error. El 400 a nivel de ruta de POST /api/pipeline anuncia “contain no script or template characters”, pero la regresión MASTER de v1.58.3 confirmó que solo <%…%> estilo ASP/EJS estaba realmente bloqueado (efecto colateral del filtro [<>"'\\s]). Las plantillas JS (${TEST}) y Mustache/Handlebars ({{TEST}}) pasaban — un desfase semántico regex↔mensaje. Opción A del fix-prompt (endurecer la regex para coincidir con el mensaje; ligero refuerzo contra inyección por templating en URL): nuevo TEMPLATE_PATTERNSen [server/lib/security.mjs](https://github.com/Fighter90/career-ops-ui/blob/main/server/lib/security.mjs) consultado víahasTemplatePlaceholder(url)antes denew URL(…). Solo se rechazan placeholders **emparejados** — {normal}` (token ATS legítimo) sigue aceptándose. 901 → 903 unitarios. (NEW-2)
[1.58.6] — 2026-05-20
fix(a11y/i18n): BUG-008-tb — el título del modal del botón Doctor en la barra superior ahora coincide con la etiqueta localizada. La regla BUG-008 (cerrada en v1.58.0) exige “título del modal == etiqueta localizada del botón”. La regresión MASTER de v1.58.3 detectó que el punto de entrada de la barra superior seguía violando la invariante: hacer clic en Doctor abría un modal con título doctor (inglés en minúsculas), independientemente del idioma. Corrección en public/js/app.js:118 — sustituir el literal 'doctor' por I18n.t('top.doctor', 'Doctor'). La clave top.doctor ya existe en los 8 idiomas (EN Doctor · ES/pt-BR Diagnóstico · KO 진단 · JA 診断 · RU Диагностика · zh-CN 诊断 · zh-TW 診斷) y es la misma que declara el botón vía data-i18n="top.doctor". Guardia estática añadida en tests/qa-report-fixes.test.mjs. 900 → 901 unitarios; Playwright 60/60. (BUG-008-tb)
[1.58.5] — 2026-05-20
fix(ui): NEW-3 — #/followup Run-live doble-POST triado no-reproducible; bloqueado con guardia de regresión Playwright. La regresión MASTER de v1.58.3 observó (mediante window.fetch parcheado) dos POSTs idénticos a /api/mode/followup en ~2 s tras un único clic en Run live en #/followup (con empresa/rol/notas rellenos y fecha vacía). Siguiendo la doctrina “reproducir primero” del fix-prompt, la inspección de public/js/views/mode-page.js::submit() muestra: (a) Run live y Generate prompt son <button> planos con un único onClick cada uno — no hay <form> padre ni addEventListener('submit') que dispare dos veces, y (b) UI.withSpinner() (FIX-L1) marca button.disabled = true mientras la petición está en vuelo, bloqueando un segundo clic físico en origen. Una nueva prueba Playwright en tests/playwright-smoke.mjs recorre la receta exacta de la regresión — rellena empresa/rol/notas, deja la fecha vacía, hace clic en el botón manual (que comparte la función submit() con Run live) y verifica exactamente un POST /api/mode/followup en una ventana de 3 s. Selector estable entre locales (el glifo ▶ es idéntico en los 8 idiomas) y addInitScript siembra career-ops-ui:lang=en para que una prueba previa de idioma en el mismo contexto del navegador no perturbe los selectores. Playwright 59 → 60. La observación QA original queda como receta; no requiere cambio de código en producción. (NEW-3)
[1.58.4] — 2026-05-19
fix(security): NEW-1 — enviar Content-Security-Policy en cada respuesta (antes limitado a no-loopback). Antes de v1.58.4 la cabecera CSP solo se añadía cuando isPubliclyExposed() era verdadero (HOST fuera de loopback); sobre 127.0.0.1 tanto / como /api/health devolvían sin CSP, dejando el contrato escape-first de UI.md() como única defensa XSS. La regresión MASTER de v1.58.3 (§5) lo marcó como invariante crítica. Ahora la CSP es incondicional e idéntica en cada respuesta: default-src 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'. script-src nunca permite 'unsafe-inline'/'unsafe-eval'. El conjunto de directivas no cambia respecto a la política anterior (ya correcta para la SPA — Google Fonts en lista blanca para Inter), sin regresión visual ni funcional. Se reescribió tests/security-headers.test.mjs; un recorrido Playwright (en/ru/ja/zh-TW × 7 rutas) verifica 0 violaciones de CSP. 900 unitarios · Playwright 58→59 · e2e 20/20+23/23. Los siguientes elementos del fix-prompt se publican como versiones one-fix posteriores. (NEW-1)
[1.58.3] — 2026-05-19
fix(deep): R-2 / FIX-C1 — elimina etiquetas de andamiaje HUÉRFANAS / desbalanceadas del output de investigación. cleanLlmMarkdown (v1.58.0) solo quitaba bloques emparejados y una etiqueta abierta colgante. Una regresión profunda de v1.58.2 halló un modelo con traza desbalanceada — un </tool_response> huérfano (y </thinking>) sin apertura — que sobrevivía y se renderizaba literal en el brief guardado de #/deep. Un barrido conservador final elimina ahora cualquier token de andamiaje suelto (abierto o cerrado), el XML de herramientas de Anthropic (<invoke>/<parameter>/antml:*) y bloques tool_*. Puro + idempotente; autoenlaces <https://…> y spans de código se preservan. FIX-C2 triado no-reproducible (i18n.js ya fija <html lang> y detecta navigator.language). Ambos bloqueados con guards. 896 → 900 unit · Playwright 58/58. Resto del fix-prompt v1.58.3 en cola como one-fix ships (doctrina: nunca en lote).
[1.58.2] — 2026-05-19
fix(i18n): I18N-011 — localiza el índice de #/help en los 7 idiomas no-EN. El TOC se construye desde los encabezados ## de docs/help/<lang>.md. Las secciones 3/4/6/7/8/9/10/11/12 aún tenían títulos en inglés en es/pt-BR/ko/ja/ru/zh-CN/zh-TW, así que el TOC salía en inglés mientras el sidebar estaba traducido. Cada encabezado afectado se localiza ahora al mismo término que la clave nav.* del sidebar (fuente única — TOC ↔ sidebar coinciden), conservando el número de sección y el paréntesis (#/route …) literal. EN sin cambios. Cierra el único pendiente i18n del barrido QA v1.58. Solo docs; 896/896 unit · 33/33 help · Playwright 58/58.
[1.58.1] — 2026-05-19
fix(test): guard checkProfileCustomized CI-aislado (parche sobre v1.58.0). v1.58.0 pasó el pre-commit (consultivo) pero falló en ci.yml (Node 18/20/22): el test usaba import dinámico cache-bust + reescritura de PATHS, pero paths.mjs resuelve la raíz una vez por proceso. Reemplazado por un guard estático robusto (allow-list + regex ^(…)$/i anclado; un nombre real con “test” nunca se marca). Sin cambios de código de producción; desbloquea publish-package.yml. 896/896 unit · Playwright 58/58. Ver qa/v158-regression/.
[1.58.0] — 2026-05-19
fix(qa): barrido de bugs del informe QA externo + salida de investigación limpia y formateada. Corregido: BUG-001 #/followup valida la fecha opcional como ISO YYYY-MM-DD en el cliente; BUG-003 **negrita**/`código`/enlaces ahora se renderizan dentro de citas en UI.md() (todas las páginas de Ayuda); BUG-005 URL duplicada en pipeline muestra «Ya está en la cola — omitido»; BUG-006 mensaje de URL inválida humanizado (contexto (POST /api/pipeline · HTTP 400) se mantiene a propósito); BUG-007/008 el toast «Running doctor.mjs…» se descarta antes del modal (nuevo UI.dismissToast()), título del modal = etiqueta localizada del botón; BUG-010 subtítulo en el estado vacío de #/reports; BUG-002/UX-032 checkProfileCustomized() marca fixtures de prueba como «no personalizado» (no se tocan profile.yml/cv.md del padre — regla #1); I18N-012/013 Deep research en ruso realmente traducido. Nuevo: cleanLlmMarkdown() elimina el andamiaje de agente (<tool_call>{…}</tool_call>, <tool_response>, <thinking>, …) de #/deep y de Investigación guardada, en todos los proveedores y al servir archivos ya guardados; alias #/outreach→#/contacto (BUG-004); error de red del cliente localizado vía I18n.t() (8 locales; los details del servidor son diagnósticos en inglés a propósito). Tests: nuevos tests/qa-report-fixes.test.mjs (10) y tests/llm-output.test.mjs (5); 881 → 896 unit; Playwright 58/58. No cambiado (con justificación): BUG-009 (H1 #/cv por diseño, WCAG single-h1), datos del padre (parent-owned), cola larga de i18n/UX menor en backlog. Detalle completo en CHANGELOG.md.
[1.57.2] — 2026-05-19
fix(config): la causa REAL del «validation failed» en /#/config — el campo lang inyectado por la SPA. public/js/api.js adjunta automáticamente un campo lang a todos los cuerpos POST JSON (para que las rutas LLM tomen el idioma de la UI). /api/config no es una ruta LLM y lang no es una clave de configuración, así que el rechazo de claves desconocidas de validateConfig (correcto y relevante para seguridad) devolvía 400 en cada Guardado: validation failed — lang: not a known config key. Era solo del navegador: los repros con curl/in-process nunca enviaban lang, por eso v1.57.0/.1 mejoraron el mensaje pero no la causa. La ruta ahora elimina el lang de transporte antes de validar; el filtro de escritura por KNOWN_KEYS sigue descartando cualquier clave realmente desconocida — la protección anti-inyección no cambia. Detectado por un nuevo barrido Playwright que pulsa el botón Guardar real. Tests: nuevo tests/playwright-forms.mjs (26, integrado en npm run test:e2e:browser) sobre todos los formularios; config-endpoint con paridad de navegador. 879 → 881 unit; Playwright 32 → 58. Detalle completo en CHANGELOG.md.
[1.57.1] — 2026-05-19
fix(ux): cada error de API ahora dice QUÉ falló, DÓNDE y POR QUÉ; el texto del error de entrada es lo más descriptivo posible. El servidor ya devolvía { error, details: ["CAMPO: motivo", …] }, pero los formularios solo mostraban la línea superior («validation failed»), así que en /#/config (y en todas partes) no se sabía qué campo estaba mal. api.js ahora incorpora los details por campo en el mensaje en todo el sitio (un cambio, todos los formularios se benefician), añade el contexto de la petición (MÉTODO /ruta · HTTP NNN) (DÓNDE), recurre a un fragmento del cuerpo crudo en errores no-JSON, y los errores de red llevan método+ruta; err.details queda expuesto. Los mensajes de validateConfig son ahora máximamente descriptivos (qué falla y cómo arreglarlo). Las claves secretas nunca muestran el valor introducido (solo su longitud) — una clave real mal tecleada no se filtra a un toast/log. El rango de PORT ahora sí se valida (99999 se rechaza). En /#/config PORT y HOST se rellenan con sus valores por defecto reales (4317 / 127.0.0.1). Los toasts de error permanecen más tiempo (9–20 s) y se ajustan/desplazan en vez de recortarse. Tests: nuevo tests/config-validation-detail.test.mjs (12); 874 → 879. Detalle completo en CHANGELOG.md.
[1.57.0] — 2026-05-19
feat(provider): OpenRouter como 5º proveedor de evaluación en vivo headless + fix(config): «validation failed» al guardar cualquier clave API. Las claves pegadas suelen llegar con un salto de línea final o espacios (portapapeles del SO, botones «copiar» de las consolas de proveedores) — antes de 1.57 eso disparaba el guard de saltos de línea para todos los proveedores, y la regex anclada en $ de ANTHROPIC_API_KEY rechazaba por error claves Anthropic reales. Ahora validateConfig normaliza (recorta) cada valor antes de validar, la ruta persiste el valor recortado (autentica en runtime, sin romper .env), y la comprobación de Anthropic es un prefijo sk-ant- + longitud resiliente (el umbral compartido isUsableKey() ≥ 20 sigue siendo la verdadera comprobación). Los saltos de línea internos se siguen rechazando (guard de inyección en .env). OpenRouter es ahora un proveedor de primera clase: OPENROUTER_API_KEY en /#/config — una clave da acceso a más de 300 modelos. Es el último del orden auto (Anthropic → Gemini → OpenAI → Qwen → OpenRouter), así que una configuración existente nunca se redirige en silencio; LLM_PROVIDER=openrouter lo fija. Conectado al mismo camino _tailProvider() que OpenAI/Qwen en /api/evaluate, /api/deep, /api/mode/:slug; expuesto en /api/status/providers + el panel de Health. Cliente compatible con OpenAI (sin dependencias nuevas — fetch directo, timeout AbortController, la clave nunca se registra) con los headers HTTP-Referer/X-Title recomendados. El desplegable de modelos es en vivo: OPENROUTER_MODEL se llena desde GET /api/openrouter/models (proxy del servidor del catálogo público de OpenRouter — mantiene CSP connect-src 'self'), con lista curada de respaldo y caché en memoria de 10 min. Nuevas claves i18n (config.openrouter*) en los 8 idiomas. Tests: nuevos suites tests/openrouter-route.test.mjs y tests/openrouter-model-selector.test.mjs; suites env-config/openai/provider-selector ampliados. 831 → 855. Detalle completo en CHANGELOG.md.
[1.56.4] — 2026-05-19
feat(ui): UX-N2 — pista visible y según plataforma de ⌘K / Ctrl K en la búsqueda global. El atajo Cmd/Ctrl+K (enfocar la búsqueda) solo estaba en el aria-label/código, así que los usuarios videntes no lo descubrían y la app parecía más lenta de lo que es. Ahora un <kbd class="kbd-shortcut"> atenuado va al final de la píldora de búsqueda, rellenado al arrancar desde data-mac/data-other según una comprobación de plataforma (navigator.platform/userAgent): ⌘K en macOS/iOS, Ctrl K en el resto. Es aria-hidden="true" (el aria-label existente ya lo anuncia a los lectores de pantalla — la insignia no debe duplicar) y pointer-events:none (decorativa). El atajo Cmd/Ctrl+K existente no cambia. Sin nuevas claves i18n (los glifos son universales); la insignia es hijo flex del .searchbar existente (sin envoltorio/posición absoluta — el input ya es flex:1). Pruebas: nueva suite estática por código aislada de CI tests/cmdk-hint-visible.test.mjs (5): el <kbd class="kbd-shortcut"> está dentro de .searchbar; es aria-hidden="true" con ambas variantes data-mac/data-other; app.js lo rellena vía comprobación navigator; el binding (e.ctrlKey||e.metaKey)&&e.key==='k' → search.focus() intacto (guard de regresión); app.css estiliza .kbd-shortcut y nunca display:none. 826 → 831. feat(ui) · test: tests/cmdk-hint-visible.test.mjs. Detalles — CHANGELOG.md.
[1.56.3] — 2026-05-19
fix(reliability): la detección de claves de proveedor rechaza marcadores de posición / valores demasiado cortos, no solo la cadena vacía. Un GEMINI_API_KEY de marcador en un .env padre se reportaba como “✓ set” Y se elegía como proveedor activo por encima de un ANTHROPIC_API_KEY válido. effectiveEnv() solo rechazaba undefined/'', así que 10 caracteres de basura contaban como clave real: el banner de onboarding mostraba GEMINI ✓ set, GET /api/status/providers devolvía activeProvider: "gemini", y toda evaluación ⚡ en vivo habría fallado en silencio contra una clave muerta ignorando una clave Anthropic válida de 108 caracteres. La nueva función pura isUsableKey() (env-config.mjs) considera un secreto configurado solo si tiene ≥ 20 caracteres (ninguna clave soportada es más corta — Gemini AIza… ≈ 39, Anthropic sk-ant-… ≈ 100+, OpenAI ≥ 40, Qwen ≈ 35) y no es un marcador conocido (your_*_here, changeme, placeholder, <…>, un solo carácter repetido…). Aplicada uniformemente a hasAnthropicKey()/hasGeminiKey() (anthropic.mjs), hasOpenAIKey()/hasQwenKey() (openai.mjs) y las filas GEMINI_API_KEY/ANTHROPIC_API_KEY de GET /api/health (movidas de process.env crudo a la misma vista effective+plausible) — la página de salud, el endpoint de proveedores y el router OR ahora siempre coinciden. selectActiveProvider() no cambia; recibe un keysConfigured correcto. Pruebas: nueva suite aislada de CI tests/key-detection-rejects-placeholder.test.mjs (5): casos unitarios de isUsableKey + reproducción in-process con createApp() del escenario reportado (.env temporal con GEMINI_API_KEY de 10 caracteres + ANTHROPIC_API_KEY real) — gemini NO está en keysConfigured, activeProvider === "anthropic", filas de /api/health coherentes. Cuatro pruebas existentes de capas effective-env alargaron sus stubs demasiado cortos (el contrato no cambia). 821 → 826. fix(reliability) · test: tests/key-detection-rejects-placeholder.test.mjs. Detalles — CHANGELOG.md.
[1.56.2] — 2026-05-19
feat(a11y): UX-N1 — document.title por ruta y con idioma (orientación multipestaña + anuncio de cambio de página del lector de pantalla). Antes las 24 rutas mantenían el <title> estático de index.html (“career-ops — command center”): pestañas con el mismo nombre, marcadores genéricos y el mismo anuncio “página cambiada”. focusNewView() en public/js/router.js ahora deriva el título del propio <h1 class="page-title"> localizado de la vista — “Vista — career-ops” — así que los títulos se traducen automáticamente (sin nuevas claves i18n) y son únicos por ruta. Se fija antes del guard de primer pintado para que la pestaña inicial también tenga título (mismo orden que el tabindex de v1.56.0 UX-12). Recurre a career-ops — command center si una vista no tiene encabezado. Pruebas: nueva suite estática por código aislada de CI tests/document-title-per-route.test.mjs (4): focusNewView asigna document.title; el título proviene del <h1> (por ruta + localizado, no un literal único); la asignación precede a !firstPaintDone; hay un valor por defecto de producto. 817 → 821. feat(a11y) · test: tests/document-title-per-route.test.mjs. Detalles — CHANGELOG.md.
[1.56.1] — 2026-05-19
fix(a11y): se suprime el anillo de foco de marca espurio en el foco del encabezado con tabindex="-1" gestionado por el router. public/js/router.js asigna tabindex="-1" al encabezado de la vista destino y le hace .focus() en cada navegación de cliente (para que el lector de pantalla anuncie la nueva página). Un elemento tabindex="-1" nunca es alcanzable por teclado, pero la heurística :focus-visible de Chromium seguía pintando el anillo de marca global (*:focus-visible { outline: 2px solid var(--rausch) }) — un rectángulo rojo alrededor del encabezado (p. ej. “Command Center” en #/dashboard) en cada navegación, que también quedó grabado en las capturas hero images/dashboard-*.png. La corrección es una sola regla acotada [tabindex="-1"]:focus, [tabindex="-1"]:focus-visible { outline: none } (patrón de foco gestionado de WAI-ARIA APG). El foco real de teclado en controles interactivos conserva el anillo global *:focus-visible (WCAG 2.4.7 intacto); el anillo del skip-link no se ve afectado (es un <a>, no tabindex="-1", con mayor especificidad). Las 8 images/dashboard-*.png se regeneraron — sin recuadro rojo. Pruebas: nueva suite estática por código aislada de CI tests/managed-focus-no-ring.test.mjs (4): el anillo global *:focus-visible sigue definido (WCAG 2.4.7 sin regresión); [tabindex="-1"]:focus,:focus-visible ⇒ outline:none; la regla de supresión va después de la global (seguridad de cascada); la corrección es acotada (sin *:focus{outline:none} general). Junto con tests/dashboard-initial-focus.test.mjs. 813 → 817. fix(a11y) · test: tests/managed-focus-no-ring.test.mjs. Detalles — CHANGELOG.md.
[1.56.0] — 2026-05-19
feat(ux): paquete de pulido LOW — UX-9 / UX-10 / UX-11 / UX-12 (una release menor agrupada). UX-9 #/cv: el título de página se degrada a un chip-breadcrumb .cv-breadcrumb discreto y el subtítulo ruidoso pasa al title del <h1>, para que el CV del usuario (su nombre, en la vista previa) tenga la jerarquía visual. Invariante F-V54-A intacto — sigue siendo exactamente un <h1>, sigue .page-title. UX-10 nuevo helper compartido UI.providerCostHint(t) junto a ⚡ Ejecutar en vivo en #/auto, #/evaluate, #/deep y cada #/<mode>; reusa GET /api/status/providers (v1.55.3): con clave muestra “Coste estimado: OpenAI gpt-5-codex · ~$0.04/eval” (orden de magnitud, “~”); sin clave indica que ⚡ copia un prompt manual sin coste de API; fail-soft. UX-11 #/help: cuando el filtro del TOC reduce a exactamente una sección, la página se desplaza allí tras 300ms de inactividad (debounced; nunca con 0 o >1). UX-12 #/dashboard: en el primer pintado el <h1> se hace enfocable (tabindex="-1") y #content sigue aria-live="polite" (anunciado al arrancar) sin robar el foco (evita pelear con el skip-link, decisión v1.41.0). Nuevas claves i18n cost.estimate, cost.manual ×8; nuevo CSS .cv-breadcrumb/.cost-hint. Tests: 4 nuevas suites estático-de-fuente CI-aisladas (cv-breadcrumb 3, run-cost-line 4, help-toc-autoscroll 4, dashboard-initial-focus 3); locks pre-existentes cv-single-h1/help-nav-a11y actualizados (invariantes preservados). 800 → 813. Sonda Playwright en vivo de los 4, 0 errores de consola. feat(ux) · 4 test suites. Detalle en CHANGELOG.md.
[1.55.8] — 2026-05-19
feat(tracker): paginación del lado servidor + chips de embudo clicables (UX-8). Servidor: GET /api/tracker gana params opcionales ?page / ?pageSize / ?status. Sin ellos, la respuesta es byte a byte el { rows: [...] } heredado (todos los llamadores/tests existentes intactos). Con ellos devuelve { rows: slice, total, page, pageSize, funnel } — pageSize clampado a [1,500], page a ≥1, status filtra rows+total, y funnel es el desglose estado→conteo de todo el historial (independiente de página o filtro, para que los chips sean siempre exactos). #/tracker: nueva barra de chips de embudo clicable arriba — “todos los estados · N · Applied · N · Interview · N …” (orden Applied → Responded → Interview → Offer → Rejected → Discarded → Evaluated → SKIP). Clicar un chip fija el filtro de Estado (clicar el activo lo limpia); el chip activo es aria-pressed y resaltado. Nueva clave i18n track.funnelAria ×8; nuevo CSS .tracker-funnel/.tracker-chip/.tracker-chip--active. test: tests/tracker-server-paged.test.mjs (nuevo, 7 casos, CI-aislado, Express in-process en puerto efímero + applications.md temporal en CAREER_OPS_ROOT — CLAUDE.md #2/#8): back-compat (sin params ⇒ exactamente {rows}); ?page&pageSize slice + total/page/pageSize/funnel sumando N; última página parcial sin solape; página fuera de rango ⇒ rows vacío + total válido; ?status= filtra total/rows con funnel de todo el historial; cap de pageSize; + lock estático-de-fuente de la barra de chips. 793 → 800. feat(tracker) · test: tests/tracker-server-paged.test.mjs. Detalle en CHANGELOG.md.
[1.55.7] — 2026-05-19
feat(pipeline): virtualización de filas en vanilla-JS para >1000 filas (UX-7). #/pipeline renderizaba todas las filas (filtered.forEach(list.appendChild(urlRow))) — un escaneo llena la cola con miles de URLs, así que miles de nodos de fila (cada uno un div flex + <a> + dos botones) se construían sincrónicamente en cada tecla del filtro, saturando el DOM y el árbol de accesibilidad. Nueva virtualización vanilla-JS (equivalente a react-window, sin deps): por encima de VIRTUALIZE_THRESHOLD = 1000, #/pipeline pasa a ser un viewport con scroll de altura fija (70vh) con un espaciador no encogible (flex:0 0 auto, height = filas × 56px) que preserva la barra de scroll real de toda la lista, y un listener de scroll con rAF renderiza solo el viewport ± un búfer de 5 filas (~16–19 nodos a la vez en lugar de N). En/por debajo del umbral se mantiene el render simple original byte a byte, así que los pipelines típicos y todos los tests/e2e existentes no se ven afectados. Cada fila virtualizada conserva su aria-label ▶/✕ desambiguado por URL (F-V54-B bloqueado por regresión). El cálculo de ventana es un helper puro computeWindow(). test: tests/pipeline-virtualize.test.mjs (nuevo, 5 casos, CI-aislado, estático-de-fuente): umbral numérico ~1000; rama ≤umbral mantiene forEach→appendChild; rama >umbral renderiza slice(start,end) con listener de scroll rAF + espaciador; computeWindow() clampa [0,total] con ± búfer; filas conservan aria-labels ▶/✕. 788 → 793. Sonda Playwright en vivo (fixture de 1200 URLs): scrollHeight≈67248, solo ~16–19 nodos en el DOM, la ventana sigue el scroll de extremo a extremo, 0 errores de consola. feat(pipeline) · test: tests/pipeline-virtualize.test.mjs. Detalle en CHANGELOG.md.
[1.55.6] — 2026-05-19
feat(scan): ocultar filtros secundarios tras un desplegable “Filtros avanzados” (UX-4). #/scan apilaba todos los filtros — texto libre, remoto/híbrido/presencial, alcance, fuente y los chips de facetas stack/nivel/dinámicos posteriores al escaneo — con igual peso, un muro de controles. Ahora los filtros cotidianos quedan visibles (texto libre + Remoto/Híbrido/Presencial; el botón 🌐 Buscar ya está aparte en la tarjeta de controles) y los secundarios colapsan tras un <details class="scan-advanced"><summary>Filtros avanzados</summary>: los selects Alcance + Fuente, y —por separado— el grupo de chips de facetas (que ahora encabeza el resultado con la tabla, no con un muro de chips, y solo se renderiza si hay al menos una fila de chips). Nueva clave i18n scan.advancedFilters en los 8 locales; nuevo estilo .scan-advanced (afordancia ⚙ discreta, sin marcador, negrita al abrir). test: tests/scan-advanced-disclosure.test.mjs (nuevo, 6 casos, CI-aislado, estático-de-fuente): existe <details>/<summary> con hook .scan-advanced y etiqueta scan.advancedFilters; texto libre + remoto siguen visibles; alcance + fuente dentro del desplegable; chipsContainer es <details>; .scan-advanced summary con estilo; scan.advancedFilters ×8. 782 → 788. feat(scan) · test: tests/scan-advanced-disclosure.test.mjs. Detalle en CHANGELOG.md.
[1.55.5] — 2026-05-19
feat(dashboard): hero con los 2 CTA P0 + pista focal de actividad reciente (UX-3). #/dashboard abría con ~30 nodos de igual peso — sin un “qué sigue” claro. Un nuevo bloque .dash-hero ahora va justo bajo el encabezado: los dos recorridos P0 — ✨ Auto-pipeline para una URL y 🌐 Buscar ahora — se promueven a botones grandes .btn-hero, y una única pista focal de actividad reciente (“Última evaluación: <score> — <título>”, enlazada al informe; estado vacío guía en arranque en frío vía dash.heroNoEval) indica al usuario recurrente dónde se quedó y al nuevo la única acción que importa. Los dos botones primarios se quitaron del encabezado (solo queda el secundario “📋 Abrir pipeline”) para no duplicar la acción. Los contadores de estado se degradaron de .badge prominentes a píldoras .dash-chip discretas. Nuevas claves i18n dash.lastEval, dash.heroNoEval en los 8 locales; nuevo CSS .dash-hero/.btn-hero/.dash-chip. test: tests/dashboard-hero.test.mjs (nuevo, 5 casos, CI-aislado, estático-de-fuente): .dash-hero existe y precede la grilla Quick-actions; ambos CTA P0 son .btn-hero con rutas /auto+/scan; pista focal dash.lastEval + estado vacío dash.heroNoEval; buckets usan .dash-chip; CSS existe; dash.lastEval+dash.heroNoEval ×8. 777 → 782. feat(dashboard) · test: tests/dashboard-hero.test.mjs. Detalle en CHANGELOG.md.
[1.55.4] — 2026-05-19
feat(ux): ETA honesta del auto-pipeline junto a Run + Stop prominente durante un escaneo (UX-6). #/auto: nueva pista .auto-eta — “⏱ ~1–2 min” (clave auto.eta, title vía auto.etaTitle) — ahora junto al botón Run, para que la promesa de un clic sea honesta sobre la duración antes de comprometerse; el texto coincide con career-ops.org/docs (“pega una URL → informe completo en 1–2 minutos”). #/scan: mientras el rastreo de varios minutos está activo (aria-busy), Stop se promueve de botón fantasma de bajo contraste a botón destructivo prominente (nuevo .btn-danger — relleno, blanco sobre coral de alto contraste, peso 600). setScanRunning(running) alterna scan-stop-btn entre btn-danger (en ejecución) y btn-ghost (inactivo, oculto de todos modos), para que el usuario encuentre y confíe en Stop bajo carga. Nuevas claves i18n auto.eta, auto.etaTitle en los 8 locales; nuevo CSS .btn-danger/.auto-eta. test: tests/auto-eta-stop.test.mjs (nuevo, 4 casos, CI-aislado, estático-de-fuente): #/auto renderiza t('auto.eta') con clase .auto-eta junto a runBtn; auto.eta ×8; setScanRunning(running) promueve Stop a btn-danger; .btn-danger existe con texto blanco de alto contraste. 773 → 777. feat(ux) · test: tests/auto-eta-stop.test.mjs. Detalle en CHANGELOG.md.
[1.55.3] — 2026-05-19
feat(onboarding): estado OR de 4 proveedores en pantalla — banner de arranque en frío + chip de proveedor activo (UX-2, ALTA). Nuevo endpoint de solo lectura GET /api/status/providers → { activeProvider, activeModel, keysConfigured }. keysConfigured usa la misma vista de env efectiva que los gate sites de llm.mjs (process.env ∨ .env del padre); activeProvider es lo que el OR-router elegiría — selectActiveProvider(), nuevo helper puro en env-config.mjs que recorre providerOrder() (un pin LLM_PROVIDER sin clave correspondiente da null). No se devuelven secretos — solo nombres de proveedor + el id de modelo. El shell de la SPA ahora renderiza una región de onboarding global (#onboarding-banner, poblada por app.js, solo DOM seguro para CSP): 0 claves → banner rojo con CTA a #/config?tab=api-keys; ≥1 clave → chip discreto con el proveedor + modelo activo. Hace descubrible en pantalla el diferenciador estrella (“uno de Anthropic / Gemini / OpenAI / Qwen, auto-ordenado”) en vez de aprenderse por ensayo. Nuevas claves i18n onboarding.* en los 8 locales; nuevo CSS .onboarding-warn/.onboarding-ok. test: tests/onboarding-key-banner.test.mjs (nuevo, 9 casos, CI-aislado): semántica de selectActiveProvider; GET /api/status/providers en proceso (puerto efímero + .env en CAREER_OPS_ROOT temporal para nunca leer la clave real del padre — CLAUDE.md #2/#8); cableado SPA estático + cobertura onboarding.* ×8. 764 → 773. feat(onboarding) · test: tests/onboarding-key-banner.test.mjs. Detalle en CHANGELOG.md.
[1.55.2] — 2026-05-18
fix(cv): dar al editor markdown de #/cv un nombre accesible descriptivo y autónomo (F-V55-H / UX-5). El <textarea id="cv-editor"> del editor principal de #/cv ahora lleva un aria-label descriptivo mediante la nueva clave cv.editorAria — “Editor markdown del CV — tu currículum profesional en formato markdown” — en lugar del nombre escueto que heredaba del encabezado visible “Markdown”. Nota: al contrario que el síntoma de F-V55-H (que solo inspeccionó aria-label/labels), el campo no carecía de nombre — v1.47.0 (WS2 #16) ya lo había vinculado vía aria-labelledby → el <h3 id="cv-md-heading">Markdown</h3>, así que un lector de pantalla anunciaba “Markdown, edición, multilínea”. v1.55.2 mejora ese escueto “Markdown” a una etiqueta autónoma. El aria-labelledby redundante se elimina (sería markup muerto — aria-label gana por precedencia ARIA); el <h3>Markdown</h3> visible permanece para usuarios videntes. WCAG 1.3.1 + 4.1.2; paralelo al arreglo batch-tsv de v1.54.5 (F-V54-C). test: tests/cv-editor-a11y.test.mjs (nuevo, 3 casos, CI-aislado, estático-de-fuente como auto-stepper-prerender.test.mjs): #cv-editor se nombra vía t('cv.editorAria', …) con fallback no vacío; cv.editorAria presente y no vacío en los 8 locales; sin aria-labelledby redundante en el elemento. 761 → 764. fix(cv) · test: tests/cv-editor-a11y.test.mjs. Detalle en CHANGELOG.md.
[1.55.1] — 2026-05-18
fix(auto): prerenderizar el stepper de 5 etapas del pipeline al montar #/auto (F-V55-E / UX-1, obs. senior S-4 reabierta). #/auto ahora muestra el esquema documentado de cinco etapas — validar → obtener → evaluar → guardar informe → añadir al tracker — en el instante en que la pantalla se monta, en lugar de quedar en blanco hasta el primer evento SSE. Antes <ol class="auto-stepper"> se creaba display:none y renderStepper() solo se alcanzaba desde setStep() / run(), así que un usuario en arranque en frío nunca veía el pipeline que prometen los docs antes de pulsar Run. El stepper ahora es visible al montar con las cinco etapas en estado pending y lleva un aria-label (auto.stepperAria) para que la tecnología asistiva anuncie la región. Cierra F-V55-E (lente a11y/garantía estática) y UX-1 (lente fidelidad de promesa) — misma corrección, ambas lentes. test: tests/auto-stepper-prerender.test.mjs (nuevo, 4 casos, CI-aislado, estático-de-fuente como router.test.mjs): el array STEPS son exactamente las 5 etapas canónicas en orden; stepperEl no es display:none al montar y lleva auto.stepperAria; una llamada renderStepper() de ámbito de montaje precede a function setStep(; auto.stepperAria presente en los 8 locales. 757 → 761. fix(auto) · test: tests/auto-stepper-prerender.test.mjs. Detalle en CHANGELOG.md.
[1.55.0] — 2026-05-18
feat(llm): la live-eval headless funciona vía “OR” — Anthropic | Gemini | OpenAI | Qwen, autoseleccionado según qué clave esté definida. A petición del usuario, la ⚡ live-eval del web-ui ahora funciona con cualquier clave de API que esté definida, no solo Anthropic/Gemini. LLM_PROVIDER gana openai y qwen; auto (por defecto) usa el primer proveedor cuya clave esté presente, prefiriendo Anthropic → Gemini → OpenAI → Qwen. Un valor explícito fija uno; un proveedor forzado sin clave aún cae al camino del prompt manual. Nuevo server/lib/openai.mjs — un cliente Chat Completions compatible con OpenAI y sin dependencias (mismo patrón HTTPS directo seguro que anthropic.mjs: timeout AbortController, clave nunca registrada, resolución de clave effectiveEnv() para que una clave del .env del padre funcione sin reinicio). Un núcleo (runOpenAICompatible) respalda runOpenAI (api.openai.com) y runQwen (modo compatible con OpenAI de Alibaba DashScope; sobrescribe el endpoint con QWEN_BASE_URL en el .env crudo para el host de China continental). Sin SDKs, sin ejecución arbitraria de CLI — el proyecto padre sigue siendo agnóstico de CLI (Claude Code · Codex · Gemini · OpenCode · Qwen · Copilot · Kimi); esto solo extiende el camino headless por clave de API. La cola OpenAI/Qwen está cableada en todas las superficies de eval: /api/evaluate, /api/deep, /api/mode/:slug y el SSE de /api/auto-pipeline — consultada tras las ramas Anthropic (inline) + Gemini (subproceso) para preservar la preferencia auto, con el mismo inlining de contexto empaquetado que usa Anthropic. env-config.mjs: QWEN_API_KEY (secreto) + QWEN_MODEL (no secreto) añadidos a KNOWN_KEYS/KEY_GROUPS.core; LLM_PROVIDERS y providerOrder() extendidos; OPENAI_API_KEY es ahora una clave de proveedor headless de primera clase (antes solo almacenada). Pestaña de claves de #/config: el select LLM_PROVIDER gana openai/qwen; nuevos campos QWEN_API_KEY + QWEN_MODEL (lista curada qwen-max/qwen-plus/qwen-turbo/qwen2.5-*); una nueva nota al inicio de la pestaña explica el padre agnóstico de CLI vs la eval headless del web-ui y el orden OR. Nuevas claves i18n en los 8 locales. test: tests/openai.test.mjs (nuevo, 9 casos, CI-aislado): éxito OpenAI/Qwen + contenido en array de bloques, auth Bearer, endpoints por defecto y sobrescrito por QWEN_BASE_URL, 4xx/5xx/malformado, clamp de max_tokens, timeout, detección de clave effectiveEnv, canario de no-fuga de clave. tests/provider-selector.test.mjs actualizado para la superficie providerOrder/LLM_PROVIDERS/SECRET de v1.55.0 + el cableado de la cola OpenAI/Qwen. 748 → 757. feat(llm) · test: tests/openai.test.mjs · test: tests/provider-selector.test.mjs. Detalle en CHANGELOG.md.
[1.54.10] — 2026-05-18
fix(auto-pipeline): higiene de desconexión del cliente SSE — eliminar el job e2e de Playwright inestable. El job e2e de Playwright se ponía en rojo de forma intermitente (32/32 pruebas individuales pasan, pero not ok 2 - tests/playwright-smoke.mjs): cerrar una página mientras el stream SSE de #/auto estaba en curso hacía que el siguiente res.write() del servidor fuera rechazado con EPIPE/"aborted", y —sin un listener 'error' en la respuesta— Node lo escalaba a una uncaughtException que node:test reportaba como “asynchronous activity after the test ended”. openSse() en auto-pipeline.mjs ahora registra un res.on('error') no-op y protege send() con res.writableEnded || res.destroyed (envuelto en try/catch) — un cliente desaparecido es esperado, no excepcional. Esto es higiene SSE de producción correcta, no solo un arreglo de prueba. tests/playwright-smoke.mjs: la prueba de Cmd+K usaba una URL saliente real (https://example.com/jobs/123) pero solo esperaba a que apareciera el modal, así que closePage() abortaba el safeGet() en curso del servidor después de terminar la prueba. Ahora espera a que el pipeline alcance un estado terminal (para que el fetch se resuelva normalmente antes del cierre). Un helper compartido closePage() (window.stop() y luego cerrar) y el hook after con server.closeAllConnections() permanecen como defensa en profundidad. Verificado: 8/8 ejecuciones verdes consecutivas (6× node --test + 2× browser-smoke), antes ~1-de-2 en rojo. tests/auto-pipeline.test.mjs +1 caso estático que fija el contrato de higiene de desconexión de openSse (listener res.on('error') + guarda writableEnded||destroyed + escrituras envueltas en try). 747 → 748. fix(auto-pipeline) · test: tests/auto-pipeline.test.mjs. Detalle en CHANGELOG.md.
[1.54.9] — 2026-05-18
fix(llm): honrar las claves LLM del .env del proyecto padre en tiempo de petición — dejar de enrutar erróneamente a un proveedor obsoleto/inválido. La evaluación en vivo podía fallar con “Gemini API error: API key not valid” incluso cuando ANTHROPIC_API_KEY era el proveedor configurado. Causa raíz: hasAnthropicKey() / hasGeminiKey() (y la búsqueda de clave/modelo de runAnthropic) leían solo el snapshot de process.env del arranque. Si la clave Anthropic se añadía al .env del padre después de arrancar el servidor, el proceso en ejecución nunca la veía → la detección de Anthropic era falsa, y la evaluación caía a cualquier clave obsoleta que sí estuviera en process.env (a menudo un GEMINI_API_KEY antiguo e inválido). La ruta de ejecución de Gemini (un subproceso Node del padre) ya leía el .env vivo del padre, así que ambos proveedores resolvían claves de forma inconsistente. Nuevo effectiveEnv(key, envFilePath) en env-config.mjs: un valor no vacío de process.env gana (cubre exports de shell y el live-apply en POST /api/config); en otro caso se consulta el .env actual del padre. anthropic.mjs ahora resuelve ANTHROPIC_API_KEY, ANTHROPIC_MODEL y la verificación de la clave Gemini a través de él, de modo que una clave puesta en el .env del padre se honra sin reiniciar el servidor y la DETECCIÓN de clave siempre coincide con la clave que la petición realmente ENVÍA. El orden de proveedores no cambia (auto → Anthropic-luego-Gemini); esto solo arregla la detección. Las claves nunca se registran ni se devuelven (la prueba de no-fuga REVIEW-B4 sigue pasando). tests/anthropic.test.mjs reescrito para ser CI-aislado (temp CAREER_OPS_ROOT, import dinámico) con 2 casos nuevos que reproducen el bug exacto (clave solo en el .env del padre → detectada; runAnthropic envía la clave + modelo del .env del padre cuando process.env está sin definir). tests/env-config.test.mjs +3 casos effectiveEnv (precedencia de process.env, fallback al .env incl. cadena-vacía-como-no-definida, archivo-ausente / clave-ausente / sin-ruta → undefined) — 100% de la rama nueva. 742 → 747. fix(llm) · test: tests/anthropic.test.mjs · test: tests/env-config.test.mjs. Detalle en CHANGELOG.md.
[1.54.8] — 2026-05-18
feat(config): el formulario por campos de Modes siempre renderiza el esquema canónico (incluso en un archivo vacío/stub) con la guía de campos de career-ops.org. El formulario por campos de Modes de v1.54.3 solo renderizaba campos para secciones ## ya existentes — así que en un modes/_profile.md recién creado, vacío o no conforme al esquema (p. ej. el stub habitual de 1 línea) caía en “No ## sections found — use the raw editor below.” y el usuario no obtenía campos. A petición del usuario (“разбей по полям … описание полей возьми из career-ops.org/docs”), el formulario ahora siempre renderiza los 5 campos canónicos en el orden documentado (Target Roles, Adaptive Framing, Exit Narrative, Comp Targets, Location Policy), prerrellenados desde el archivo cuando existen y vacíos-pero-editables cuando no — de modo que un perfil totalmente nuevo puede rellenarse íntegramente a través del formulario. Cada campo muestra una descripción tomada del §Step-5 del Quick Start canónico de career-ops.org (qué poner en Target Roles / Adaptive Framing / Exit Narrative / Comp Targets / Location Policy), cableada vía aria-describedby para lectores de pantalla. Tolerante a variantes de encabezado: el ## Your Target Roles (etc.) de la plantilla mapea al mismo campo canónico que ## Target Roles, de modo que ni la plantilla ni la convención del andamiaje del servidor rompen el formulario. collect() es ahora una carga etiquetada: una fusión { sections } no destructiva cuando los encabezados renderizados coinciden exactamente con los del archivo (preámbulo + secciones intactas + personalizadas sobreviven byte-estables), o una reconstrucción de archivo completo { markdown } que arranca/normaliza un documento conforme al esquema cuando el archivo carecía de él. La ruta de reconstrucción está protegida por confirmación en config.js (reemplaza el archivo padre — invariante de guardado destructivo WS2 #4), preserva el preámbulo existente (o un valor por defecto documentado) y mantiene las secciones no canónicas verbatim. 6 nuevas claves i18n (config.modesDescTargetRoles … config.modesDescLocationPolicy + config.modesFormRebuildBody) en los 8 locales. tests/modes-form.test.mjs reescrito para el contrato v1.54.8: esquema + orden canónico, cableado de carga/confirmación de config.js, presencia de la descripción de cada campo proveniente de la documentación en los 8 locales, tolerancia canonicalKey “Your X”, estabilidad del round-trip de listas, la garantía de bootstrap-siempre-renderiza, y el collect() etiquetado secciones-vs-markdown con seguridad de datos. Verificado en vivo contra el archivo stub real del padre (5 campos + descripciones aparecen, 0 errores de consola) y un fixture stub aislado (rellenar → guardado protegido por confirmación → las 5 secciones canónicas persistidas). feat(config) · test: tests/modes-form.test.mjs. Detalle en CHANGELOG.md.
[1.54.7] — 2026-05-18
fix: W-001 — los assets de código/estilo + el shell de la SPA se servían con Cache-Control: no-store (higiene de despliegue). La SPA carga api.js / router.js / cada vista mediante <script src> plano sin query string de versión, y no hay paso de build (sin content hashing), por lo que tras un despliegue un navegador podía seguir sirviendo un bundle viejo cacheado durante horas → 404 de caché obsoleta en rutas con query string (observado en vivo durante la regresión v1.29.2; corrida de regresión W-001). server/index.mjs ahora fija Cache-Control: no-store en .js / .mjs / .css / .html vía el hook setHeaders de express.static, y explícitamente en el catch-all del shell de la SPA (que usa sendFile y evita setHeaders), de modo que el navegador siempre revalida el código que dirige el enrutado. Los assets estáticos no-código mantienen el caching por defecto de express.static. Las cabeceras de seguridad (CSP / nosniff / frame-deny / referrer-policy) no cambian — verificado por la suite security-headers existente (8 casos) corriendo en verde junto a la nueva prueba. +1 archivo de pruebas tests/asset-cache-control.test.mjs — 4 casos (assets JS no-store, CSS no-store, index.html estático no-store, shell de ruta profunda del catch-all de la SPA no-store), arrancando la app real contra un CAREER_OPS_ROOT aislado. Más un arreglo de teardown flaky en tests/playwright-smoke.mjs (commit test(e2e) aparte): la prueba de humo SSE del auto-pipeline ahora cancela el reader + aborta el fetch en un finally y el hook after cierra a la fuerza los sockets persistentes, eliminando el “Error: aborted” post-teardown que enrojecía el job Playwright e2e de v1.54.6. 738 → 742. fix · test: tests/asset-cache-control.test.mjs. Detalle en CHANGELOG.md.
[1.54.6] — 2026-05-18
fix(a11y): S-7 — el botón back-to-top de #/help lleva la clase selectora canónica back-to-top. El botón flotante back-to-top de #/help funcionaba correctamente (verificado en vivo) pero su lista de clases (btn btn-primary help-back-top) quedaba fuera de la convención del selector .back-to-top que apunta la prueba de spec §2 #28 — un selector más estricto habría flaqueado (corrida de regresión S-7, “victoria fácil”). El botón ahora lleva también la clase canónica back-to-top. Puramente aditivo y un no-op de CSS: help-back-top (el hook CSS existente) no cambia y back-to-top no tiene regla CSS — es solo un asa estable de prueba/automatización. Verificado en vivo: document.querySelector('.back-to-top') resuelve el botón, aria-label intacto, 0 errores de consola. Se extendió el caso #12 existente en tests/help-nav-a11y.test.mjs con una aserción de que la lista de clases del botón back-to-top incluye el selector canónico back-to-top (sin archivo nuevo). fix(a11y) · test: tests/help-nav-a11y.test.mjs. Detalle en CHANGELOG.md.
[1.54.5] — 2026-05-18
fix(a11y): F-V54-C — el editor TSV de #/batch tiene un nombre accesible. El <textarea> TSV de #/batch tenía una pista cableada vía aria-describedby pero ningún nombre accesible — sin <label htmlFor>, sin aria-label/aria-labelledby (corrida de regresión F-V54-C; WCAG 1.3.1 Info & Relationships / 4.1.2 Name, Role, Value). aria-describedby aporta una descripción, no un nombre, así que un lector de pantalla anunciaba un “edit text” sin etiqueta. El textarea ahora lleva un aria-label vía la nueva clave i18n batch.tsvAria, consistente con las entradas hermanas de control de corrida que ya usan claves *Aria; la pista describedby existente se conserva. Verificado en vivo: aria-label presente + localizado, aria-describedby intacto, 0 errores de consola. Nueva clave i18n batch.tsvAria en los 8 locales. +1 archivo de pruebas tests/batch-tsv-accessible-name.test.mjs (2 casos: el bloque batch-tsv tiene un aria-label vía t(batch.tsvAria) manteniendo su pista describedby; batch.tsvAria definida en los 8 locales); 736 → 738. fix(a11y) · test: tests/batch-tsv-accessible-name.test.mjs. Detalle en CHANGELOG.md.
[1.54.4] — 2026-05-18
fix(a11y): F-V54-B — los botones de acción por fila de #/pipeline tienen nombres accesibles. Los botones ▶ (evaluar) y ✕ (eliminar) por fila en #/pipeline eran solo iconos con únicamente un atributo title (corrida de regresión F-V54-B; WCAG 4.1.2 Name, Role, Value). title no es un nombre accesible fiable, así que un usuario de lector de pantalla oía una larga sucesión de “button” indistintos y no podía saber a qué fila afectaría un borrado. Ambos botones llevan ahora un aria-label explícito desambiguado por una URL compacta vía un nuevo helper shortUrl() (host + …/ + los 2 últimos segmentos de ruta; fallback de recorte final para entradas no parseables), de modo que el árbol de a11y lee p. ej. “Delete: hh.ru/…/vacancy/12345”. Sin nuevas claves i18n — reutiliza common.delete / pipe.evaluateBtn + la URL. Verificado en vivo: 1385 filas, cada nombre de botón único por fila, 0 errores de consola. +1 archivo de pruebas tests/pipeline-row-action-names.test.mjs (4 casos: ambos botones cableados con shortUrl(url) + exactamente dos de esas etiquetas, shortUrl declarado antes de su uso, URLs de mismo host pero distinto empleo no colapsan, fallbacks de host pelado / no parseable / vacío); 732 → 736. fix(a11y) · test: tests/pipeline-row-action-names.test.mjs. Detalle en CHANGELOG.md.
[1.54.3] — 2026-05-18
feat(config): formulario de campos estructurado para la pestaña “Modes” de #/config (sin más markdown crudo). La pestaña “Modes” editaba modes/_profile.md como un único <textarea> crudo por sección ## (granularidad a nivel de sección de v1.36.0). A petición del usuario, ahora renderiza un formulario de campos estructurado derivado del esquema documentado (career-ops.org Quick Start §Step-5): Target Roles / Adaptive Framing / Comp Targets → entradas de línea etiquetadas repetibles para añadir/quitar (una línea de rol/ángulo/comp por campo, + Add line / ✕ por fila con aria-label); Exit Narrative / Location Policy → un único <textarea> de prosa etiquetado. Cada campo es un control real vinculado con <label htmlFor> y un nombre de sección i18n. El nuevo public/js/lib/modes-form.js (window.ModesForm) posee la lógica parse → render → collect(); alimenta la ruta de merge existente PUT /api/modes/_profile { sections }, de modo que el preámbulo, el orden y cualquier sección que el formulario no toque sobreviven byte-estables (merge-no-reemplazo, aplicado por el servidor). Seguridad de datos: una sección de lista canónica cuyo cuerpo no sea una lista de viñetas pura (el usuario puso prosa ahí) y cualquier sección ## no canónica recurren a un <textarea> literal etiquetado con una nota explicativa — el contenido arbitrario hace round-trip intacto, nunca se reescribe ni se pierde silenciosamente. Estabilidad de round-trip probada: serialise(parse(body)) re-parsea idénticamente. El editor de markdown crudo de archivo completo permanece como la divulgación Advanced con confirmación para añadir/quitar sección y editar el preámbulo (puerta de guardado destructivo de WS2 #4 sin cambios). 10 nuevas claves i18n (config.modesTargetRoles … config.modesUnknownNote) en los 8 locales. +1 archivo de pruebas tests/modes-form.test.mjs (7 casos); 725 → 732. Verificado en vivo contra una fixture aislada CAREER_OPS_ROOT: 5 secciones canónicas renderizadas como campos + 1 sección personalizada como fallback etiquetado, round-trip de editar-y-guardar preservó el preámbulo + la sección personalizada, 0 errores de consola. feat(config) · test: tests/modes-form.test.mjs. Detalle en CHANGELOG.md.
[1.54.2] — 2026-05-18
feat(config): selector de modelo OpenAI / Codex en #/config. #/config no tenía forma de elegir el modelo OpenAI / Codex — solo ANTHROPIC_MODEL y GEMINI_MODEL tenían desplegables, aunque OPENAI_API_KEY ya estaba expuesta para el flujo multi-CLI del proyecto padre (Codex / OpenCode). Ahora OPENAI_MODEL es una clave de entorno de primera clase: añadida a KNOWN_KEYS de env-config.mjs (ordenada justo después de OPENAI_API_KEY) y al grupo de claves core, y deliberadamente no en SECRET_KEYS — es un id de modelo, no una credencial, así que nunca se enmascara. config.js gana una lista curada OPENAI_MODELS (gpt-5-codex por defecto, luego gpt-5 / gpt-5-mini / gpt-4.1 / o4-mini / o3) y un campo <select> OPENAI_MODEL renderizado justo después de la clave OpenAI, reflejando exactamente los campos de modelo Anthropic/Gemini. Nuevas claves i18n config.openaiModel + config.openaiModelHint en los 8 locales. +1 archivo de pruebas tests/openai-model-selector.test.mjs (4 casos); 721 → 725. Verificado en vivo: #/config → select OPENAI_MODEL con 6 opciones, por defecto gpt-5-codex, vinculado a etiqueta, 0 errores de consola. feat(config) · test: tests/openai-model-selector.test.mjs. Detalle en CHANGELOG.md.
[1.54.1] — 2026-05-18
fix(a11y): F-V54-A — un único <h1> en #/cv. El propio # Name del markdown del CV se renderizaba como un segundo <h1> de nivel superior junto al <h1>CV</h1> del título de página (corrida de regresión F-V54-A; WCAG 1.3.1 Información y relaciones / 2.4.6 Encabezados). Ahora cv.js canaliza cada punto de inyección de la vista previa del CV (render inicial, refresco al importar archivo, sincronización en vivo del editor) a través de un cvMd() con alcance acotado que baja los encabezados un nivel (h1→h2 … h6→role="heading" aria-level="7"), de modo que la página conserva exactamente un <h1>. Acotado a cv.js a propósito — UI.md lo comparten help/reports/deep/evaluate, que gestionan sus encabezados a su manera. +1 archivo de pruebas tests/cv-single-h1.test.mjs (4 casos); 717 → 721. Verificado en vivo: #/cv → 1 <h1>, el # Name del usuario ahora es <h2>, 0 errores de consola. fix(a11y): F-V54-A · test: tests/cv-single-h1.test.mjs. Detalle en CHANGELOG.md.
[1.54.0] — 2026-05-18
WS10 — re-validación de docs canónicos + paridad H3 del paquete de ayuda (la versión final de convergencia). El gate de CI de CHANGELOG/estructura solo comprobaba H2, así que docs/help/en.md había derivado en silencio hasta 70 subsecciones H3 mientras los 7 paquetes localizados se quedaban en 68 — la brecha era §17 (la tabla «Reference adapters» + la lista «Common pitfalls», solo en inglés). Ambas están ahora traducidas a los 7 idiomas (nombres de archivo / enlaces / identificadores de adaptadores mantenidos byte a byte idénticos); los 8 paquetes ahora suman 17 H2 / 70 H3. Un nuevo gate de paridad H3 en help-ru-config-section.test.mjs lo fija (716 → 717). canonical-docs-coverage.test.mjs 7/7 confirma que la ayuda sigue reflejando las 5 guías de career-ops.org/docs; la auditoría UX de WS2 (40 hallazgos v1.41→v1.52) validó cada pantalla frente a los docs — sin divergencia. docs/sdd/CONVENTIONS.md actualizado a v1.54.0 (totales de pruebas, gate de paridad H3, archivos atípicos por tamaño, nueva sección de convenciones de Accesibilidad). WS0–WS10 completos; solo queda WS11. fix(docs): WS10 canonical re-validation + H3 parity · test(help): H3-parity gate. Detalle en CHANGELOG.md.
[1.53.0] — 2026-05-18
WS9 — pirámide de pruebas de la superficie shell (la última capa sin probar). Los 4 scripts bin/*.sh y el hook .githooks/pre-commit tenían cobertura cero; el nuevo tests/sh-files.test.mjs añade 10 casos que fijan la sintaxis bash -n/sh -n, el shebang + bit ejecutable y los contratos de comportamiento de los que dependen otros workstreams: career-ops-ui.sh — help sale con 0 sin fuga de shell-source (guarda de regresión v1.40.0), un verbo desconocido sale con 2, y usage() es un heredoc; start.sh — respeta NO_OPEN, exige Node ≥ 18 y delega el levantamiento del navegador a scripts/open-dashboard.mjs (guarda v1.43.0); setup.sh — modo estricto, SKIP_START, clona ambos repos; run_all.sh — parseo de --quick/--no-e2e y las 4 suites; .githooks/pre-commit ejecuta el revisor de WS7 y ningún archivo shell invoca git --no-verify (guarda de la regla dura #7 de CLAUDE.md); install-hooks.mjs cablea core.hooksPath. docs/architecture/TESTING.md — se añadió la capa base de superficie shell al diagrama de la pirámide + una nota de totales v1.53.0 (716 casos node --test / 90 archivos + 4 superficies E2E). 706 → 716. Detalle en CHANGELOG.md.
[1.52.0] — 2026-05-18
WS2 LOWs #33–#40 — barrido de pulido por lotes (cierra la cola de la auditoría de UX). Ocho hallazgos de baja severidad. fix(a11y/i18n): WS2 LOW batch — #33: #/dashboard — los 3 CTA de cabecera eran inconsistentes (solo 2 tenían icono inicial); «Open Pipeline» ahora lleva 📋 y los tres coinciden. #34: #/profile — los fit/level del arquetipo se mostraban como dos chips ambiguos; ahora van prefijados (Fit: / Level:) con aria-label a juego. #35: #/health — los toasts de Run-doctor / verify mostraban cadenas crudas de doctor.mjs; ahora con clave i18n. #36: #/health — los resultados de las comprobaciones eran <div>s planos; ahora son un role=list <ul>/<li> y el badge de estado lleva aria-label="<check>: <status>". #37: #/reports — las tarjetas eran <div onClick> solo de ratón; ahora role=link + tabindex + manejador Enter/Espacio + aria-label. #38: #/activity — el comentario del paginador decía «200» mientras el código pedía 500; reconciliado a una constante CAP y un aviso role=note aflora cuando el límite de 500 trunca el historial antiguo. #39: #/batch — los placeholders en prosa estaban codificados en inglés mientras sus aria-labels estaban localizados; los cuatro ahora con clave i18n. #40: las páginas de modo relabelaban el botón primario en silencio tras la sonda asíncrona; ahora una región role=status cortés lo anuncia. 10 nuevas claves i18n × 8 idiomas ({n} preservado); +9 tests: test: tests/low-sweep.test.mjs. 697 → 706. Cierra la cola de la auditoría de UX de WS2 (#1–#40 de v1.41→v1.52); siguiente WS9 → WS10 → WS11. Detalle en CHANGELOG.md.
[1.51.0] — 2026-05-18
WS2 #13 + #14 + #18 + #19 + #20 — barrido de feedback/i18n en #/auto y #/evaluate. Cinco hallazgos de la auditoría de UX. fix(a11y/ux): auto+evaluate — busy state, actionable HTTP errors, clipboard fallback, aria-live result, spinner-guarded submit — #13: el botón Run de #/auto ahora muestra un estado ocupado (is-loading + aria-busy + «Running…») en lugar de solo deshabilitarse. #14: una petición HTTP fallida ahora aflora un mensaje i18n accionable sobre el paso Y un toast (auto.httpFail con {n}), en vez de un escueto «HTTP 500». #18: el «Copy prompt» del modo manual ahora usa la Clipboard API asíncrona con respaldo execCommand, y emite un toast de fallo real en lugar de un falso «Copied». #19: el contenedor del resultado de evaluate es ahora role=status aria-live=polite, de modo que la larga llamada al LLM se anuncia a los lectores de pantalla. #20: el botón Evaluate va envuelto en UI.withSpinner (era un onClick: run plano que permitía envíos duplicados). 3 nuevas claves i18n × 8 idiomas; +6 tests: 691 → 697. También una corrección solo de pruebas (commit 7f8e250): el teardown de e2e pipeline-delete estaba en la ruta del confirm nativo previo a v1.48; pasado a DELETE por API (fix(test): … — el Playwright-e2e de CI estaba en rojo; no es una regresión del producto). Detalle en CHANGELOG.md.
[1.50.0] — 2026-05-18
WS2 #12 + #27 + #28 — accesibilidad de la navegación de la ayuda. Tres hallazgos de la auditoría de UX en #/help sobre una guía de 17 secciones y 90+ encabezados, corregidos en help.js. fix(a11y): help — single h1, labelled+filterable TOC, focus-on-anchor, back-to-top — #28: el markdown del documento abría con su propio # Title, produciendo un SEGUNDO <h1> en una página cuyo encabezado ya aporta el h1 canónico; ahora se elimina todo <h1> del artículo, de modo que hay exactamente un h1 y la jerarquía arranca limpiamente en las secciones <h2>. #27: el <nav> del TOC era un punto de referencia sin nombre (dos <nav> sin etiqueta en la página); ahora tiene aria-label (help.toc), y al pulsar una entrada del TOC el foco se mueve al encabezado de la sección (tabindex=-1 + focus()), no solo el desplazamiento del viewport. #12: no había forma de encontrar nada en un documento largo; un filtro type=search sobre el TOC reduce las entradas por texto de encabezado en vivo, y un botón flotante con aria-label «Back to top» aparece tras desplazarse, vuelve al inicio y devuelve el foco al <h1> de la página; su listener de scroll se elimina en hashchange al salir de #/help. 2 nuevas claves i18n × 8 idiomas — help.tocFilter, help.backToTop; +6 tests: test: tests/help-nav-a11y.test.mjs. 685 → 691. Detalle en CHANGELOG.md.
[1.49.0] — 2026-05-18
WS2 #10 + #11 + #25 + #26 — accesibilidad y ordenación de la tabla del tracker. Cuatro hallazgos de la auditoría de UX en #/tracker, corregidos en tracker.js. fix(a11y): tracker headers, sortable table, localized fix labels, empty state — #10: el encabezado de la columna de acción era una cadena vacía y el botón Report por fila no tenía contexto; ahora cada <th> lleva scope=col, el encabezado de acción y los de Score/PDF están con clave i18n (estaban vacíos o en inglés codificado), y el botón Report gana un aria-label con la empresa (<report> — <company>). #11: un tracker sin forma de ordenar; los encabezados Date / Score / Status son ahora botones de ordenación operables por teclado dentro del <th> con aria-sort (none/ascending/descending); un comparador sorted() (numérico para score, comparación de locale para date/status) corre antes de la paginación, y el clic alterna la dirección y reinicia el paginador. #25: track.normalize/dedup/merge eran inglés idéntico en los 8 idiomas pese a ser los controles destructivos de mayor riesgo (reescriben data/applications.md) — ahora correctamente localizados, más un title de ayuda. #26: la primera ejecución con cero filas mostraba el mismo mensaje «no match» que una lista sobrefiltrada; rows.length === 0 ahora muestra un estado vacío distinto (título + cuerpo + CTA «Open pipeline»). 7 nuevas claves i18n × 8 idiomas + 3 relocalizadas; +6 tests: test: tests/tracker-a11y-sort.test.mjs. 677 → 683. Detalle en CHANGELOG.md.
[1.48.0] — 2026-05-18
WS2 #8 + #22 — pipeline: confirmación con foco atrapado + accesibilidad de la vista previa. Dos hallazgos de la auditoría de UX en #/pipeline, corregidos en pipeline.js. fix(a11y): pipeline UI.confirm() + live preview region — #8: las tres acciones de #/pipeline usaban confirm() nativo (no atrapaba el foco): el Delete del panel de vista previa, el ✕ de cada fila y “Evaluate first”; ahora todas pasan por el UI.confirm() con foco atrapado (infra v1.44.0) — los dos borrados danger:true (Cancelar por defecto), “Evaluate first” danger:false; ya no queda ningún confirm() nativo en pipeline.js. #22: previewPane no tenía rol en vivo y un fallo de fetch se metía en previewBody, renderizado como una <pre> “preview” engañosa; ahora es role=region aria-live=polite con un aria-label, y los fallos fijan un previewError aparte renderizado como un bloque role=alert distinto (se limpia al (re)seleccionar o al borrar la fila activa). 4 nuevas claves i18n × 8 idiomas; +5 tests: test: tests/pipeline-confirm-preview.test.mjs. 672 → 677. Detalle en CHANGELOG.md.
[1.47.0] — 2026-05-18
WS2 #7 + #30 + #31 + #16 — barrido de accesibilidad de etiquetas sin vincular. Cuatro hallazgos de la auditoría de UX donde controles de formulario no tenían etiqueta programática (WCAG 1.3.1 / 3.3.2 / 4.1.2), ahora vinculados. fix(a11y): bind every swept form control to an accessible name — #7 scan.js: el checkbox dry-run y el desplegable company-select tenían etiquetas sin for; se añadió htmlFor (con los id existentes). #30 deep.js: los inputs company / role tenían etiquetas sin vincular; se añadió id + htmlFor (deep-company, deep-role). #31 apply.js: url / jd tenían etiquetas sin vincular; se añadió id + htmlFor (apply-url, apply-jd). #16 cv.js: el <textarea> principal de markdown no tenía nombre accesible; se vinculó mediante aria-labelledby al encabezado visible “Markdown” — nombre para lectores de pantalla idéntico al título en pantalla, sin nueva clave i18n. Usa el patrón explícito label[for]↔control[id] ya estándar en batch.js / mode-page.js; sin nuevas claves i18n; cero cambio de comportamiento. +5 tests: test: 667 → 672. Detalle en CHANGELOG.md.
[1.46.0] — 2026-05-18
WS2 #5 + #6 + #21 + #24 — accesibilidad del SSE de scan. Cuatro hallazgos de la auditoría de UX en #/scan, corregidos en scan.js. fix(a11y): scan SSE — live-log region, Stop, run-state, error banner — #5: la consola de streaming ahora es role=log aria-live=polite (+ aria-label, tabindex=0, desplazable con teclado), con una región oculta visualmente aparte role=status asertiva que anuncia los eventos terminales (completado / fallido / detenido). #6: un botón Stop cierra el EventSource en vuelo (es.close()), cancela el sondeo de resultados y restablece el estado; solo se muestra mientras un scan corre. #21: el botón Scan se desactiva + recibe aria-busy mientras corre y Stop aparece, en ambas rutas de stream (streamTo de una fase y runScanAll multifase — esta última solo finaliza la ejecución en el done terminal, final !== false). #24: un fallo del SSE ya no es solo un toast de 3,5 s; ahora un banner persistente role=alert muestra el error con una acción de reintento (reinvoca la última función de ejecución), limpiado en la siguiente ejecución. 8 nuevas claves i18n × 8 idiomas; +7 tests: test: tests/scan-sse-a11y.test.mjs. 660 → 667. Detalle en CHANGELOG.md.
[1.45.0] — 2026-05-18
WS2 #3 — pestañas de #/config: patrón WAI-ARIA Tabs completo. Las tres pestañas de #/config (API keys / Profile / Modes) eran <button class="tab-btn"> simples con activación solo por clic: sin role, sin aria-selected, sin modelo de teclado (UX-audit HIGH #3, WCAG 4.1.2 / 2.1.1). fix(a11y): config.js tabs implement role=tablist/tab/tabpanel — ahora un contenedor role=tablist con aria-label; cada pestaña role=tab + id + aria-controls + aria-selected (sincronizado en activate()) + tabindex itinerante (0 activa / -1 resto); el panel role=tabpanel + tabindex=0 + aria-labelledby siguiendo a la pestaña activa. Navegación de teclado completa: ←/→/↑/↓ (con envolvente) + Home/End mueven el foco Y activan. El gancho CSS heredado .tab-btn.is-active se conserva. +1 clave i18n × 8 idiomas (config.tablistLabel); +7 tests: test: tests/config-tabs-aria.test.mjs. Además, una corrección solo de pruebas: fix(test): retarget 2 stale auto-pipeline smoke tests — dos smoke tests de Playwright-e2e anteriores a v1.34 afirmaban un modal transitorio que el botón “Auto-pipeline” del dashboard dejó de abrir en v1.34.0 (→ Router.go('/auto')); llevaban en rojo en el job de CI Playwright-e2e separado. Reorientados a la pantalla #/auto. 653 → 660. Detalle en CHANGELOG.md.
[1.44.0] — 2026-05-18
WS2 #4 + #9 — confirmación con foco atrapado antes de sobrescrituras destructivas de archivos del proyecto padre. Dos HIGH de la auditoría de UX, ambos con pérdida de datos: (#4) config.js saveProfileRaw/saveModesRaw reemplazaba el config/profile.yml / _profile.md completo del padre sin confirmación; (#9) tracker.js Normalize/Dedup/Merge reescribía data/applications.md del padre in situ sin confirmación. fix(a11y/safety): UI.confirm() gate before whole-file parent overwrites — nuevo UI.confirm() en public/js/api.js, un diálogo con foco atrapado que reutiliza la infraestructura modal WAI-ARIA existente (un gancho _onClose hace que Esc / backdrop / × / Cancel resuelvan todos false; el foco recae por defecto en Cancel; devuelve Promise<boolean>; NO el confirm() nativo). Las tres llamadas destructivas quedan ahora protegidas antes de su escritura. 8 nuevas claves i18n × 8 idiomas (el marcador {op} se preserva verbatim); +8 tests: test: tests/confirm-gate.test.mjs, 644 → 652. Detalle en CHANGELOG.md.
[1.43.0] — 2026-05-18
Solicitado por el usuario — career-ops-ui open + autostart que trae el navegador al frente. Tras setup/run, un open/xdg-open pelado dejaba la pestaña del dashboard en segundo plano cuando el navegador ya estaba abierto, obligando al usuario a buscarla. feat(cli): career-ops-ui open — open AND raise the dashboard tab — el nuevo scripts/open-dashboard.mjs construye la URL desde HOST/PORT (reescribiendo un bind 0.0.0.0 a loopback), opcionalmente espera a /api/health, abre el navegador por defecto y luego lo fuerza al frente — osascript en macOS activando el que esté corriendo de Chrome/Brave/Edge/Safari/Arc/Firefox, xdg-open+wmctrl en Linux, start en Windows. Expuesto como el verbo career-ops-ui open (alias dash, focus). El autostart de bin/start.sh ahora delega en él para que la pestaña se traiga al frente automáticamente; NO_OPEN=1 desactiva el auto-open en arranques headless/CI. README ×8 + help §1 ×8 actualizados; +8 tests: test: tests/open-dashboard.test.mjs, 636 → 644. Detalle en CHANGELOG.md.
[1.42.0] — 2026-05-18
WS2 corrección n.º 2 — ruta muerta #/portals → enlace profundo a config. #/portals era una ruta no registrada que renderizaba la vista 404, pese a ser una URL plausible de marcador/tecleo para gestionar fuentes de portales (HIGH n.º 2 de la auditoría UX). fix(router): #/portals 404 → alias to config + Regional-sources deep-link — se añadió portals: 'config' a ALIASES de router.js (mismo patrón de estabilidad de marcadores que settings→profile), así que ahora resuelve a la vista config con el ítem de navegación config activo. Cuando existe un grupo Regional-sources, la vista (config.js) detecta el hash #/portals, fuerza la apertura de ese grupo <details>, lo desplaza a la vista y mueve el foco a su summary (anulando el foco h1 por defecto), de modo que el usuario aterriza justo en los controles de fuentes de portales; nunca renderiza un grupo regional vacío solo por el alias. help-bundle §5 × 8 obtuvo una nota de atajo; +1 test de router: test(router): portals→config alias guarantee en router.test.mjs, 635 → 636. Detalle en CHANGELOG.md.
[1.41.0] — 2026-05-18
WS2 — auditoría sénior de UX/usabilidad + corrección transversal de gestión de foco. Una auditoría heurística de más de 10 años (Nielsen × WCAG 2.2 AA × convenciones del proyecto) de las 17 rutas produjo una cola de 40 hallazgos ordenada por severidad (.planning/.../UX-AUDIT.md); HIGH→MEDIUM→LOW se entregan ahora una corrección por release. Esta release aterriza el HIGH transversal n.º 1. Correcciones: fix(a11y): move focus to the new view on every route change — router.js render() reemplazaba #content en cada hashchange pero nunca movía el foco, así que los usuarios de teclado / lector de pantalla quedaban en el nodo destruido y perdían su sitio (WCAG 2.4.3 Focus Order / 4.1.3 Status Messages — transversal, afectaba las 17 pantallas); el nuevo focusNewView(content) enfoca el primer h1/.page-title de la vista nueva (anuncio SR conciso + orden de foco correcto), haciendo el encabezado enfocable (tabindex=-1) si hace falta y recurriendo a #content; el primer pintado se omite para no pelear con el skip-link; cableado en las rutas de render de éxito y error; verificado en vivo: tras navegar, document.activeElement es el H1.page-title de la vista nueva. Tests: test(router): focus-management static guarantees — 4 casos en router.test.mjs (helper definido, destino-encabezado + fallback a content, guarda de salto en primer pintado, ≥2 puntos de llamada); 631 → 635. Detalle en CHANGELOG.md.
[1.40.0] — 2026-05-18
WS8.3 — barrido de actualización de docs + corrección de career-ops-ui help + endurecimiento de askSecret. Correcciones: fix(cli): career-ops-ui help no longer leaks shell source — el dispatcher imprimía su comentario de cabecera con sed -n '2,12p', pero la línea 12 (set -euo pipefail) es código, no comentario, así que career-ops-ui help (y el texto de uso de verbo desconocido) terminaba con una línea set -euo pipefail perdida; acotado a 2,11p (el bloque de comentario) en los casos help y *); help sale 0, verbo desconocido sale 2 — verificado. fix(cli): scripts/init.mjs key entry never echoes — el seguimiento de v1.39.0 reemplazó la máscara cosmética de readline por un lector real en modo raw: setRawMode(true) + línea con búfer para que los bytes de clave tecleados/pegados nunca lleguen al terminal (sin fuga en scrollback / tmux / pantalla compartida); un FSM completo de escape VT consume cada secuencia CSI/SS3/OSC/DCS/SOS/PM/APC para que las teclas de flecha y función no corrompan el secreto; stdin se inyecta por dependencia, así que el fallback no-TTY se prueba unitariamente sin tocar el global; iterado hasta un LGTM limpio de la revisión IA. Documentación: README ×8 — la antigua sección “instalación en un comando” se reemplaza por una sección destacada “Lanzar e inicializar en un comando” (el one-liner de curl más la cadena explícita del CLI career-ops-ui: clone → npm link → setup → init → doctor → run → help, la explicación del asistente de proveedor, la forma CI --provider --anthropic-key --yes y la nota de LLM_PROVIDER); las 8 insignias de README actualizadas de v1.22–v1.24 / tests-461–474 a v1.40.0 / tests-631 (insignia e2e hecha no numérica para evitar un conteo inventado); help-bundle ×8 §1 — se añade un callout “Lanzamiento e init en un comando” al inicio del manual de inicio rápido (antes de “A. Setup”) en los 8 idiomas; paridad de secciones H2 preservada (17 cada uno — gate de CI verde). Tests: test(init): non-TTY askSecret fallback — provider-selector.test.mjs gana un caso de stdin por DI que comprueba que askSecret delega en ask() plano (paridad de trim) sin TTY y sin mutar el global compartido; 629 → 631. Detalle en CHANGELOG.md.
[1.39.0] — 2026-05-18
WS8.2 — selector de proveedor LLM + clave OpenAI/Codex + asistente init interactivo. LLM_PROVIDER (auto|claude|gemini) + OPENAI_API_KEY en env-config (secreto). providerOrder() consultado por los 6 gate-sites de llm.mjs vía _provGate(); sin cambio de comportamiento para auto. Select + campo en #/config. scripts/init.mjs ahora es un asistente real (escribe parent .env por la ruta validada). 7 tests. 622 → 629. README ×8 / fold canónico = WS8.3/WS10. Detalle en CHANGELOG.md.
[1.38.0] — 2026-05-17
WS8.1 — dispatcher CLI unificado + verbo doctor. bin/career-ops-ui.sh despacha setup/run/doctor/init/help. scripts/doctor.mjs reutiliza el motor /api/health exacto (createApp in-process → reporte terminal); exit 0 sólo si todos los checks REQUERIDOS pasan. docs/sdd + help §1 ×8. 6 tests. 616 → 622. README quickstart ×8 = WS8.3 (paso final). Detalle en CHANGELOG.md.
[1.37.0] — 2026-05-17
WS7 — revisión AI pre-commit en el workflow git. Floor determinista (fail-HARD): bloquea .env/secretos staged, patrones de clave en el diff, .also( en vistas staged, fallo node --check. Capa AI (fail-SOFT): claude -p sobre el diff si el CLI está disponible y AI_REVIEW != off. .githooks/pre-commit + prepare cablea core.hooksPath. Nunca --no-verify. docs/sdd. 6 tests. 610 → 616. Detalle en CHANGELOG.md.
[1.36.0] — 2026-05-17
WS6.3 — pestaña Modes: blob crudo → editor por secciones. WS6 completo. modes/_profile.md se edita por sección ## (un textarea plegable por encabezado). Server splitProfileSections byte-exacto; PUT { sections } fusiona solo las secciones nombradas — preámbulo + secciones ajenas + orden se conservan byte a byte. Encabezado desconocido → 400. Ruta raw { markdown } intacta. i18n 5 claves ×8. help §2 ×8. 6 tests nuevos. 604 → 610. WS6 cerrado (API-keys/Profile-escalares/Profile-arrays/Modes-secciones todo estructurado). Detalle en CHANGELOG.md.
[1.35.0] — 2026-05-17
WS6.4 — editores de arrays del Profile + auditoría WS6.2 de API-keys. PUT /api/profile acepta { arrays } (combinable con { fields }): Target roles/Superpowers (listas), Archetypes (name/level/fit), Proof points (name/url/hero-metric). Misma garantía merge-not-replace; filas vacías descartadas; lista vacía elimina la clave. 4 editores add/remove en #/config. i18n 6 claves ×8. Auditoría: server KNOWN_KEYS ≡ client FIELDS, sin gap. 7 tests nuevos. 597 → 604. Detalle en CHANGELOG.md.
[1.34.0] — 2026-05-17
WS5 — pantalla Auto-pipeline de un clic (#/auto). El modal de auto-pipeline pasó a una página dedicada y enlazable. Un clic ejecuta validar→obtener→evaluar→guardar informe→tracker vía SSE. Stepper accesible (lista ordenada, aria-current, live-region), deep-links a informe/tracker, modo manual sin key, enlazable #/auto?url=…&go=1. Entrada en sidebar; botón ✨ del dashboard ahora va aquí. i18n 14 claves ×8. help §1 ×8 + README ×8. 8 tests nuevos. 589 → 597. Detalle en CHANGELOG.md.
[1.33.0] — 2026-05-17
WS4 — auditoría de paridad con career-ops 1.8.0 + location_filter. El scan.mjs del proyecto padre ganó location_filter (#570); los scanners in-process de web-ui no delegan en él, así que no fluía. Nuevo server/lib/location-filter.mjs replica la semántica verbatim; integrado en ambos scanners. Doc help §5 ×8. 8 tests nuevos. 581 → 589. Resto del delta padre clasificado en PARENT-PARITY.md (FLOW/CLI-ONLY/N/A). Detalle en CHANGELOG.md.
[1.32.0] — 2026-05-17
Pestaña Profile de #/config — blob YAML crudo → formulario por campos (WS1). 3 secciones plegables (Candidato / Narrativa / Compensación), 14 rutas escalares. El guardado por campos hace merge en config/profile.yml: arquetipos, proof points y claves propias se conservan intactos. Escape-hatch raw-YAML retenido en Advanced (preserva comentarios). 23 claves i18n ×8. 7 tests nuevos (incl. invariante de supervivencia de claves desconocidas). 574 → 581. Detalle en CHANGELOG.md.
[1.31.0] — 2026-05-17
Sync con career-ops 1.8.0 — #/batch expone --model + --start-from. El proyecto padre subió 1.7.1 → 1.8.0; batch-runner.sh ganó --model NAME (#504) y --start-from N. web-ui los expone en #/batch (campos Model y Desde #) con validación defense-in-depth en el servidor (charset para model, entero 1..100000 para start-from). i18n ×8. 7 tests nuevos. 567 → 574. Detalle completo en CHANGELOG.md.
[1.30.0] — 2026-05-14
Paginador en #/scan — reemplaza el truncamiento «primeros 200 de N» de v1.12.
Pre-v1.30 la tabla de resultados de scan estaba acotada a las primeras 200 filas filtradas con una nota «Showing first 200 of N» al pie. Las filas 201..N eran inalcanzables desde la UI. v1.30.0 cambia el cap por UI.paginate (mismo helper que #/tracker / #/reports / #/activity). PAGE_SIZE = 200 conserva la densidad visual previa; orden boosted-to-top estable entre páginas (orden COMPLETO y luego paginación); reset automático a página 1 al cambiar filtros. Clave i18n obsoleta scan.shownTop eliminada (×8 locales). 9 nuevos casos de prueba en tests/scan-paginator.test.mjs (7 canarios estáticos + 1 tabla lógica con 6 casos límite + 1 cómputo del resumen). 558 → 567 unit + acceptance (+9). Detalles completos en CHANGELOG.md.
[1.29.2] — 2026-05-14
Hot-fix: 🌐 Scan con source=both solo ejecutaba la fase EN. La fase RU se eliminaba silenciosamente.
El cliente SSE (public/js/api.js:156) cerraba el EventSource en el PRIMER evento done, pero el servidor emite uno por fase en source=both. La fase RU arrancaba e inmediatamente se cancelaba. Fix: el servidor marca cada done con final: true|false; el cliente cierra solo cuando final !== false. Retrocompatible — los productores de fase única sin final siguen cerrando como antes. 547 → 558 unit + acceptance (+11 nuevos). Detalles completos en CHANGELOG.md.
[1.29.1] — 2026-05-14
Guía detallada del usuario para configurar los 5 portales RU en el help-bundle §5, en las 8 locales.
Nueva subsección “Configurar los portales rusos — guía detallada” dentro de §5 (Portals & sources): tabla inventario de 5 fuentes con auth y restricciones geográficas, paso a paso para localizar y editar portals.yml, ejemplo YAML completo de las 5 fuentes, colisión con la lista negativa con corrección, cómo deshabilitar una fuente, cómo verificar vía 🌐 Scan + log SSE. §17 (shipped en v1.29.0) cubre el flujo del desarrollador; §5 v1.29.1 cubre el flujo del usuario final. 540 → 547 unit + acceptance (+7 nuevos). Detalles completos en CHANGELOG.md.
[1.29.0] — 2026-05-14
Scanner de portales rusos pasa de 2 a 5 fuentes; registry + dropdown dinámico; nueva sección §17 “Cómo añadir un nuevo portal”.
- 3 nuevos adaptadores RU:
Trudvsem(API open-data del gobierno, sin auth ni geo-gate),GetMatchyGeekJob(HTML scrape con parser defensivo —[]si no parsea, nunca throw en 200 sano). - Source registry en
server/lib/sources/registry.mjs— única fuente de verdad consumida por dispatcher + endpoint + dropdown. Pre-v1.29 el listado de fuentes vivía hardcoded en TRES lugares. - Nuevo endpoint
GET /api/scan/sourcesconCache-Control: max-age=60— el SPA reconstruye el dropdown del filtro de fuente al cargar#/scan. - Help-bundle §17 nueva en las 8 locales: «Cómo añadir un nuevo portal» (plantilla de adaptador, entry del registry, dispatcher, test mockeado,
portals.yml). russian_portals.sourcespor defecto cambia de["hh", "habr"]a las 5 fuentes; si tuportals.ymlya listasources:explícitamente, debes añadir las 3 nuevas a mano.- Tests: 520 → 540 (+20). Detalles completos en
CHANGELOG.md.
[1.28.1] — 2026-05-14
Hot-fix: router 404 con hashes que llevan ?query. Fila HH_USER_AGENT eliminada de health.
Antes de v1.28.1 Router.go('/evaluate?url=…') producía un hash cuyo primer split('/') era el literal "evaluate?url=…", que nunca coincidía con una ruta registrada → __not_found__ (404). Fix de una línea: hash.split('?')[0] antes del split del nombre. Cubre dos clicks reportados: #/pipeline → ▶ y “App settings → Modes”. La fila opcional HH_USER_AGENT se elimina de /api/health (la pista 403-fuera-de-Rusia sigue en help-bundle §16 y se emite en stderr al escanear). 515 → 520 unit + acceptance (+ 5 nuevos). Detalles completos en CHANGELOG.md.
[1.28.0] — 2026-05-14
Alineación de docs + nuevo control --max-retries N en #/batch. Cierra las dos issues abiertas levantadas por qa/QA-PROMPT-docs-vs-app.md.
- Issue #2 —
#/batchahora expone un campo numérico “Max retries” (1–10) que sólo se habilita cuando “Retry failed” está activado. El servidor parsea + valida 1≤N≤10 (los valores fuera de rango se descartan silenciosamente) y omite--max-retriessin--retry-failed. 7 casos de prueba entests/batch-max-retries.test.mjs. 2 claves i18n nuevas × 8 locales. - Issue #1 — la lista de CLIs de IA en los 8 help-bundles y 8 READMEs se alinea con el canon de career-ops.org/docs (Claude Code · Codex · OpenCode · Qwen CLI), con una frase localizada: «otras CLIs compatibles con Claude también funcionan vía la misma superficie de slash-comandos». El bullet “Multi-CLI” del README sobre los archivos shim de web-ui se conserva intacto (describe otra superficie). 2 nuevos canarios en
tests/canonical-docs-coverage.test.mjs. - 506 → 515 unit + acceptance (+ 9 nuevos). Playwright 32/32 sin cambios. Detalles completos en
CHANGELOG.md.
[1.27.0] — 2026-05-14
Pulido cosmético + a11y: deduplicar la entrada de barra lateral #/dashboard.
En la barra lateral, el logo de marca (<a class="logo" href="#/dashboard">) y el primer ítem de navegación apuntaban a la misma ruta. Los lectores de pantalla anunciaban «Dashboard» dos veces y los usuarios de teclado tenían un tab-stop redundante. El bloque de marca ahora es un <div class="logo"> plano; el ítem de navegación sigue siendo el único enlace a #/dashboard. 506 / 506 unit + 32 / 32 Playwright — sin cambios. Detalles completos en CHANGELOG.md.
[1.26.1] — 2026-05-14
Hot-fix WCAG 2.5.5 — altura mínima 44 px de .btn restaurada.
v1.26.0 perdió la declaración min-height: 44px en .btn; los botones del header renderizaban a 39-41 px (violación WCAG 2.5.5). v1.26.1 restaura el suelo de 44 px + flex-shrink: 0 + line-height: 1.2. 502 → 506 unit, 32/32 Playwright sin cambios. Detalle en CHANGELOG.md.
[1.26.0] — 2026-05-14
Pirámide de tests + cobertura ≥ 93 % línea.
Adopta la estructura de 4 niveles (unit → functional → acceptance → e2e) según el backlog de v1.25. Añade 22 tests nuevos cubriendo los mayores gaps de cobertura de v1.25 (jds.mjs 61.64 % → 100 %, ramas de rechazo en auto-pipeline). Introduce el directorio tests/acceptance/ para tests de jornada de usuario multi-endpoint. 480 → 502 unit + acceptance, 32/32 Playwright sin cambios. Detalle completo en CHANGELOG.md y docs/architecture/TESTING.md.
[1.25.0] — 2026-05-14
Cortocircuito manual del auto-pipeline + ajuste cosmético del dashboard + nivelación de paridad del CHANGELOG. Cierra G-014 (el auto-pipeline ignoraba mode: 'manual'), G-012 (deriva de paridad del CHANGELOG — 6 locales iban 2 versiones por detrás) y la duplicación cosmética del glifo ✨ ✨ en el dashboard. G-003 (renombrado de README.cn.md) ya estaba cerrado de facto — el repositorio solo contiene README.zh-CN.md. G-005 (realineamiento del bloque de informe A-G → A-F) requiere un commit coordinado en el proyecto padre y queda diferido.
🛡️ G-014 — Cortocircuito de mode: 'manual' en el auto-pipeline
fix(auto-pipeline): G-014 — honour mode:'manual' short-circuit(server/lib/routes/auto-pipeline.mjs:158-195) — antes de v1.25 el endpoint siempre llamaba a un LLM. Pasarmode: 'manual'(replicando/api/evaluatedesde v1.10.2) era ignorado de forma silenciosa y la petición quedaba colgada de 1 a 3 minutos contra Anthropic. Ahora el manejador:- Acepta
modeYevalModepara retrocompatibilidad. Cualquiera de los dos con valor'manual'dispara el cortocircuito. - Emite las 5 etapas SSE con
status: 'done'/status: 'skipped'. Sin fetch. Sin llamada al LLM. Sin los 0,05 $ por petición. - El payload
donelleva{ mode: 'manual', prompt: <buildEvaluationPrompt scaffold>, message }— la SPA puede renderizarlo como la actual tarjeta de prompt manual de/api/evaluate.
- Acepta
- Cierra un riesgo de DoS en
HOST=0.0.0.0: anteriormente, incluso conllmRateLimitlimitando a 10 req/60 s/IP, 10 atacantes × 10 peticiones = 50 $/min en consumo de Anthropic. El cortocircuito actúa antes de que el decremento del limitador de tasa cuente como llamada real. - Pruebas —
tests/auto-pipeline-manual-mode.test.mjs: 3 tests confirman (1) quemode: 'manual'responde en menos de 2 s con las 5 claves de etapa, (2) que incluso conANTHROPIC_API_KEYconfigurado el cortocircuito sigue activándose (el síntoma original), (3) que los llamadores antiguos conevalMode: 'manual'continúan funcionando.
📝 G-012 — Nivelación de paridad del CHANGELOG (6 locales × 2 versiones ausentes)
docs(changelog): backfill v1.23.0, v1.24.0, v1.24.1, v1.25.0 in 6 lagging locales— antes de v1.25 solo EN tenía v1.23-v1.24; RU iba 1 versión por detrás y los otros 6 iban 2 versiones por detrás. v1.25 despliega agentes de traducción en paralelo (replicando el patrón de v1.23) para incorporar las cuatro entradas enCHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md. RU recibe v1.24.0 + v1.24.1 + v1.25.0 (ya tenía v1.23.0 del ciclo v1.23).feat(ci): scripts/check-changelog-parity.mjs gate— falla la build si la entrada más reciente de cualquier locale del CHANGELOG es más antigua que la canónica en EN. Conectado anpm run test:ci. La deriva preexistente de G-012 se habría detectado a sí misma en el momento mismo de cruzar el umbral de EN.
✨ Cosmético — deduplicación del doble glifo del dashboard
fix(dashboard): dedup ✨ glyph in auto-pipeline button label(public/js/lib/i18n-dict.js:219) —dash.autoPipelinellevaba un✨inicial en la cadena de cada locale Ypublic/js/views/dashboard.js:58anteponía otro✨en la vista. Resultado: el botón se renderizaba como✨ ✨ Auto-pipeline …. v1.25 retira el glifo inicial de la entrada DICT de cada locale; el prefijo de la vista pasa a ser la única fuente. La misma pasada de auditoría barrió el resto del bundle i18n — no se hallaron otros patrones de doble glifo.
🚫 Diferido a una versión futura
- G-005 — Realineamiento del bloque de informe A-G → A-F por canónico career-ops.org/docs — requiere un commit coordinado en el proyecto padre
santifer/career-ops(reescribirmodes/oferta.mdpara emitir A=Role, B=CV-match, C=Strategy, D=Comp, E=Personalization, F=STAR — eliminando C-Risks/G-Legitimacy como bloques separados). v1.25.0 entrega el lado web-ui listo para el nuevo esquema (reports.jsacepta letras de bloque arbitrarias desde v1.13). Se rastrea para la próxima ventana de versión en que padre e hijo puedan aterrizar juntos. - G-003 — Renombrado de
README.cn.md→README.zh-CN.md— verificado durante la preparación de v1.25: el repositorio ya contieneREADME.zh-CN.md(no hay ningúnREADME.cn.mdhuérfano bajo el worktree). El hallazgo de G-003 estaba obsoleto.
🧪 Pruebas
- 477 → 480 unit (+3 de PR-B
auto-pipeline-manual-mode.test.mjs). - 32/32 Playwright sin cambios.
npm run test:ciejecuta ahoranpm test+check-no-also-leftovers.mjs+check-changelog-parity.mjs.
Verificación
$ npm run test:ci
# 480 / 480
# ✓ no .also( leftovers in views/
# ✓ CHANGELOG parity: all 8 locales at v1.25.0
# G-014 — el modo manual responde en < 2 s incluso con ANTHROPIC_API_KEY configurado:
$ ANTHROPIC_API_KEY=sk-ant-test PORT=4317 npm start &
$ sleep 3
$ time curl -sS -X POST -H 'Content-Type: application/json' \
-d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/x","mode":"manual"}' \
http://127.0.0.1:4317/api/auto-pipeline | head -20
# real 0m0.1xx s (antes 1-3 min)
# event: start … event: step (×5) … event: done {"mode":"manual","prompt":"…"}
# G-012 — cada CHANGELOG localizado incluye la entrada v1.25.0:
$ grep -c '^## \[1.25.0\]' CHANGELOG*.md
# 8 ficheros, cada uno → 1
# Cosmético — glifo del dashboard:
$ grep "dash.autoPipeline" public/js/lib/i18n-dict.js
# Ningún ✨ inicial en ningún valor de locale (la vista aporta el único glifo)
Cambios incompatibles
Ninguno. mode: 'manual' es opcional; los llamadores antiguos con evalMode: 'manual' continúan funcionando sin cambios.
Fuera de alcance (v1.26+)
| Ítem | Notas |
|---|---|
| G-005 — Realineamiento A-F del bloque de informe | Requiere un commit coordinado del padre (santifer/career-ops reescribe modes/oferta.md). |
| Ejecución en vivo de los sub-tests visuales del escenario QA 31 | Requieren un agente con navegador (Claude Cowork). Cubierto parcialmente por el smoke de Playwright. |
Objetivo de 400 LOC en i18n-dict.js |
Fixture de traducción — exento por política. Dividirlo añadiría peticiones HTTP sin un bundler. |
[1.24.1] — 2026-05-14
Parche urgente: caída de #/config en los 8 locales (G-015).
🚑 Hot-fix crítico
fix(config): G-015 — replace removed Element.prototype.also call in config.js(public/js/views/config.js:371) — N-2 de v1.22.0 retiró el monkey-patch globalElement.prototype.alsoy migrócv.jsa un patrón de instrucciones libres, pero se olvidó deconfig.js. Resultado:#/configse caía en la primera invocación en cada locale conc(...).also is not a function. v1.24.1 aplica el mismo patrón de migración decv.js:188-201— extraer el árbol a unaconst root = c(...), ejecutar el bloque de activación por separado y luegoreturn root;.
🛡️ Verja de CI
feat(ci): scripts/check-no-also-leftovers.mjs sweep— recorre cada archivo bajopublic/js/views/y falla la build ante cualquier llamada.also((se permiten las referencias en comentarios). Conectado al nuevo scriptnpm run test:ci. Una futura reversión de la eliminación del monkey-patch no podrá reintroducir la misma regresión en silencio.
🧪 Pruebas
test: tests/config-view-syntax.test.mjs— tres guardas:- parsea
config.jsvíanode:vm.Script(detecta regresiones a nivel de sintaxis sin necesidad de Playwright) - comprueba que no sobrevive ningún
.also(fuera de comentarios - comprueba que están presentes los anclajes de migración
const root = c(...)/return root;
- parsea
- 474 → 477 unit (+3) + 32/32 Playwright sin cambios.
Verificación
$ npm run test:ci
# 477 / 477
# ✓ no .also( leftovers in views/
# Smoke en navegador:
$ open http://127.0.0.1:4317/#/config
# → se renderiza con normalidad, sin tarjeta "is not a function". Lo mismo en cada locale.
Fuera de alcance (diferido a v1.25)
- G-014, G-012, G-005, G-003 — ver la entrada v1.25.0 más arriba para el lote.
[1.24.0] — 2026-05-14
Renovación del help-bundle en profundidad de contenidos + ejecución en vivo del escenario QA 31 + CHANGELOG RU de cabo a rabo. Cierra los dos ítems que la tabla “Fuera de alcance” de v1.23.0 difirió a v1.24: la renovación completa en profundidad de contenidos de los 8 help bundles desde las 5 URL canónicas de career-ops.org/docs (era solo cobertura de URL desde v1.11.x) y la ejecución en vivo del escenario QA 31 contra un servidor activo (era “requiere agente con navegador + credenciales LLM” — resultó que 6 de 6 sub-tests son alcanzables vía curl + grep; solo los sub-tests visuales necesitan navegador).
📖 Renovación en profundidad de contenidos del help-bundle
docs(help): refresh en.md from 5 canonical career-ops.org/docs URLs(docs/help/en.md) — antes de v1.24 el bundle EN tenía 1113 líneas y enumeraba las 5 URL canónicas en el front-matter pero no las desarrollaba en el cuerpo. v1.24 obtiene las 5 URL vía WebFetch y profundiza las secciones H2 correspondientes:- About career-ops (front-matter) — añadidos los principios (soberanía de datos, agnosticismo de IA, control humano), bloque “What career-ops is NOT”, inventario de conceptos ampliado de 6 a 10 filas (añadidos Proof points, JD store, Interview-prep, Batch additions).
- §5 Portals — añadido el bootstrap canónico
cp templates/portals.example.yml portals.yml, aclarados los campos obligatorios frente a los opcionales por cada entradatracked_companies. - §7 Scan — añadida la nota “no se consumen tokens de IA” para la Opción A, lista de órdenes de seguimiento (
apply/contacto/deep/tracker). - §14 Apply checklist — dividida en modo SPA checklist frente a Manual-vs-Playwright-assisted frente al flujo CLI completo (los 8 pasos numerados canónicos desde
/career-ops apply <company>hastaSubmitted.con transición automáticaEvaluated → Applied); la subsección batch evaluate tiene ahora tabla de esquema TSV + las 4 banderas documentadas +merge-tracker.mjs --dry-run; la subsección Playwright Setup enumera órdenes de instalación, registro MCP, alternativa.claude/settings.local.json, nota headless-by-default.
- Paridad de 16 secciones H2 preservada (el test CI
help-ui.test.mjs::section-parityexige exactamente 16 secciones H2 en los 8 locales). - Cada una de las 5 URL canónicas aparece ≥ 2 veces en el bundle (lo impone el test CI
canonical-docs-coverage.test.mjs). Recuento por URL tras v1.24:what-is-career-ops× 4,scan-job-portals× 5,apply-for-a-job× 3,batch-evaluate-offers× 5,set-up-playwright× 3. docs(help): translate the v1.24 deepening to 7 non-EN locales— 7 agentes de traducción en paralelo. Cada locale destino (es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW) recibe un bundle renovado que refleja la estructura EN sección por sección, preserva verbatim los bloques de código / URL / rutas de fichero / etiquetas de botón (📁 Upload CV / 🌐 Scan now / ▶ Evaluate / 📄 Generate PDF / 💾 Save) y las abreviaturas inglesas (CSP, SSRF, TOCTOU, WCAG, ATS, JD, SSE, REST, API), y traduce la profundización a un estilo técnico nativo de calidad publicable en el idioma destino.
🧪 Escenario QA 31 — ejecución en vivo (6/6 PASS)
-
docs(qa): append last-verified live-execution log to qa/claude-cowork-browser-test-prompt.md— antes de v1.24 el escenario 31 estaba documentado pero nunca se había ejecutado contra un servidor real (diferido como “requiere agente con navegador + credenciales LLM”). v1.24 ejecutó los 6 sub-tests contrahttp://127.0.0.1:4317:Sub Descripción Estado 31.1 Umbrales de puntuación en los help bundles ✅ PASS (4.5 × 3, 4.0 × 9, 3.5 × 6 menciones en docs/help/en.md)31.2 Endpoints del flujo de scan ✅ PASS ( /api/stream/scan-{en,ru}+/api/scan-ru/config→ 404;/api/scan/regional/config→ 200)31.3 Checklist de /api/apply-helper✅ PASS (el cuerpo contiene career-ops apply+ avisoauto-submit)31.4 Endpoint /api/batch✅ PASS (claves [exists, runnerExists, raw, rows, additions])31.5 Disponibilidad de Playwright ✅ PASS ( /api/healthreportaPlaywright (parent node_modules) ok: true, value: installed)31.6 Cobertura de URL en help-bundles (5 URL × 8 locales) ✅ PASS (40 / 40 ✓) Los sub-tests exclusivamente visuales (requieren navegador) se marcan aparte en el prompt QA — siguen siendo ejecutables vía Claude Cowork o
npm run test:e2e:browser.
🌐 CHANGELOG RU de cabo a rabo (seguimiento de M-9)
docs(translate): CHANGELOG.ru.md retry agent — full body translation(CHANGELOG.ru.md) — la versión v1.23.0 se publicó con el agente de reintento del CHANGELOG RU aún en curso (había caído con un error de socket y fue redespachado). v1.24 recoge la traducción completa de 1542 líneas del agente: cada entrada de v1.23.0 → v1.6.0 tiene un cuerpo en ruso de calidad publicable, sin más parches provisionales con cuerpo en EN. La disciplina estilística iguala la renovación de calidad del README en v1.22.0: “функциональность” / “возможности” / “поведение” reemplazan al torpe “функционал”; “через” / “с помощью” reemplazan “при помощи”; voz activa frente a pasiva; “эндпоинт”, “лимит запросов”, “состояние гонки”, “санитайзинг” como términos canónicos; abreviaturas inglesas (TOCTOU, CSP, SSRF, WCAG, ATS, JD, SSE, REST, API) preservadas.
🧪 Pruebas
- 474 / 474 unit + 20 / 20 smoke E2E + 32 / 32 Playwright. Cero deltas en pruebas de comportamiento; cada aserción CI del help-bundle (16 secciones H2 × 8 locales, 5 URL × ≥ 2 menciones, suelo de contenido) sigue en verde.
Verificación
$ npm test # 474 / 474
# Profundización del help-bundle:
$ wc -l docs/help/en.md
# ~1270 líneas (antes 1113 — profundizado, no inflado)
$ for url in what-is-career-ops scan-job-portals apply-for-a-job \
batch-evaluate-offers set-up-playwright; do
echo -n "$url: "
grep -c "$url" docs/help/en.md
done
# what-is-career-ops: 4
# scan-job-portals: 5
# apply-for-a-job: 3
# batch-evaluate-offers: 5
# set-up-playwright: 3
# Escenario 31.6 — 40/40 cobertura de URL:
$ for lang in en es pt-BR ko ja ru zh-CN zh-TW; do
echo -n "$lang: "
for url in what-is-career-ops scan-job-portals apply-for-a-job \
batch-evaluate-offers set-up-playwright; do
curl -sS "http://127.0.0.1:4317/api/help/$lang" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('markdown',''))" \
| grep -q "$url" && echo -n "✓ " || echo -n "✗ "
done
echo
done
Cambios incompatibles
Ninguno.
Fuera de alcance (v1.25+)
| Ítem | Notas |
|---|---|
| Ejecución en vivo de los sub-tests visuales del escenario 31 | Requieren un agente con navegador (Claude Cowork o npm run test:e2e:browser). Fuera de alcance para la ejecución solo con curl; cubierto por el smoke de Playwright existente. |
| Traducción del cuerpo del CHANGELOG RU de las entradas más antiguas (v1.5.x y anteriores) | El agente de reintento solo cubrió desde v1.6.0 en adelante. Las entradas pre-v1.6 (v1.5.x, etc.) — si alguna vez existieron — permanecen como contenido preexistente. |
| Regresión visual sobre capturas del dashboard tras futuros cambios de la SPA | scripts/capture-dashboard-screenshots.mjs regenera los PNG por locale; actualmente no hay diff automatizado. |
[1.23.0] — 2026-05-14
División de i18n + arreglo CI del banner de conexión + capturas del dashboard localizadas + cada parche provisional del backlog cerrado. Entrega los tres ítems que la tabla “Fuera de alcance” de v1.22.0 marcó para v1.23 (cuerpos del CHANGELOG por locale M-9, división LOC de i18n.js N-1, auditoría de contenido del help-bundle) más un parche urgente para el test smoke E2E que dejó en rojo el CI de la rama principal tras v1.22.0.
🚑 Hot-fix de CI — recuperación del banner de conexión
-
fix(client): reset health-poll cadence + visibilitychange eager re-check(public/js/api.js:21-91) — el backoff exponencial M-6 de v1.22.0 era correcto (3 s → 6 s → 12 s → tope de 15 s, frente al tope original de 60 s) pero elsetTimeouten curso quedaba anclado al delay fijado previamente. Un servidor caído en t=0,1 con la primera ping en t=3 fallaba, duplicaba el delay a 6 y la siguiente sonda de recuperación no se disparaba hasta t=9. El smoke E2E “Flujo 2a: el banner de conexión aparece al caer el servidor y se oculta al recuperarse” esperaba solo 4 s y se ponía en rojo enmain.v1.23.0 remodela el bucle de polling:
- Se rastrea
_healthHandlepara quesetConnectionState(lost=true)pueda hacerclearTimeouty reprogramar con_HEALTH_MIN. La primera sonda de recuperación se dispara ahora dentro de los 3 s desde la caída, independientemente del delay que estuviera encolado. _HEALTH_MAXrebajado de 60 s a 15 s. Una pestaña en segundo plano contra un servidor caído sigue recuperándose dentro de un ciclo de polling cuando el usuario vuelve; el ahorro de ancho de banda se mantiene sustancial.document.addEventListener('visibilitychange')revuelve a sondear ansiosamente cuando la pestaña recupera el foco yconnectionLost === true— un Cmd-Tab de vuelta no espera al siguiente tick del backoff.
- Se rastrea
🧹 N-1 — División de i18n.js (por encima del objetivo de 400 LOC)
refactor(client): split DICT into i18n-dict.js (data) + i18n.js (logic)— antes de v1.23public/js/lib/i18n.jstenía 639 LOC. El grueso (líneas 23-586) era la tabla de traducciónDICT— datos estructurados puros. v1.23.0 extrae eso apublic/js/lib/i18n-dict.js(578 LOC, exento de la regla de LOC según CLAUDE.md “Exempt from these limits: generated files, migrations, test fixtures, lock files, vendored code” — las tablas de traducción califican como fixtures), dejandopublic/js/lib/i18n.jsen 86 LOC de pura lógica de módulo (muy por debajo del objetivo de 400 LOC).- Contrato del cargador:
i18n-dict.jsrellenawindow.__I18N_DICT = { … }, y luegoi18n.jslo lee dentro de la IIFE existente.public/index.htmllos carga en orden —i18n-dict.jsantes quei18n.js— de modo que la IIFE ve un DICT completamente poblado en el momento de su construcción. Fallback de DICT ausente: cada llamada at()devuelve su fallback en línea o la clave desnuda, lo que aflora una mala configuración de forma ruidosa sin tumbar la SPA. - Cableado de pruebas actualizado:
tests/i18n-coverage.test.mjs,tests/help-ui.test.mjs,tests/canonical-docs-coverage.test.mjsejecutan ahora ambos archivos a través del contexto VM de prueba (o concatenan su fuente para el barrido regex), preservando cada aserción existente.
🌐 M-9 — Traducciones del cuerpo del CHANGELOG por locale
docs(translate): 7 non-EN CHANGELOG files end-to-end— antes de v1.23CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.mdllevaban notas provisionales con cuerpo en EN para cada entrada desde v1.13.0 en adelante, con un pie remitiendo al lector al canónico EN. v1.23.0 despliega 7 agentes de traducción en paralelo — uno por locale — que reescriben cada cuerpo a un estilo técnico de calidad publicable en el idioma destino. Notas provisionales retiradas. Bloques de código, rutas de fichero, URL, cadenas con estilo de commit-message (fix(security): B-1 — …), variables de entorno y etiquetas de enlace preservadas verbatim en todos los locales.
🖼️ Capturas del dashboard localizadas en cada README
docs(readme): wire each locale README at its locale-specific PNG— antes de v1.23 soloREADME.pt-BR.mdreferenciabadashboard-pt-BR.png; los otros 6 README no ingleses seguían apuntando adashboard-en.png. Las capturas (ya tomadas en el ciclo de v1.22.0 porscripts/capture-dashboard-screenshots.mjs) estaban presentes enimages/pero sin uso. v1.23.0 actualiza la línea 14 de cadaREADME.{es,ja,ko-KR,ru,zh-CN,zh-TW}.mda su propiodashboard-<locale>.png.
🧪 Pruebas
- Los mismos 474 / 474 unit + 32 / 32 Playwright que v1.22.0. El smoke E2E pasa ahora a 20 / 20 (era 19 / 1 fallo en
maintras v1.22.0 por la regresión de recuperación del banner; la reprogramación de v1.23.0 lo cierra). - Tres pruebas existentes recableadas para acomodar la división de i18n. Cero archivos de prueba nuevos; cero aserciones eliminadas.
Verificación
$ npm test
# 474 / 474
$ npm run test:e2e
# passed: 20 failed: 0 (era 19/1 en main v1.22.0)
$ wc -l public/js/lib/i18n.js public/js/lib/i18n-dict.js
# 86 public/js/lib/i18n.js ← lógica, bajo el objetivo
# 578 public/js/lib/i18n-dict.js ← fixture de datos, exento
$ grep -h 'dashboard-' README*.md | sed -E 's/.*(dashboard-[^)]+).*/\1/' | sort -u
# dashboard-en.png (solo README.md)
# dashboard-es.png dashboard-ja.png
# dashboard-ko-KR.png dashboard-pt-BR.png
# dashboard-ru.png dashboard-zh-CN.png dashboard-zh-TW.png
# Cordura de la traducción del CHANGELOG: cada archivo de locale > 200 líneas de contenido nativo
$ wc -l CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md | grep -v total
Cambios incompatibles
Ninguno. public/index.html carga ahora dos scripts donde antes cargaba uno — quien sirva la SPA desde una CDN debe incorporar i18n-dict.js; el orden de carga del script lo impone el orden de las etiquetas <script src> en index.html. El fallback en tiempo de ejecución (DICT vacío → t() devuelve el fallback EN en línea) previene caídas duras cuando el archivo nuevo falta.
Fuera de alcance (v1.24+)
| Ítem | Notas |
|---|---|
| Renovación en profundidad del CONTENIDO del help-bundle desde career-ops.org/docs (frente a cobertura de URL) | Las 5 URL canónicas ya aparecen en cada help-bundle localizado desde v1.11.x y el escenario 31.6 del prompt QA verifica la cobertura. La renovación en profundidad del cuerpo del contenido es candidata a v1.24+. |
| Ejecución en vivo del escenario QA 31 contra un servidor activo | Requiere agente con navegador + credenciales LLM en vivo. Candidato a v1.24. |
| Barrido por componente del touch-target en los nuevos párrafos hint de la página de modos | M-1 de v1.22.0 añadió elementos <p class="field-hint"> que aún no se han verificado contra la altura mínima WCAG 2.5.5 en los 8 locales. |
[1.22.0] — 2026-05-14
Limpieza del backlog M/L/N + alineación documental + pase de calidad en traducciones. Todo el tramo medio-e-inferior de v1.20.1-BACKLOG.md se entrega en una sola versión: nueve ítems M, cinco ítems L, dos nits. Se suma una auditoría de alineación contra las cinco guías canónicas de career-ops.org/docs, una actualización de los system prompts bajo .claude/ y .github/, y un repaso de calidad de los README en los 7 locales no ingleses.
🛡️ Endurecimiento de seguridad (defensa en profundidad)
-
fix(security): M-4 — stripDangerousMarkdown consciente de entidades(server/lib/security.mjs) — la regex previa a v1.22 coincidía con<script>,javascript:,on*=como subcadenas literales.<script>,javascript:y<img src="data:image/svg+xml,<svg onload=…>">se colaban sin filtro. El saneamiento ahora decodifica entidades<,>,&,", numéricas (&#NN;) y hexadecimales (&#xHH;) antes de ejecutar la regex de saneamiento. Validado por 11 pruebas entests/cv-xss-bypasses.test.mjs. La defensa real sigue siendo la canalización escape-firstUI.mddel lado cliente; esto endurece el fichero en reposo. -
fix(security): L-2 — bash --noprofile --norc en el ejecutor de lotes(server/lib/routes/batch.mjs:108) —spawn('bash', [PATHS.batchRunner, ...])heredaba el~/.bashrcdel usuario. Un fichero rc hostil podía influir en la ejecución. Ahoraspawn('bash', ['--noprofile', '--norc', PATHS.batchRunner, ...]).
🔒 Resiliencia
-
fix(client): M-6 — *backoff* exponencial en el ping de salud(public/js/api.js:22-48) — el poller del estado desconectado lanzaba 28 800 fetches contra un servidor caído durante la noche. Ahora 3 s → 6 s → 12 s → 24 s → 60 s, con reinicio a 3 s en el primer 2xx de recuperación. La configuración es una cadena desetTimeout(no unsetInterval), de modo que cada paso adopta el nuevo retardo. -
fix(client): M-5 — protección frente a localStorage del modo privado de Safari(public/js/lib/i18n.js:572-583) — Safari en modo privado lanzaSecurityErroren cadalocalStorage.getItem/setItem. La IIFE de carga rompía el módulo i18n completo y dejaba el SPA mostrando claves en bruto. Se envolvieron ambas llamadas en try/catch con respaldo adetect()por el idioma del navegador. -
fix(server): M-2 — tope de tamaño en *fetches* salientes de vista previa (prueba + verificación)— elsafeGetintroducido en v1.21.0 ya transmitía por chunks y cortaba enopts.maxBytes. v1.22 añade una prueba explícita de regresión entests/ssrf-redirect-rebind.test.mjspara fijar el contrato: 100 KB de origen + 4 KB de tope → respuesta ≤ 4 KB. -
fix(client): L-5 — limpiar setTimeout al cambiar de hash en scan.js(public/js/views/scan.js:6-22, :113-120) — el temporizador derefreshResults()a los 300 ms tras done tenía fuga si el usuario salía de#/scanen esa ventana. El handle ahora se captura y se cancela en__cancelActiveScanPoll. -
fix(client): L-4 — combinador de líneasdata:multilínea en SSE(public/js/lib/auto-pipeline.js:158-176) — el parser SSE usabamatch()(una sola línea). Según la especificación, un evento puede arrastrar varias líneasdata:que el consumidor une con\n. El servidor envía hoy JSON en línea única, así que el código antiguo funcionaba — pero era frágil ante cualquier carga útil multilínea futura.
♿ Accesibilidad
-
feat(a11y): M-3 — WCAG 1.4.1 pistas redundantes en píldoras de puntuación y banner de conexión(public/css/app.css:602-625, :812-822) —score-high/score-mid/score-lowtransmitían el estado solo por tono (rojo/ámbar/verde). Quien no percibe el tono se quedaba sin recurso. Cada nivel recibe ahora un glifo redundante vía::before(✓ / ◐ / ○). El banner de conexión incorpora un glifo⚠en el estado offline. Los puntos de renderizado no se tocan — endurecimiento puramente CSS. -
feat(a11y): M-1 — párrafos de pista en línea para cada campo de mode-page(public/js/views/mode-page.js,public/js/lib/i18n.js) — v1.20.0 cableóhtmlFor → iden cada campo de mode-page pero no arrastró el texto de pista en línea; sólo los recorridos del README describían la intención del campo. v1.22.0 incorpora 19 claves i18n de pista × 8 locales = 152 traducciones nuevas y el constructorfield()renderiza ahora un<p id="…-hint">con su cableadoaria-describedbypor campo. Los usuarios de lector de pantalla oyen la pista al enfocar el campo. -
fix(a11y): M-7 — protección contra null en el alias htmlFor de UI.el()(public/js/api.js:194-198) —htmlFor: nullproducía un literalfor="null". Espejo en una línea de la guardav != null && v !== falsede la rama de respaldo.
🧹 Calidad / portabilidad
-
fix(server): L-1 — radix en parseInt en health.mjs + bin/start.sh + bin/setup.sh—parseInt(process.versions.node)sin radix dispara un aviso del linter y es frágil si Node llegara a publicar versiones hexadecimales. Se añadió10en todas partes. -
fix(server): L-3 — comprobación de punto de entrada segura en Windows(server/index.mjs:159-163) —import.meta.url === \file://${process.argv[1]}`maneja mal letras de unidad y barras invertidas en Windows. Sustituido porfileURLToPath(import.meta.url) === path.resolve(process.argv[1])`. -
refactor(client): N-2 — eliminar el parche de Element.prototype.also(public/js/views/cv.js:188-201) — contaminación global del prototipo DOM. Sustituido por una variable local con la raíz del árbol. -
test(canary): M-8 — prueba de regresión 404 para /api/scan-ru/config retirado(tests/scan-consolidated.test.mjs) — v1.20.0 retiró el alias pero no dejó canario. Añadido en tres líneas como espejo de las pruebas de retirada de v1.18.
📚 Documentación + system prompts
-
docs(architecture): refrescar OVERVIEW + DATA-FLOWS para la superficie de v1.21+— añadidossafe-fetch.mjs(GET con DNS fijado),file-lock.mjs(mutex por ruta),rate-limit.mjs(estrangulamiento del LLM) ysanitizePathNamea OVERVIEW.md. DATA-FLOWS.md gana dos secciones nuevas: “Outbound URL fetches (DNS-rebind-safe)” y “LLM endpoint rate-limiting”. -
docs(readme): refresco de la sección sobre el sobre de seguridad— la sección “Security notes” del README.md documenta ahora cada helper del sobre de seguridad de v1.21+ (sanitizePathName, safeGet, withFileLock, llmRateLimit, stripDangerousMarkdown consciente de entidades). -
docs(qa): escenario 31 — alineación con career-ops.org/docs(qa/claude-cowork-browser-test-prompt.md) — seis nuevas subpruebas (31.1–31.6) que verifican que la UI coincide con el comportamiento descrito en las cinco guías canónicas de career-ops.org/docs: umbrales de puntuación, flujo de scan (un botón), flujo de aplicación (lista, no autoenvío), flujo de lotes (editor TSV), arranque de Playwright (fallo elegante) y cobertura del help bundle (5 URLs × 8 locales). -
docs(translate): refresco de calidad del README × 7 *locales* no ingleses— cada README no inglés reescrito a un estilo técnico de calidad editorial en su lengua nativa. Reemplazados los calcos torpes habituales; añadidas las menciones al sobre de seguridad v1.21/v1.22; badges de versión y de pruebas actualizados. -
docs(system): .claude/PROJECT-CONTEXT.md + .github/copilot-instructions.md— orientación en un único fichero para los agentes que se incorporan a una sesión. Resume CLAUDE.md, nombra los helpers de v1.21+ y enumera los tropiezos habituales. -
docs(bin): actualizar comentarios de start.sh / setup.sh / run_all.sh— “dos dependencias” → “tres dependencias” (express + js-yaml + multer); “298 tests” → “474+ tests”; radix deparseIntañadido.
🧪 Tests
- 461 → 474 unit (+13) + 32/32 Playwright sin cambios.
- Ficheros nuevos:
cv-xss-bypasses.test.mjs(M-4, 11 pruebas). - Ampliados:
ssrf-redirect-rebind.test.mjs(+1 para el tope de cuerpo M-2),scan-consolidated.test.mjs(+1 para el canario M-8 del alias). - Cero deltas de comportamiento en suites existentes — cada fix es aditivo o queda cubierto por un canario nuevo.
Verificación
npm test # 474 / 474
npm run test:e2e:browser # 32 / 32
# Saneamiento XSS con entidades:
node -e "import('./server/lib/security.mjs').then(({stripDangerousMarkdown}) => console.log(stripDangerousMarkdown('<script>alert(1)</script>')))"
# → '' (ningún <script> sobrevive)
# Backoff del health-ping (abre devtools, mata el servidor, observa el panel de red):
# 3 s → 6 s → 12 s → 24 s → 60 s, reinicio en el primer ping exitoso
# Glifo de la píldora de puntuación (abre #/reports en tema claro y oscuro):
# .score-high muestra ✓ + puntuación numérica
# .score-mid muestra ◐ + puntuación numérica
# .score-low muestra ○ + puntuación numérica
# Pistas en mode-page (#/contacto, etc):
# <input aria-describedby="mode-contacto-recipient-hint"> ← apunta a <p id="…">
# Alias retirado:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/scan-ru/config
# → 404
Cambios incompatibles
Ninguno. Cada fix es aditivo o preserva los contratos de los endpoints existentes.
Fuera de alcance (v1.23+)
| Ítem | Notas |
|---|---|
| M-9 — traducción de los cuerpos del CHANGELOG por locale | Todas las entradas v1.13+ de CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md eran provisionales con cuerpo inglés. Candidato a traducción masiva cuando se ralentice la cadencia de versiones. |
N-1 — public/js/lib/i18n.js por encima del objetivo de 400 LOC |
Dividir por locale incrementa el coste HTTP sin un empaquetador. Se aplaza hasta que se decida el paso de build. |
| Refresco del contenido del help bundle desde career-ops.org/docs | Las cinco URLs canónicas ya aparecen en el help bundle de cada locale (desde v1.11.x). El escenario 31.6 del prompt QA verifica la cobertura. Refresco de profundidad es candidato a v1.23. |
[1.21.0] — 2026-05-14
Pulido de seguridad + concurrencia + accesibilidad procedente de dos pases independientes de revisión de código. Siete hallazgos del documento docs/specs/V1.20.1-BACKLOG.md se entregan en una sola versión: un bloqueante (TOCTOU de DNS-rebind), seis errores de severidad alta (dispersión del saneamiento de path traversal, hueco de rate-limit en despliegues LAN, condición de carrera de escritura, agujero de cobertura i18n, aria-describedby huérfano, asociaciones de etiqueta ausentes). 34 pruebas nuevas; la línea base sube de 427 → 461 unit + 32/32 Playwright. Cada fix aterriza tras una prueba de regresión con nombre.
🛡️ Seguridad
-
fix(security): B-1 — cerrar el TOCTOU de DNS-rebind con safe-fetch.mjs(server/lib/safe-fetch.mjs) — el patrón previo hacía undnsLookupexplícito para validar y luego dejaba quefetch()resolviera por su cuenta. Un atacante de DNS rebind con TTL=0 podía devolver una IP pública en la búsqueda 1 y127.0.0.1/169.254.169.254/ una dirección LAN en la búsqueda 2, sorteandoisPrivateOrLoopbackHost. El nuevosafeGetresuelve UNA vez, fija la conexión TCP a esa IP exacta vía node:http(s), y configura SNI/Host para que la validación del certificado siga apuntando al hostname original. Lo usan/api/pipeline/previewy/api/auto-pipeline. Fallo CERRADO ante un error de búsqueda (invierte el anteriortry { … } catch { /* fall through */ }). Validado por 8 pruebas nuevas entests/ssrf-redirect-rebind.test.mjs. -
fix(security): H-4 — consolidar sanitizePathName a través de 10 rutas(server/lib/security.mjs) — la regexreplace(/[^\w\-.]/g, '')estaba duplicada a lo largo dejds.mjs,content.mjs,reports.mjs,llm.mjs,runners.mjsy conservaba los caracteres., de modo que..pdf,....mdy los nombres con punto inicial sobrevivían. Sóloreports.mjs::sanitizeSluglo hacía bien. v1.21.0 eleva la versión correcta (sanitizePathName) asecurity.mjs, elimina 10 copias rotas y rechaza con 400 los resultados vacíos. Validado por 12 pruebas entests/path-traversal.test.mjs. -
fix(security): H-5 — limitar la tasa en los *endpoints* LLM con bind público(server/lib/rate-limit.mjs) —/api/evaluate,/api/deep,/api/mode/:slug,/api/auto-pipelineno tenían antes un throttle por IP. Los usuarios loopback no se ven afectados; los despliegues expuestos a LAN (HOST=0.0.0.0) reciben 10 req/min/IP con cabecerasRetry-AfteryX-RateLimit-*al rebosar. Configurable medianteLLM_RATE_LIMIT="N/Ws". Defensa interina barata antes de la puerta de autenticación P-12 de v2.0. Validado por 6 pruebas entests/rate-limit.test.mjs.
🔒 Concurrencia
fix(data): H-6 — mutex por fichero en applications.md / pipeline.md(server/lib/file-lock.mjs) — unPOST /api/trackerconcurrente (o el auto-pipeline compitiendo contra un alta manual) leíanum=42por ambos lados, escribíanum=43por ambos lados y descartaba en silencio la fila más antigua.withFileLock(path, fn)serializa el ciclo leer-modificar-escribir por ruta; las rutas independientes siguen ejecutándose en paralelo. Cableado entracker.mjs,pipeline.mjs(POST + DELETE) y en el paso tracker deauto-pipeline.mjs. Validado por 5 pruebas entests/concurrent-tracker-write.test.mjs, incluida una de integración con 20 POSTs concurrentes que comprueba que las filas 001..020 aterrizan en orden.
♿ Accesibilidad
-
fix(a11y): H-1 — id="batch-tsv-hint" en el párrafo de pista de batch.js(public/js/views/batch.js) — v1.20.0 añadióaria-describedby="batch-tsv-hint"al textarea TSV pero nunca dotó al<p>de pista de unidcoincidente. Los lectores de pantalla no tenían nada que vocalizar. Corregido. -
fix(a11y): H-2 — htmlFor en las etiquetas batch-parallel / batch-min-score(public/js/views/batch.js) — cuatro inputs de v1.20.0 recibieron ids nuevos pero sus etiquetas no estaban asociadas programáticamente. WCAG 3.3.2 queda ahora satisfecho. -
Nuevo canario de análisis estático en
tests/a11y-form-wires.test.mjs— recorre cada fichero de vista y comprueba que cada IDREF dearia-describedby/htmlForapunta a unid:hermano. Detecta regresiones tipo-errata en tiempo de CI.
🌐 i18n
fix(i18n): H-3 — 13 claves de v1.20.0 caían en silencio al inglés en 7 *locales*(public/js/lib/i18n.js) —pipe.filter,pipe.count,pipe.preview*,pipe.openTab,pipe.evaluateAll*,eval.jdHint,batch.parallelAria,batch.minScoreAria, además decommon.delete,config.group{Core,Runtime,Regional},config.profileEmpty,config.viewProfile,scan.atsBadge,scan.regionalBadgese referenciaban cont('key', 'EN fallback')pero nunca se añadieron a DICT. Los usuarios de lector de pantalla en ruso, japonés y chino oíanaria-labelen inglés — anulando directamente la conquista WCAG 3.3.2 que reclamaba v1.20.0. v1.21.0 añade las 19 claves × 8 locales (≈ 150 traducciones nuevas) y extiendetests/i18n-coverage.test.mjscon un pase de análisis estático que escanea cada llamadat('key', …)enpublic/js/**/*.jsy comprueba que cada clave existe en DICT. La deriva futura queda detectada en tiempo de CI.
🧪 Tests
- 427 → 461 unit (+34) + 32/32 Playwright sin cambios.
- Ficheros nuevos:
ssrf-redirect-rebind,path-traversal,concurrent-tracker-write,rate-limit,a11y-form-wires. pipeline-preview.test.mjsrecableado desde el mockglobalThis.fetchal nuevo punto de inyección_setTransportdesafe-fetch.mjs— la ruta SSRF ya no pasa por fetch, así que el mock viejo era sorteado en silencio.
Verificación
npm test # 461 / 461
npm run test:e2e:browser # 32 / 32
node --test tests/ssrf-redirect-rebind.test.mjs tests/path-traversal.test.mjs \
tests/concurrent-tracker-write.test.mjs tests/rate-limit.test.mjs \
tests/a11y-form-wires.test.mjs # 34 pruebas nuevas, todas verdes
# Path-traversal: cada :name al estilo traversal devuelve 400 / 404
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/jds/..pdf
# → 400
# Rate-limit con bind público:
HOST=0.0.0.0 LLM_RATE_LIMIT=3/60s npm start &
for i in 1 2 3 4; do
curl -sS -o /dev/null -w '%{http_code} ' -X POST -H 'Content-Type: application/json' \
-d '{"jd":"…"}' http://0.0.0.0:4317/api/evaluate
done
# → 200 200 200 429
# Escrituras concurrentes al tracker: 20 POSTs paralelos, 20 filas aterrizan:
node tests/concurrent-tracker-write.test.mjs
# 20 filas secuenciales 001..020
# Cordura del cableado aria:
grep -r 'aria-describedby' public/js/views/ | wc -l
# todas las búsquedas `id:` correspondientes resuelven (canario a11y-form-wires.test.mjs)
Fuera de alcance (v1.22+)
| Ítem | Notas |
|---|---|
Tope por streaming del cuerpo en pipeline-preview (M-2) |
await upstream.text() lee el cuerpo entero antes del corte a 8 KB; un stream malicioso de 1 GB podría agotar la memoria. Lectura por stream con contador de bytes y abort. |
WCAG 1.4.1 — estado sólo-color en .connection-banner y píldoras de puntuación (M-3) |
El tono por sí solo señala el estado; añadir prefijo de icono (✓ / ◐ / ○) o sufijo de texto. |
Sorteos de stripDangerousMarkdown vía entidades HTML (M-4) |
<script>, javascript:, <img src="data:image/svg+xml,<svg onload=…>"> sobreviven a la regex. La defensa en profundidad vía UI.md aguanta; documentar y fijar los sorteos en una pasada de pruebas. |
Acceso a localStorage en modo privado de Safari sin try/catch (M-5) |
i18n.js:544/571 lanza → el SPA renderiza claves en bruto. Envolver en try/catch con 'en' por defecto. |
setInterval(checkHealth, 3000) poll sin backoff (M-6) |
Exponencial 3s → 6s → 12s → tope 60s. |
Falta de guarda para null en el alias htmlFor (M-7) |
Defensa de una línea if (v != null && v !== false). |
Canario 404 para /api/scan-ru/config retirado (M-8) |
Prueba de tres líneas siguiendo el precedente de v1.18. |
| Traducciones del cuerpo del CHANGELOG por locale (M-9) | Candidato a traducción masiva cuando se ralentice la cadencia de versiones. |
| Párrafos de pista en línea para cada campo de mode-page (M-1) | ~168 claves i18n × 8 locales; reservado como pulido. |
| Pequeñeces L-1 a L-5 | Radix de parseInt, bash –noprofile, fileURLToPath seguro en Windows, SSE multilínea, limpieza del temporizador de scan.js. |
[1.20.0] — 2026-05-13
Pulido de accesibilidad por componente + paridad de README no inglés + retirada del alias /api/scan-ru/config. Cierra los cuatro ítems que la tabla “Fuera de alcance” de v1.19.0 marcó para v1.20.
♿ WCAG 2.5.5 / 2.5.8 — auditoría de objetivo táctil por componente
a11y(touch-target): chip min-height 28 px + 8 px de gap (excepción 2.5.8 spaced-target)—.chipera 24 × ~50 px (la vertical caía a 24, por debajo del piso 24 px de 2.5.5 para controles agrupados); la excepción spaced-target de 2.5.8 exige o bien ≥ 24 × 24 px, o bien 24 px de separación. Se elevó.chipamin-height: 28px; padding: 6px 12px;y la fila contenedora.chip-rowagap: 8px;para que ambas condiciones se cumplan.a11y(touch-target): sidebar nav-item min-height 44 px—.nav-itemsólo añadía10px 14pxde relleno, con altura calculada ~36 px en la mayoría de viewports. Ahorapadding: 12px 14px; min-height: 44px; box-sizing: border-box;. Coincide con el piso de.btn.a11y(touch-target): tab-btn min-height 44 px— mismo tratamiento para Sortable Headers / pestañas de categoría en Reports, Tracker y resultados de Scan.
♿ WCAG 1.3.1 / 3.3.2 — aria-describedby en pistas en línea
Cada control de formulario del SPA dispone ahora de un id estable, su <label> lo apunta vía htmlFor, y todo párrafo de pista en línea se asocia mediante aria-describedby. Cinco ficheros de vista han sido recableados:
a11y(forms): config.js—idpor clave + asociación de pista (cfg-<key>/cfg-<key>-hint).a11y(forms): evaluate.js— textareaeval-jd+ párrafoeval-jd-hintque documenta el mínimo de 50 caracteres tras el saneamiento.a11y(forms): batch.js—batch-tsv/batch-tsv-hint, másaria-labelenbatch-parallel,batch-min-score,batch-dry-run,batch-retry.a11y(forms): pipeline.js—pipe-filter+pipe-new-url/pipe-new-url-hint.a11y(forms): mode-page.js— cada campo a lo largo de los 7 modes genéricos (project,training,followup,batch-prompt,contacto,interview-prep,patterns) obtiene idsmode-<slug>-<name>y etiquetas conhtmlFor.
UI.el() aprende un alias htmlFor al estilo React para que el código de vista permanezca declarativo — establece el atributo subyacente for (que está reservado en JS como nombre de propiedad).
🌍 Paridad de README no inglés
docs(readme): traducir 7 locales a la paridad de 585 líneas con el maestro EN—README.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.mdocupaban entre 306 y 316 líneas (cubrían titulares pero saltaban los recorridos marketing y la mayor parte de la referencia de API). Los siete reflejan ahora la estructura EN extremo a extremo: About → instalación de un comando → Why? → Quick start (3 pasos numerados) → Requirements → tabla What you get → Scan → Architecture (árbol de directorios completo) → referencia de API (cada tabla de rutas) → Tests → Configuration → Security notes → Limitations → Contributing → recorrido 🌍 Getting Started de 5 pasos → License.
🧹 Alias /api/scan-ru/config retirado
feat!(scan): remove /api/scan-ru/config legacy alias (sunset v1.20)— conservado como alias de una versión en v1.19 por compatibilidad hacia atrás. El canónico/api/scan/regional/configes ahora la única ruta. Eliminados: registro de ruta enserver/lib/routes/scan.mjs, referencias enREADME.md,docs/architecture/{OVERVIEW,SERVER,API}.md. Las pruebas ya cubrían la ruta canónica — sin cambios de prueba.
🧪 Tests
- Misma suite que v1.19. 427 / 427 unit + 20/20 smoke + 23/23 comprehensive + 32/32 Playwright. Todo el cableado de accesibilidad es aditivo (más atributos
id/for/aria-describedby) — sin cambios de comportamiento, sin deltas de prueba.
Verificación
npm test # 427 / 427
npm run test:e2e:browser # 32 / 32
# Objetivos táctiles — cada chip / nav-item / tab-btn ≥ 28 / 44 / 44 px:
# Chrome DevTools → Computed → height/min-height en .chip, .nav-item, .tab-btn
# Etiquetas de formulario — cada input tiene una asociación label[for=…]:
# document.querySelectorAll('input,textarea,select').forEach(el =>
# console.assert(el.labels?.length || el.getAttribute('aria-label'), el))
# Alias desaparecido:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/scan-ru/config
# → 404
# Canónico sigue funcionando:
curl -s http://127.0.0.1:4317/api/scan/regional/config | jq '.'
Cambios incompatibles
DELETE /api/scan-ru/config— eliminado. Usa/api/scan/regional/config. Se anunció el sunset en el CHANGELOG de v1.19.0 y en su script de verificación.
Fuera de alcance (v1.21+)
| Ítem | Notas |
|---|---|
| Párrafos de pista en línea por cada campo de mode-page | Hoy sólo la asociación <label for=…> está en su sitio; el texto de pista visible por campo sigue en inglés en el SPA. Los recorridos del README documentan la intención del campo en cada locale, así que esto es pulido, no bloqueante. |
Estado por sólo-color en .connection-banner y píldoras de puntuación del dashboard (WCAG 1.4.1) |
El banner depende de rojo/ámbar/verde; necesita icono o sufijo de texto para quien no percibe el tono. |
| Traducción del cuerpo del CHANGELOG por locale | Provisionales con cuerpo inglés persisten en CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md. La traducción se hará cuando se ralentice la cadencia v1.x. |
[1.19.0] — 2026-05-13
Contraste WCAG 1.4.3 + unificación del scan (final) + retirada de HH_USER_AGENT de la UI. Cierra la auditoría de contraste fuera de alcance de v1.18, finaliza la eliminación del split EN/RU iniciada en v1.18, y retira el control de configuración HH_USER_AGENT de la UI por indicación del usuario (un valor por defecto razonable ya viene incrustado en el servidor para IPs no rusas, que cubre a la mayoría).
♿ Pase de contraste WCAG 1.4.3
a11y(contrast): introduce AA-passing *-text variants for accent tokens— tema claro:--rausch-text: #b80f42(6,59:1 sobre blanco, antes 3,52:1),--kazan-text: #066507(7,31:1, antes 4,53:1),--darjeeling-text: #7a5800(5,73:1 sobre fondo ámbar, antes 4,24:1),--babu-text: #00665e(6,09:1, antes 2,70:1). Tema oscuro: espejos aclarados (#ff8aa0,#6ee7b7,#fcd34d,#5eead4) alcanzan el mismo piso 4,5:1 sobre papel#161a22.- Las clases de badge (
.badge-ok,.badge-warn,.badge-bad,.badge-info) y las píldoras de puntuación (.score-high,.score-mid,.score-low) se enrutan ahora por las nuevas variantes*-text— cada combinación texto-sobre-fondo-tintado pasa AA. Los tokens de relleno de acento (--rausch,--kazan, etc.) no varían para bordes y contornos (que sólo necesitan 3:1 al ser componentes UI no textuales).
🧹 Unificación del scan (cierra el trabajo de v1.18)
docs(scan): scrub remaining EN/RU split references across READMEs + help + architecture docs— ocho READMEs + ocho help bundles + tres documentos de arquitectura (API.md, SERVER.md, OVERVIEW.md, DATA-FLOWS.md) + un comentario en scan.js describen ahora un único método de scan consolidado. Los alias legacy/api/stream/scan-{en,ru}ya habían desaparecido en v1.18; v1.19 atrapa la documentación y la copia que todavía enmarcaba el scan como un proceso EN+RU en dos pasos.feat(scan): canonical /api/scan/regional/config endpoint—/api/scan-ru/configse conserva como alias delgado durante una versión por compatibilidad. La nueva ruta sigue la convención de nomenclatura por origen (?source=regional).
🛠️ HH_USER_AGENT retirado de la UI
feat!(config): drop HH_USER_AGENT field from /#/config + KNOWN_KEYS— los usuarios avanzados pueden seguir fijandoHH_USER_AGENTdirectamente encareer-ops/.env(el servidor lo lee medianteprocess.env.HH_USER_AGENTenserver/lib/sources/hh.mjscon el UA incrustado como respaldo). La UI deja de exponerlo porque el valor por defecto funciona para la mayoría y ver un campo User-Agent indescifrable en la página de App Settings era una fuente recurrente de confusión.- Las menciones en los README de 8 locales + las menciones en el help bundle de 8 locales se reemplazan por el consejo “ejecuta vía una IP rusa / VPN”. La clave i18n
scan.hhWarningse reformula para descartar el detalle de configuración de la variable de entorno. KEY_GROUPSse colapsa: ya no hay clasificaciónregional(sólo contenía HH_USER_AGENT). Pruebas actualizadas; el camporegionalActivedel payload se conserva por compatibilidad hacia atrás del SPA.
🧪 Tests
tests/env-config.test.mjs— la aserciónKNOWN_KEYSya excluye HH_USER_AGENT; nueva aserción que la clave está deliberadamente ausente.tests/config-endpoint.test.mjs— la prueba multi-clave POST-write usaGEMINI_MODELcomo segunda clave conocida en lugar de HH_USER_AGENT.tests/config-groups.test.mjs—groups.HH_USER_AGENTse espera ahoraundefined.- Total: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright. Mismos conteos que v1.18.0 porque cada prueba ajustada ya estaba contada.
Verificación
npm test # 427 / 427
# Contraste (Chrome DevTools o axe) en claro + oscuro:
# .badge-ok / .badge-warn / .badge-bad / .badge-info → AA pass (4,5:1+)
# .score-high / .score-mid / .score-low → AA pass
# HH_USER_AGENT ya no aparece en /api/config:
curl -s http://127.0.0.1:4317/api/config | jq '.values | keys'
# → ["ANTHROPIC_API_KEY","ANTHROPIC_MODEL","GEMINI_API_KEY","GEMINI_MODEL","HOST","PORT"]
# (sin HH_USER_AGENT)
# Endpoint canónico de configuración regional:
curl -s http://127.0.0.1:4317/api/scan/regional/config | jq '.'
# Alias legacy todavía vivo hasta v1.20:
curl -s http://127.0.0.1:4317/api/scan-ru/config | jq '.'
Fuera de alcance (v1.20+)
| Ítem | Notas |
|---|---|
| Auditoría de objetivo táctil por componente (chips de filtro, cabeceras ordenables, nav lateral) | v1.18 fijó el piso global (.btn 44 px, .btn-sm 32 px); la verificación por componente a lo largo del SPA queda pendiente. |
aria-describedby en pistas de formulario en línea (#/config, #/pipeline, #/evaluate, #/batch) |
v1.17 cubrió aria-label en la búsqueda global y el cierre de modal. La asociación de pista por input es la siguiente capa de pulido. |
| Paridad total de README no inglés (585 líneas como EN) | v1.18 llevó los no ingleses a ~307 (53 % del EN). Los recorridos “Quick start” y “🌍 Getting Started” marketing-heavy siguen sólo en inglés. |
Retirar el alias legacy /api/scan-ru/config |
Sunset planificado para v1.20. El canónico /api/scan/regional/config es el destino de migración. |
[1.18.0] — 2026-05-13
Consolidación del endpoint scan + paso WCAG 2.2 AA + finalización i18n long-tail. Retira los aliases legacy /api/stream/scan-{en,ru} (ventana Sunset 2026-10-01 adelantada a v1.18 por dirección del usuario). Lleva los READMEs no ingleses a ~307 líneas y traduce las entradas CHANGELOG v1.16.0 + v1.17.0 RU-bodied restantes en 6 locales.
🚪 Breaking
feat!(scan): retire legacy /api/stream/scan-{en,ru} aliases— los endpoints SSE split EN/RU deprecados desaparecen. Cada consumidor pasa por el endpoint consolidado/api/stream/scan?source=ats|regional|both(vivo desde v1.12.0). Las rutas legacy llevaban cabeceras Deprecation + Sunset (RFC 8594) desde v1.15.0; la ventana de migración está cerrada. Las integraciones externas en las rutas antiguas reciben un 404 limpio en vez de ser enrutadas silenciosamente al catch-all del SPA.
♿ Accesibilidad (paso WCAG 2.2 AA)
- WCAG 2.4.1 Bypass Blocks — nuevo enlace Skip to main content como primer enfocable en cada página. Visualmente oculto vía
.skip-linkhasta recibir foco, se ancla en la esquina superior izquierda al pulsar Tab desde la carga. - WCAG 2.4.7 Focus Visible — estilo global
*:focus-visible. Anillos de foco off en clics de ratón, on en Tab desde teclado (patrón estándar WAI-ARIA AP). El cierre del modal (×) recibe un anillo de foco de mayor contraste. - WCAG 2.5.5 Target Size — objetivo táctil mínimo 44×44 px en
.skip-link..btn-smmantienemin-height: 32px(combinado con el espaciado de fila cumple la excepción AAA 24×24 + spacing para controles compactos en filas de tabla). - WCAG 3.1.1 Language of Page —
<html lang="en">corregido desdelang="ru". El bootstrap JS i18n ya sobrescribía esto en carga, pero el default SSR coincide ahora con el locale por defecto del SPA. - WCAG 1.3.1 Info & Relationships —
#contentrecibetabindex="-1"para que el destino del skip-link reciba foco limpiamente. (Los roles ARIA + focus-trap ya estaban en v1.17.)
📚 i18n long-tail
docs(i18n): CHANGELOG v1.16.0 + v1.17.0 traducidos en 6 locales— las entradas antes con cuerpo en ruso enCHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.mdestán ahora en su idioma nativo. El conteo de caracteres rusos por locale cayó 79 → 42 → 23.docs(readme): expandir READMEs no ingleses con Why / Requirements / Features / Configuration / Contributing— cada README no inglés creció de 240 → ~307 líneas. Cubre ahora las mismas secciones no marketing que el EN de 585 líneas.
🧪 Tests
- Total: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright (conteo sin cambios; +2 nuevas aserciones correctas de retirada legacy reemplazan las +2 aserciones legacy-still-works).
[1.17.0] — 2026-05-13
Pulido + accesibilidad + fix de CI. Cierra 9 follow-ups del REVIEW de v1.16.0: verificación de humo en navegador, badge truth en READMEs, refresco de cobertura, chip 🔒 lastWorkdayFallback en el SPA, re-baseline E2E completo tras el cambio UX de v1.16, escenarios Playwright para auto-pipeline, pase de accesibilidad ARIA + focus trap, condensación del CHANGELOG histórico en 6 locales, expansión de los READMEs no ingleses con secciones de referencia.
🐛 Fixes
fix(e2e): smoke + comprehensive re-alineados con UX de v1.16— el cambio de v1.16 Cmd+K Enter → modal AutoPipeline hizo quesearch.press('Enter')en los tests E2E abriera un modal que interceptaba los clics siguientes. Los tests usan ahoraShift+Enterpara el camino legacy quick-add. Este era el fallo de CI en el push de v1.16.0 — Playwright E2E expiraba a los 30 s en clics interceptados por el backdrop.fix(mode-page): /#/batch-prompt → modes/batch.md vía serverSlug— v1.15 renombró el slug legacy abatch-prompt, peroPOST /api/mode/:slugen el servidor buscabamodes/batch-prompt.md, que no existe. El nuevo camposerverSlugdesacopla el hash de ruta del nombre de fichero del mode del padre.chore: bump de mensajes de deprecación de v1.16.0 → v1.17.0— la copia de deprecación descan-en/scan-ruy el banner debatch-promptreferenciaban la versión pasada.
✨ Features
feat(scan): chip 🔒 Workday CAPTCHA en card Active Companies— el export server-sidelastWorkdayFallbackde v1.16 PR-7 lo consume ahora el SPA./api/scan-resultsdevuelve la instantánea;#/scanrenderiza una tarjeta tintada de aviso sobre Active Companies cuando un tenant Workday cayó al respaldo (“🔒 Workday tenant blocked — fallback: usa /career-ops scan (Playwright)”). El nuevogetLastWorkdayFallback()evita la ambigüedad de live-binding en ESM. 2 nuevas claves i18n × 8 locales.
♿ Accesibilidad
a11y: pase de roles ARIA + gestión de foco—index.html: atributosroleen<aside>(navigation),<header>(banner),<section id="content">(main),<div id="modal">(dialog conaria-modal/aria-labelledby),<div id="toast">+#conn-banner(status conaria-live),<div class="searchbar">(search).#sidebar-togglerecibearia-controls="sidebar"+aria-expandedsincronizado por JS al abrir/cerrar.#global-searchobtiene un<label>visually-hidden más unaria-labelexplícito que aflora la pista del atajo Cmd+K.- El cierre del modal (×) recibe
aria-label="Close dialog". - Los backdrops decorativos reciben
aria-hidden="true". - Focus trap en modal —
UI.modal()recuerda el propietario del clic, enfoca el primer enfocable no close al abrir, y cicla Tab/Shift+Tab dentro del modal.UI.closeModal()restaura el foco al propietario previo. - Nueva clase de utilidad
.visually-hiddenenpublic/css/app.css(patrón estándar WAI-ARIA AP).
📚 Documentación
docs(readme): badge truth a través de 8 READMEs— badge de pruebas284 / 379 / 360→ 427; badge de versiónv1.9.1 / v1.13.0→ v1.16.0, luego → v1.17.0 vía el bump de v1.17. Destinos de los enlaces de versión actualizados.docs(readme): expandir 7 READMEs no ingleses con secciones de referencia— cada uno creció de 170 a ~240 líneas con nuevas secciones Architecture / API reference / Security notes / Tests / A11y / Limitations / License en el idioma nativo. Todavía no en paridad completa de 585 líneas con EN, pero cubre todas las superficies no marketing clave.docs(changelog): condensar entradas pre-v1.12 en 6 locales— las entradas largas con cuerpo en ruso de v1.11.x + v1.10.x que sangraban en los CHANGELOG no-EN/no-RU se reemplazan ahora por un resumen ejecutivo compacto “Earlier releases” en el idioma nativo de cada locale. La historia detallada queda enCHANGELOG.md(EN).
🛠️ Tooling
coverage: refresh de números— el último publicado fue 95,46 % de líneas / 84,06 % de ramas (REVIEW v1.13.0). Línea base v1.17: 94,14 % líneas / 82,98 % ramas / 93,20 % funciones. Caída ligera por nuevas rutas de error en auto-pipeline + reports-write; muy por encima aún del piso de 80 % en CLAUDE.md.
🧪 Tests
- Total: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32 / 32 Playwright (era 28; +4 escenarios nuevos de auto-pipeline: botón abre modal, Cmd+K paste dispara modal, URL inválida cierra el paso 1, encuadre de eventos SSE
POST /api/auto-pipeline). - Suite E2E re-alineada con UX de v1.16.0 (Shift+Enter quick-add, /#/batch-prompt para el mode legacy).
Fuera de alcance (v1.18+)
| Ítem | Notas |
|---|---|
| Traducir la entrada v1.16.0 en los CHANGELOG no ingleses | Actualmente con cuerpo en ruso (~30 líneas × 6 locales = 180 líneas). Quedó fuera del alcance explícito v1.11.x/v1.10.x del usuario. |
| Paridad completa de README no inglés (585 líneas como EN) | v1.17 llevó los no ingleses a ~240; los recorridos “Why?” / “Quick start” marketing-heavy permanecen sólo en inglés. |
| Auditoría completa WCAG 2.2 AA | v1.17 cubrió ARIA estructural + focus trap; la auditoría por componente de contraste/orden de Tab queda pendiente. |
[1.16.0] — 2026-05-13
Finalización del auto-pipeline + pulido de adaptadores + i18n long-tail. Cierra los 11 follow-ups del REVIEW de v1.15.0: SSE auto-pipeline en el servidor, primitiva POST /api/reports, atajo Cmd+K, paginación de SmartRecruiters, respaldo CAPTCHA de Workday, puerta de CI para drift de capturas, UX del filtro de origen en scan, traducción del CHANGELOG histórico (v1.13.0/v1.12.0 × 6 locales), expansión de los READMEs no ingleses, importador paste-ready de empresas en tendencia.
✨ Features
feat(auto-pipeline): orquestador SSE server-side(#1, #2, #3, #8) — el orquestador client-side chained-fetch de v1.15 se elimina.POST /api/auto-pipelinees ahora un endpoint SSE curl-able que ejecuta validar → traer JD → evaluar → guardar informe → tracker en el servidor con eventos por paso en tiempo real. La llamada lenta a Anthropic (30–90 s) emite ahora eventosrunningen vez de un spinner genérico. Los fallos emitenerrorconstep+message. El orquestador también persiste el markdown del informe enreports/<slug>.mddel padre (se perdía en v1.15).feat(reports): primitiva POST /api/reports— nuevo writer enserver/lib/routes/reports.mjs. Saneamiento de slug con guarda contra path traversal. Tope de 1 MB (413). 409 si el fichero existe sinoverwrite:true. Escritura atómica a través destripDangerousMarkdown. Registro de actividadactivity.reports.save. Pruebas: 9 casos.feat(app): Cmd+K paste URL → auto-pipeline— pegar una URL en la búsqueda global + Enter abre el modal AutoPipeline conautoStart=true. Shift+Enter preserva el camino legacy “add to pipeline only”.feat(portals): paginación SmartRecruiters(#4) —server/lib/sources/smartrecruiters.mjsrecorre páginas mediante?limit=100&offset=Nhasta alcanzartotalFound, O página vacía, O tope de seguridad de 30 páginas / 3000 jobs. Boards grandes (Procter & Gamble) ya no pierden el resto de sus publicaciones. Pruebas: 6 casos.feat(portals): respaldo CAPTCHA de Workday elegante(#7) —server/lib/sources/workday.mjsdeja de lanzar en 4xx / no-JSON / errores de red. Devuelve[]y anota el nuevo exportlastWorkdayFallback. La línea de tiempo del scanner continúa con el siguiente tenant. Opt-in al throw de v1.14 víastrict:true. Pruebas: 7 casos.
🛠️ Tooling + CI
ci(workflows): puerta de drift de dashboard-screenshots(#5) — nuevo.github/workflows/dashboard-screenshots.yml. En PRs que tocanpublic/css/app.css,public/js/views/dashboard.js,public/js/lib/i18n.jsopublic/index.html, el workflow arranca el servidor contra un andamiaje en /tmp, regenera los 8 PNG hero vía Playwright + chromium, y falla la build si el resultado deriva del committed.feat(scripts): import-trending-companies.mjs(#11) — verifica las 13 empresas en tendencia dedocs/portals-examples.mdvía su API de boards real y emite YAML pegable para elportals.yml::tracked_companiesdel padre.enabled: falsese estampa en candidatos cuyo slug devuelve 404. Se ejecuta connpm run import:trending.feat(scripts): npm run capture:dashboards— exponescripts/capture-dashboard-screenshots.mjscomo script de primer nivel.
🎨 UX
fix(scan): dropdown de filtro source consolidado(#6) — el desplegable de origen en#/scanreconstruido a partir del registry de adaptadores de v1.14: 6 ATSes + hh.ru + Habr Career, alfabético, sin prefijos geográficos.runEnScan/runRuScanapuntan ahora al endpoint consolidado/api/stream/scan?source={ats,regional}.
📚 i18n long-tail
docs(i18n): traducir CHANGELOG v1.13.0 + v1.12.0 en 6 locales(#9) — entradas antes con cuerpo en ruso enCHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.mdahora en su locale real. Cada CHANGELOG no-EN/no-RU recibe además una nota i18n explicando que las entradas pre-v1.12 permanecen en ruso por convención del proyecto.docs: expandir READMEs no ingleses con sección de destacados v1.16.0(#10) — 6 READMEs no ingleses (es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW) reciben una nueva sección de ~35 líneas que cubre: flujo de un clic auto-pipeline + ejemplo curl, paginación de SmartRecruiters, respaldo de Workday, UX del filtro de origen en scan, script importador y workflow CI de capturas.
🧪 Tests
- Nuevo
tests/reports-write.test.mjs(9 casos) — camino feliz, saneamiento de slug (incl. guarda contra path traversal), conflicto 409, flag overwrite, strip XSS, 400 con campos faltantes, 413 con >1 MB, round-trip GET/POST. - Nuevo
tests/auto-pipeline.test.mjs(5 casos) — encuadre SSE, puerta de URL inválida, puerta SSRF/loopback, ruta de error sin clave LLM, cabeceraContent-Type: text/event-stream. - Nuevo
tests/smartrecruiters-pagination.test.mjs(6 casos) — single page, 3 páginas, early-stop por página vacía, tope duro respetado, query strip, lanzamiento en 503. - Nuevo
tests/workday-fallback.test.mjs(7 casos) — camino feliz, 403/429 elegante, cuerpo no-JSON, error de red, opt-in estricto para 4xx y errores de red. - Total: 427 / 427 unit (era 400; +27 netos). 0 fallos. 28/28 Playwright + 23/23 comprehensive E2E + 20/20 smoke E2E en verde desde la línea base v1.15.0.
Fuera de alcance (v1.17+)
| Ítem | Notas |
|---|---|
| Traducir entradas pre-v1.12 del CHANGELOG (v1.11.x, v1.10.x) | Convención preservada: con cuerpo en ruso. El backport son ~1800 líneas de traducción; diferido. |
| Paridad completa de README no inglés (585 líneas como EN) | v1.16 añadió ~35 líneas por locale; el espejo completo es un pase de traducción aparte. |
Aflorar lastWorkdayFallback en la tarjeta Active Companies del SPA |
Export en servidor cableado; consumo en UI es v1.17. |
Bulk add per-empresa de tracked_companies para las 9 en tendencia ya verificadas |
El script import:trending lo hace en 1 comando + 1 pegado. Automatizar escrituras al portals.yml del padre violaría la regla dura #1 de CLAUDE.md. |
Verificación
npm test # 427 / 427
node -e "import('./server/lib/portals/registry.mjs').then(m => console.log(m.ALL_ADAPTERS.length))" # → 6
curl -N -X POST http://127.0.0.1:4317/api/auto-pipeline \
-H 'Content-Type: application/json' \
-d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/4567"}'
curl -X POST http://127.0.0.1:4317/api/reports \
-H 'Content-Type: application/json' \
-d '{"slug":"smoke","markdown":"# smoke
"}'
[1.15.0] — 2026-05-13
Release de conformidad documental. Cierra 9 de 10 hallazgos abiertos en la auditoría de conformidad (qa/conformance-vs-docs/00-CONFORMANCE-REPORT.md) más imágenes hero localizadas. Alinea la UI con el flujo canónico de career-ops.org/docs — el mismo pipeline que promete el CLI, ahora extremo a extremo en navegador y en las 8 locales.
✨ Features
feat(auto-pipeline): PR-C — un clic "pegar URL → informe + PDF + fila de tracker"(G-007) — hasta v1.15 el usuario hacía 5 clics manuales por /#/pipeline → /#/evaluate → /#/cv → /#/tracker. Ahora un único botón ✨ en /#/dashboard encadena: validar URL → traer JD (a prueba de SSRF) → evaluar contra el CV → generar PDF → añadir fila al tracker. Línea de tiempo modal paso a paso con [✓]/[…]/[✗]. Extracción heurística de empresa/rol. Fichero nuevo:public/js/lib/auto-pipeline.js. 19 claves i18n nuevas × 8 locales.feat(modes): PR-D — editor modes/_profile.md como pestaña Modes en #/config(G-008) — el fichero canónico “Career framing” del Quick Start §Step-5 es ahora visible en la UI. Nuevos endpointsGET/PUT /api/modes/_profilecon tope 256 KB, pase XSS destripDangerousMarkdown, andamiaje desde_profile.template.md. 9 claves i18n nuevas × 8 locales.feat(profile): PR-E — esquema canónico + location + headline(G-009) —/api/profileacepta TANTO el legacy (candidate:{...}) COMO el canónico (top-levelfull_name,narrative.headline,target_roles.primary,compensation.target_range). El legacy gana en colisión. NuevosummarizeProfile(). /#/profile muestranarrative.headlinecomo nueva tarjeta. 2 claves i18n nuevas × 8 locales.feat(tracker): PR-B — columna Legitimacy en #/tracker(G-006) — restaura paridad con la tabla de salida del pipeline canónico. Entre Status y PDF, con realce badge-ok/warn/bad. Degradación elegante para filas previas a v1.15. 1 clave i18n nueva × 8 locales.fix(routing): PR-H — deduplicar sidebar; #/batch → SPA TSV de v1.13.0(G-011) — antes del fix, /#/batch aparecía DOS veces en el sidebar Y ambas entradas llevaban al constructor de mode-prompt legacy. El SPA TSV de v1.13.0 (8 KB) era inaccesible. Duplicado eliminado; el legacy renombrado abatch-promptcon banner de deprecación.
📚 Documentación
docs(evaluate): PR-A — realineación Block A-F(G-005) — career-ops.org/docs usa A–F (Estrategia/Personalización/historias STAR en C/E/F). Emitíamos A–G. v1.15 actualiza los 8 help bundles §9 con A–F canónico y un callout de compatibilidad hacia atrás. ⚠ Aún se requiere un commit en el padre:santifer/career-ops::modes/oferta.mddebe reescribirse aguas arriba.docs: PR-F — seniority_boost + search_queries en help §5 × 8 locales + scaffold(G-010) — la sección §5 de la ayuda en las 8 locales documenta la tercera clave title-filter + bloque ejemplosearch_queries.bin/setup.shsiembraseniority_boost: ["Senior", "Staff", "Lead"]por defecto.docs: PR-I — imágenes hero localizadas por locale de README— cada uno de los 8 README tiene suimages/dashboard-<locale>.pngespecífico (HiDPI 1440×900) generado víascripts/capture-dashboard-screenshots.mjs. El viejopublic/images/screen_vacancy_found.pngse elimina.
🧹 Limpiezas arrastradas
PR-G — G-001i18nscan.noResults: 8 cadenas con el literal “EN or RU scan” reemplazadas.PR-G — G-002📄 Generate PDF aflora ahora en los paneles de resultado de#/interview-prep.PR-G — G-003README.cn.md→README.zh-CN.md(etiqueta locale canónica).PR-G — G-004/api/stream/scan-en+scan-ruemiten ahora cabeceras RFC 8594 Sunset + Deprecation + Link (sunset 2026-10-01). Retirada en v1.16.0.
🧪 Tests
- Nuevo
tests/profile-canonical-schema.test.mjs(6 casos). - Nuevo
tests/modes-profile-crud.test.mjs(8 casos). - Corregida una regresión de aislamiento en fixtures de prueba: las pruebas usan ahora el patrón
before/after + dynamic-importpara no mutar elconfig/profile.ymldel padre. - Total: 400 / 400 pruebas unit (era 386; +14). 0 fallos.
Fuera de alcance (v1.16+)
| Ítem | Notas |
|---|---|
| Commit en el padre para el prompt canónico A–F | santifer/career-ops::modes/oferta.md debe reescribirse aguas arriba. La regla dura #1 de CLAUDE.md nos prohíbe tocar el padre. |
POST /api/auto-pipeline SSE en el servidor |
El orquestador client-side aporta la victoria UX; el server-side dará retry-from-step-N + ejecutable por curl en CI. |
Primitiva POST /api/reports |
Auto-pipeline muestra el markdown en línea pero no lo persiste en reports/ del padre. |
| Cmd+K pegar-URL → ejecutar auto-pipeline | Diferido a v1.16+. |
[1.14.0] — 2026-05-13
3 nuevos adaptadores ATS sobre el registry de v1.13.0, llevando el total de 3 → 6 ATSes soportados (Greenhouse / Ashby / Lever + Workable / SmartRecruiters / Workday-beta). Documentación de cara al usuario actualizada en los 17 archivos de “3 ATSes” a “6 ATSes” en una sola pasada (42 frases): README × 8 locales, help bundle × 8 locales, PROJECT.md. Añadidos bloques YAML listos para pegar de 13 empresas en tendencia en docs/portals-examples.md para el portals.yml del padre.
✨ Features
feat(portals): 3 nuevos ATS — Workable, SmartRecruiters, Workday-beta— el registry resuelve ahora 6 ATSes (antes 3). Ficheros nuevos:server/lib/portals/adapters/{workable,smartrecruiters,workday}.mjs(envoltorios finos con el contrato uniforme) +server/lib/sources/{workable,smartrecruiters,workday}.mjs(HTTP crudo + normalización al shape canónico).- Workable: detecta
apply.workable.com/<slug>Y legacy<subdomain>.workable.com. Endpoint:https://apply.workable.com/api/v3/accounts/<slug>/jobs?details=true. - SmartRecruiters: detecta
jobs.smartrecruiters.com/<slug>Ycareers.smartrecruiters.com/<slug>. Endpoint:https://api.smartrecruiters.com/v1/companies/<slug>/postings. - Workday (beta): detecta
<tenant>.wd<N>.myworkdayjobs.com/<lang>/<site>. Endpoint: POST a/wday/cxs/<tenant>/<site>/jobs.site=Externalpor defecto si la URL no incluye site. Beta porque algunos tenants cierran el feed CXS con CAPTCHA — el respaldo es el/career-ops scandel padre (Playwright).
- Workable: detecta
📚 Documentación
docs(portals-examples): bloque de boards en tendencia—docs/portals-examples.mdextendido con la sección v1.14.0 que lista 13 empresas en tendencia como YAML listo para pegar entracked_companies: alojadas en Greenhouse (Stripe, GitLab, HashiCorp, Cloudflare, Datadog, Hugging Face) + alojadas en Ashby (Notion, Linear, PostHog, Replicate, Modal Labs, Fly.io, Render). Todas conenabled: false— el usuario verifica el slug antes de activar. Bloques de ejemplo adicionales para Workable / SmartRecruiters / Workday.docs(framing): 42 frases ATS actualizadas en 17 ficheros de cara al usuario— cada aparición de “Greenhouse / Ashby / Lever” en documentación de usuario se lee ahora como “Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday”. Afectados: README × 8 locales, help bundle × 8 locales, PROJECT.md. Las entradas históricas del CHANGELOG y los documentos prescriptivos de bug-fix (qa/fixes/F-014,qa/FIX-PROMPT) se dejan intactos deliberadamente — describen estado pasado o ya correcto.docs(qa): escenario 19 del *browser test*—qa/claude-cowork-browser-test-prompt.mdextendido con el escenario 19: invarianteALL_ADAPTERS.length === 6, barrido de detección de URL víaresolveAdapter()para los 6, comprobación soft de la tarjeta Active Companies en#/scan, comprobación estructural dedocs/portals-examples.md.
🧪 Tests
tests/adapter-registry.test.mjsextendido con 7 casos nuevos para los 3 adaptadores (Workable apply-URL, Workable subdominio legacy, SmartRecruiters jobs.* + careers.*, Workdaytenant.wd5.*con site explícito, Workday respaldo a site por defecto, invarianteALL_ADAPTERS.length === 6, compatibilidad del shape legacydetectApi()).- Total: 386 / 386 pruebas unit (antes 379; +7 netos). 0 fallos.
Fuera de alcance
| Ítem | Notas |
|---|---|
| Entradas por empresa para las 13 en tendencia Greenhouse/Ashby | El bloque v1.14.0 de docs/portals-examples.md las lista como YAML pegable; el bulk-add al portals.yml del padre es fase aparte. |
| Automatización del respaldo CAPTCHA de Workday | El adaptador Workday lanza cuando el feed CXS está bloqueado; el respaldo planificado delega al /career-ops scan del padre (Playwright). El cableado en el UX de scan del SPA es para v1.15+. |
[1.13.0] — 2026-05-13
Gran versión. Cierra los 4 ítems diferidos en un solo commit: PR-4 (pipeline multer completo), Adapter registry (continuación arquitectónica de F-018), página SPA Batch evaluate, y andamiaje de mode-template consciente del locale. Más un fix a mitad de sesión de tablas en tema oscuro.
✨ Features
feat(cv): subida multipart con multer (PR-4 completo)—/api/cv/importacepta ahora TANTO octet-stream (contrato original) COMOmultipart/form-datamediante multer. El rechazo 415 de v1.10.2 era un parche; v1.13.0 es la corrección real. curl-F, default de Postman, cualquier cliente HTTP funcionan sin fricción. Nueva dependencia:multer ^2.1.1.feat(portals): adapter registry— los fetchers de Greenhouse / Ashby / Lever extraídos aserver/lib/portals/adapters/*.mjscon contrato uniforme.server/lib/portals/registry.mjs::resolveAdapter()es el único punto de despacho. Añadir un nuevo ATS = un fichero enadapters/+ una línea enALL_ADAPTERS.feat(batch): página #/batch *evaluate*— nueva vista SPA + 4 endpoints (GET /api/batch,PUT /api/batch,GET /api/stream/batch,POST /api/batch/merge). Editor TSV parabatch/batch-input.tsv, controles parallel/min-score/dry-run/retry, log SSE en vivo debash batch/batch-runner.sh, botónMerge to tracker(ejecutanode merge-tracker.mjs). Enlace en sidebar. 21 claves i18n nuevas × 8 locales.feat(prompts): andamiaje de mode consciente del locale—buildModePrompt+buildEvaluationPromptenvuelven ahora el cuerpo inglés de la plantilla mode del padre con andamiaje localizado (línea de rol, “Read these files first”, “User-supplied context”) en 8 locales.
🎨 Fixes de UX
fix(theme): tablas en modo oscuro + tab-btn—#fafafa/#fff/#f7f7f7hardcoded sustituidos por tokens. El hover en oscuro es ahora legible. Añadido.row-boostedcon franja de acento.
🧪 Tests
- Nuevos
tests/adapter-registry.test.mjs(7),tests/batch-endpoints.test.mjs(5),tests/locale-scaffold.test.mjs(6). tests/cv-upload-multipart-reject.test.mjsreescrito al contrato v1.13.0 (multipart parseado correctamente).- Total: 379 / 379 unit (era 360; +19). 0 fallos. Cobertura 95,46 % líneas / 84,06 % ramas.
- 20/20 smoke E2E · 23/23 comprehensive E2E · 28/28 Playwright.
Fuera de alcance
- 14 adaptadores de portal nuevos — el registry está; añadirlos = un fichero cada uno; queda el research portal por portal.
- Traducir cuerpos de
modes/<slug>.mddel padre — requiere PR aguas arriba asantifer/career-ops(regla dura #1 de CLAUDE.md).
Documentación
docs/reviews/REVIEW-2026-05-13-v1.13.0.md.
[1.12.0] — 2026-05-13
Pase de bug-fix + UX + brand. Cierra 8 ítems del backlog tras v1.11.1 (huecos de prueba #9–12, error de consola #8, deriva portals-dead #4, afloramiento de seniority_boost #6, consolidación de endpoint F-018). Añadido conmutador día/noche de tema, eliminada la mención “Airbnb-styled” de todos los documentos, metadata del paquete y descripción del repo de GitHub.
✨ Features
feat(theme): conmutador día/noche— nuevo botón de tema en la top-bar. Ciclo claro ↔ oscuro, persistente enlocalStorage, restaurado antes del primer pintado víapublic/js/lib/theme-bootstrap.js. Respetaprefers-color-schemeen la primera carga. Paleta oscura completa enpublic/css/app.cssbajo[data-theme="dark"].- **
feat(scan): /api/stream/scan?source=ats|regional|both(F-018 LITE)** — un *endpoint* SSE consolidado. El SPA abre UN único *event-stream* que ejecuta secuencialmente ambas fases (ATS, después regional). Los legacy/api/stream/scan-en+/api/stream/scan-ru` permanecen como alias deprecados. feat(scan): afloramiento de seniority_boost— ambos scanners leenportals.yml::title_filter.seniority_boosty marcan_boosted: trueen jobs coincidentes. El SPA ordena las filas potenciadas arriba y renderiza un badge⬆ boosted.
🐛 Fixes
fix(ui): .message null-safe en 4 sitios (#8)—app.js,views/tracker.js,views/apply.js,views/evaluate.js. Antes, un Promise rejection sin payload Error lanzaba “Cannot read properties of undefined” en el teardown E2E.fix(test): drift portals-dead como aviso, no fallo (#4)— aserción convertida en aviso por stderr. CI sigue en verde ante deriva del padre; las decisiones de versión son manuales.
📝 Brand / docs
docs(brand): eliminadas referencias 'Airbnb' de todos los doc + package + descripción del repo de GitHub— 8 README, CLAUDE.md, FRONTEND.md, package.json y la descripción del repo migrados de “Airbnb-styled” a “Clean, docs-style”.
🧪 Tests
- Nuevo
tests/canonical-docs-coverage.test.mjs(5 casos) cierra los huecos de prueba #9–12. - Nuevo
tests/scan-consolidated.test.mjs(6 casos) cubre F-018 LITE. - Total: 360 / 360 unit (era 349; +11 netos). 0 fallos. Cobertura: 95,62 % líneas / 84,37 % ramas.
- 20/20 smoke E2E · 23/23 comprehensive E2E · 28/28 Playwright.
Documentación
docs/reviews/REVIEW-2026-05-13-v1.12.0.md.
Fuera de alcance (sin cambios desde v1.11.1)
Página SPA Batch evaluate; adapter registry completo (refactor arquitectónico F-018); pipeline multer completo (PR-4); traducción de plantillas mode.
Versiones anteriores (v1.11.x y v1.10.x)
Las entradas detalladas para v1.11.0 / v1.11.1 / v1.10.0–v1.10.3 viven en el 🇬🇧 CHANGELOG EN. Resumen ejecutivo:
- v1.11.1 — 2026-05-13 · Pulido: pista de Playwright en
#/apply, taglines unificadas, tarjeta de umbrales de puntuación en el dashboard. 349/349 pruebas. - v1.11.0 — 2026-05-13 · Integración de career-ops.org/docs en los 8 help bundles y los 8 README. Nuevo
docs/career-ops-canonical.md. Documentados los conceptos Mode/Archetype/Pipeline/Tracker/Report/Scan history. 348/349 pruebas. - v1.10.3 — 2026-05-12 · Tramo de bug-fix: cierra 7 de 11 hallazgos QA del pase de regresión de v1.10.2.
- v1.10.2 — 2026-05-12 · Rechazo 415 en multipart CV (parche temporal hasta el multer de v1.13.0); corrección de generación de PDF.
- v1.10.1 — 2026-05-09 · Parche crítico del pase de regresión QA de v1.10.0.
- v1.10.0 — 2026-05-08 · Editor
#/profile+ UX de subida de CV (pandoc/pdftotext/passthrough), 8 locales × 16 H2 de paridad de ayuda, conmutador de locale.