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.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.