Changelog

GitHub

Todas as mudanças relevantes, versão por versão. O mesmo arquivo é publicado no repositório e traduzido nos 17 idiomas.

Histórico de mudanças

Todas as mudanças relevantes do career-ops-ui estão documentadas aqui. O formato segue Keep a Changelog e o projeto adere ao Versionamento Semântico.

Traduções: 🇬🇧 English · 🇪🇸 Español · 🇰🇷 한국어 · 🇯🇵 日本語 · 🇷🇺 Русский · 🇨🇳 简体中文 · 🇹🇼 繁體中文 · 🇫🇷 Français · 🇵🇱 Polski · 🇺🇦 Українська · 🇩🇰 Dansk · 🇸🇦 العربية · 🇩🇪 Deutsch · 🇮🇹 Italiano · 🇹🇷 Türkçe · 🇮🇳 हिन्दी

Nota — este arquivo está integralmente traduzido para o português brasileiro. Cada entrada de versão foi reescrita em PT-BR técnico de qualidade editorial, preservando blocos de código, mensagens de commit, caminhos de arquivo, URLs, variáveis de ambiente, comandos e rótulos de link em sua forma original.


[1.134.1] — 2026-08-05

Fortalecimento de validação — correções reveladas por uma auditoria completa do projeto.

Corrigido

  • successfactors não descarta mais as vagas coletadas em uma falha no meio da varredura (regressão no port do lançamento de erro para boards mortos da v1.134.0) — seu loop de paginação não tinha try/catch, então uma falha na página 2 ou posterior (depois do sucesso da página 1) lançava erro e descartava tudo o que já havia sido coletado; e se essa falha fosse um 404 (um startrow fora do intervalo), o en-scanner colocaria em quarentena um tenant ativo como morto por dias. Agora espelha phenom/radancy: uma falha na página 0 ainda lança erro (board morto), mas uma falha em página posterior preserva os resultados parciais.
  • Os chips de filtro do #/scan agora são operáveis por teclado (WCAG 2.1.1) — os chips de faceta (e o chip “limpar”) eram spans com um manipulador de clique, mas sem tabindex/role, então usuários de teclado e de leitor de tela não conseguiam alcançá-los ou alterná-los. Agora eles têm role="button", tabindex="0", aria-pressed e ativação por Enter/Espaço.
  • Três strings fixas em inglês agora estão localizadas — o tooltip do selo de confiança do #/scan, o cabeçalho da coluna de realocação do #/scan e o cabeçalho de pontuação do #/dashboard eram literais isoladas que o gate de paridade de i18n não conseguia detectar (nunca foram chaves), então permaneceram em inglês em todos os idiomas não ingleses. Agora com scan.trustTip + scan.col.reloc (2 novas chaves) e reaproveitamento de track.col.score, com uma guarda estática de fonte.

[1.134.0] — 2026-08-05

Paridade com o career-ops pai v1.25.0.

Adicionado

  • Nova fonte de varredura: getManfred (manfred) — um feed de todo o board com vagas de tecnologia da Espanha/UE com salários publicados, a partir de www.getmanfred.com/api/v2/public/offers (zero-auth, host fixado + somente HTTPS, catálogo completo em uma única requisição). Fonte + adaptador + uma suíte isolada para CI (tests/sources-manfred.test.mjs); o registro agora soma 73 fontes = 68 em inglês + 5 russas (ALL_ADAPTERS 68). Aparece no filtro Fonte de #/scan e na landing do cvstart.org.

Corrigido

  • O feed da a16z Speedrun estava truncando silenciosamente em 50 vagas (#2404) — o feed limita uma página a 50, mas a fonte solicitava PER_PAGE = 100, então a paginação parava depois da página 1. Corrigido para 50.
  • Boards mortos agora lançam erro em vez de serem lidos como “ativos, porém vazios” (#2379) — cryptocurrencyjobs, phenom, radancy, successfactors: uma falha de busca em que nenhuma requisição jamais se resolve agora lança um erro (para que a saúde de #/portals e a varredura registrem uma falha real), em vez de a engolir e resultar numa lista vazia; uma falha no meio da varredura, após pelo menos um sucesso, preserva os resultados parciais.
  • workable agora usa a API pública do widget (#5ab8425) — trocado para apply.workable.com/api/v1/widget/accounts/<slug>, que retorna a lista completa de vagas de uma conta grande em uma única requisição, então contas grandes não são mais truncadas.

Notas

  • Não portado (exclusivo de CLI ou não espelhado pelo web-ui): a reescrita de performance com bucketing por título do detect-reposts #2389; as correções de chave de empresa Unicode (a deduplicação do próprio tracker do web-ui já é segura para não-Latin); scan --since; cv-facts; a auditoria do template de CV / PDF; doctor; a diretiva de conteúdo não confiável dos modos.

[1.133.1] — 2026-08-02

Corrigido

  • #/funded (Empresas financiadas) agora renderiza os resultados — dois bugs faziam a tabela sempre mostrar “nenhuma empresa financiada”, mesmo quando o company-funded.mjs do pai retornava uma lista completa. (1) A view lia os resultados em res.candidates, mas o pai os emite em companies (cada uma { company, amount, round, funding: { sources: [{ source, url, observed_date }] } }); o cliente agora lê a chave correta e mapeia o formato real da evidência. (2) A tabela de resultados passava suas células para UI.el('tr', {}, …) como varargs, mas UI.el(tag, attrs, children) espera children como um único nó ou um array, então só a primeira coluna (Empresa) era renderizada — as células agora são passadas como um array. Verificado em um navegador real: 11 empresas nos quatro feeds são renderizadas com as colunas Empresa / Sinal de financiamento / Fonte / Data e links de evidência funcionais, zero erros no console. Uma passagem vazia agora também exibe os diagnósticos por fonte, para que um dia de notícias parado seja distinguível de um feed bloqueado.
  • Guardas de regressão em tests/parity-routes-v1133.test.mjs: o script falso do pai agora emite o formato real de saída companies (a fixture original espelhava o formato errado candidates — motivo exato pelo qual o bug foi lançado em verde), além de canários estáticos de fonte que garantem que funded.jsres.companies (nunca res.candidates) e constrói as linhas da tabela com filhos em array (+1 → 2144).

[1.133.0] — 2026-08-01

Adicionado

  • Descoberta de empresas financiadas (#/funded, paridade com o pai #2117) — uma nova view somente leitura que retransmite o company-funded.mjs do career-ops pai via GET /api/company-funded: uma lista para revisão prévia de empresas recém-financiadas, descobertas a partir de feeds públicos de financiamento com host fixado (TechCrunch, PR Newswire, The Guardian, Hacker News). A retransmissão executa o script com --json --dry-run (JSON para stdout, sem escrita de arquivos), nunca encaminha entrada do usuário para --sources, aplica limite de taxa e é acionada pelo usuário (um botão Descobrir, nunca ao montar a página). Novo módulo de rota server/lib/routes/funded.mjs + public/js/views/funded.js, em Sourcing.
  • Resumo semanal de entrevistas (#/interview-digest, paridade com o pai #2129/#2130) — uma nova view somente leitura que retransmite o weekly-digest.mjs zero-LLM do pai via GET /api/interview/weekly-digest: uma consolidação mecânica das notas de sessão de entrevista — quais empresas e rodadas você teve entrevistas nesta semana, competências recorrentes e lacunas em aberto (best-effort). O intervalo opcional ?from=&to= só é encaminhado quando AMBOS os valores são YYYY-MM-DD válidos; um intervalo vazio é um resumo available:true válido. Adicionado a server/lib/routes/interview.mjs + public/js/views/interview-digest.js, em Analytics.
  • Ambas as retransmissões seguem o contrato consolidado de fail-soft available:false para quando o script do pai estiver ausente (CI, instalações standalone). 26 novas chaves de i18n ×17 locales; suíte isolada para CI tests/parity-routes-v1133.test.mjs (+5 → 2143).

Notas

  • O career-ops pai avançou além da v1.24.0 com a página Follow-up Tracker (#1422) do app web/ em Next.js e a renderização de PDF no backend (#2182) — não portadas: o web-ui já tem sua própria retransmissão de follow-up e seus próprios runners de PDF, e o robustecimento subjacente do followup-cadence.mjs chega de graça via a retransmissão por shell-out. As mudanças em set-status.mjs / tracker-utils.mjs são internas à CLI e não são espelhadas.

[1.132.0] — 2026-07-31

Alterado

  • Subsistema de renderização de resultados do #/scan extraído para public/js/lib/scan-results.js (amortização do contrato de tamanho de arquivo — public/js/views/scan.js havia crescido para ~1254 LOC). O subsistema — renderResults, buildChipRow, os construtores de linha/facetas, os pintores de opções e o espelho do registro FALLBACK_SOURCES — passa para uma fábrica window.ScanResults.create(ctx) que fecha sobre um objeto de contexto fornecido pela view. Nenhuma mudança de comportamento — as funções foram movidas literalmente (verbatim) e suas variáveis de closure foram religadas para ctx.*; scan.js agora tem ~906 LOC (uma segunda passada de extração rumo à meta de 800 LOC está planejada).
  • Novo portão de regressão no navegadortests/playwright-scan-filters.mjs semeia um data/last-scan.json fixo e conduz cada filtro de #/scan, verificando contagens exatas de linhas, de modo que a extração é validada contra o comportamento real do navegador.
  • Banner do README enxugado — a longa narrativa por versão de “Última versão” é aposentada em favor de um resumo de uma linha + um link para o changelog completo (este arquivo).

[1.131.2] — 2026-07-31

Alterado

  • app.css dividido em três folhas de estilo ordenadas (débito do contrato de tamanho de arquivo — o arquivo único havia crescido para ~1990 LOC, bem além da meta rígida de 800 LOC). Agora são app.css (~672 — a11y, tokens de design/tema, sidebar, main, botões, content-shell), components.css (~595 — cards, grids, paginador, badges, tabelas, formulários, log/console, markdown, seletor de idioma, filtro de chips, banner de conexão) e overlays.css (~737 — toast, gaveta de notificações, modal, misc/responsivo, o espelho [dir="rtl"], docs-fab, usage-hud), cada uma dentro do limite rígido.
    • O corte é contíguo e na mesma ordem, então a cascata é byte a byte idêntica ao arquivo pré-split; index.html carrega as três como <link>s ordenados. Nenhuma mudança de comportamento, markup ou i18n.
    • Os testes que verificam CSS agora leem a concatenação via um helper compartilhado tests/helpers/css.mjs::loadAppCss(). O novo tests/css-modularization.test.mjs trava o split (arquivos existem · cada um ≤ 800 LOC · ordem dos links no index.html) → suíte 2138. Verificado no navegador: as três folhas de estilo são interpretadas (parse) e suas regras se aplicam.

[1.131.1] — 2026-07-31

Corrigido

  • Consistência de fixação de host (host-pinning) dos adaptadores nas duas fontes da v1.130.0 (desdobramentos de revisão de código, defesa em profundidade; sem mudança de comportamento para entradas válidas):
    • O adaptador a16z-speedrun-talent agora revalida o override api: / a16z-speedrun-talent: em buildEndpoint (HTTPS + host exato speedrun-talent-network.com) e recai para o feed canônico quando a validação falha — paridade com o adaptador cryptocurrencyjobs, de modo que um valor de host diferente nunca alcança o slot de busca (antes dependia só da guarda assertSpeedrunUrl em tempo de busca). A checagem de host exato agora é uma única SPEEDRUN_TALENT_HOST_RE exportada, compartilhada pela guarda e pelo adaptador.
    • O parser cryptocurrencyjobscleanUrl agora usa a mesma guarda de host por correspondência exata que assertCryptocurrencyJobsUrl e o override do adaptador (antes era endsWith, que aceitava subdomínios). O parser nunca é mais permissivo que a guarda de SSRF: um link de item sub.cryptocurrencyjobs.co é descartado.
    • +2 testes → suíte 2135.

[1.131.0] — 2026-07-31

Adicionado

  • Board de abas de estágio (CRM) do #/tracker (portado da view /pipeline do web app do pai). A barra de chips de funil + dropdown de status do tracker são substituídos por uma faixa de abas de estágio: uma aba All mais uma aba por status canônico — Evaluated · Applied · Responded · Interview · Offer · Rejected · Discarded · SKIP · Hired — cada uma mostrando uma contagem ao vivo do histórico completo, incluindo estágios com contagem zero para que o funil completo esteja sempre visível (o visual CRM). A aba ativa comanda o filtro; clicar nela de novo limpa de volta para All. As linhas mantêm o tom de score, legitimidade, PDF e as ações de relatório, e a célula da empresa agora mostra a marca da empresa (logo) quando os logos estão habilitados (desabilitado por padrão → zero requisições extras).
    • Nova rota somente leitura GET /api/tracker/stages retorna o funil canônico (rótulos em ordem) + um mapa de dobra de aliases, extraído de server/lib/states.mjs (templates/states.yml, com o fallback embutido) — assim o cliente nunca fixa a whitelist de status. A resposta legada de GET /api/tracker sem parâmetros permanece inalterada (só { rows }).
    • Nova lib de cliente pura e testada por unidade public/js/lib/tracker-stages.js agrupa as linhas nos estágios do servidor, tolerando negrito markdown perdido e aliases localizados (ex.: aplicadoApplied). As abas são acessíveis (role tablist/tab, aria-selected, área de toque ≥44 px, contagens no nome acessível de cada aba). Sem novas chaves i18n. Suíte 2133.

[1.130.0] — 2026-07-31

Adicionado

  • Duas novas fontes de varredura portadas do career-ops pai v1.24.0 (em processo, sem novas dependências; ambas aparecem no filtro Fonte de #/scan e na landing do cvstart.org):
    • a16z Speedrun (a16z-speedrun-talent, #2231) — o feed JSON de todo o board da rede de talentos do a16z Speedrun. Fixado no host speedrun-talent-network.com, somente HTTPS, paginação indexada a partir de 0 com limite de páginas, q/config encadeados por empresa, fail-soft.
    • Cryptocurrency Jobs (cryptocurrencyjobs) — o board de vagas Web3 cryptocurrencyjobs.co, ingerido via seu feed RSS 2.0 público (zero-auth). Decodificação de entidades XML em duas passagens, vagas somente remotas, empregador extraído da cauda do título "… at <Company>".
    • O registro agora soma 72 fontes = 67 em inglês + 5 russas (ALL_ADAPTERS = 67 adaptadores de portais em inglês).

Corrigido

  • echojobs — vagas híbridas continuam distinguíveis do remoto (espelha o #2258 do pai). Um marcador hybrid sem distinção de maiúsculas/minúsculas agora produz "<City> · Hybrid" (ou um Hybrid isolado quando não há cidade) e workplaceType: 'Hybrid', em vez de ser colapsado em Remote.
  • radancy — markup legado do TalentBrew + transporte via fragmento JSON de resultados (espelha o commit a3e6df9 do pai), condicionado a um opts.fetchJson injetável.

Notas

  • Não portado — recursos do pai exclusivos de CLI. A ampla superfície de CLI/modos da career-ops v1.24.0 permanece fora do web-ui, que é um visualizador + escrita fina, não um host de modos: as tabelas de compliance/jurisdição, a agenda de contatos + vCard, o transcript-debrief de entrevista / detecção de plataforma de chamada, o set-status do ledger, o registro de outcome, a triagem em duas passagens, o jd-similarity, o schema versionado de artefato de CV de candidatura, a detecção do Playwright-MCP no doctor, e portals/fix-slugs.mjs. Mudanças de orquestração de varredura que vivem no scan.mjs do pai — o scanner Playwright do Interamt.de, a varredura reversa completa do iCIMS, o filtro de elegibilidade remota por país, o espaçamento de lookups de DNS, a deduplicação rltr do StepStone e a coluna de empresa normalizada no histórico de varredura — não se aplicam: o web-ui roda os scanners EN/RU em processo e não invoca scan.mjs.
  • Já coberto. A correção de dobra de acentos do role-matcher (#2209) foi portada na v1.127.0, então é um no-op aqui.

[1.129.1] — 2026-07-29

Corrigido

  • Desdobramentos da revisão de IA sobre os ports web de v1.128/v1.129 (todos consultivos, corrigidos na origem): precedência de nível em job-facets.js (um modificador explícito agora vence uma palavra de gestão — Senior Engineering Managersenior, antes lead); o fallback de states.mjs não é mais fixado (uma leitura bem-sucedida é memoizada, o fallback volta sem cache, então um pai momentaneamente indisponível no boot é relido depois) e avisa (console.warn) num arquivo presente mas malformado; score-tone.js — uma linha sem pontuação é neutra (muted), não vermelha; domainFromName() ignora slugs não-ASCII antes do /api/logo; +uma verificação de isolamento em tests/states.test.mjs. +4 testes → 2073.

[1.129.0] — 2026-07-29

Adicionado

  • Faceta de nível + coluna de idade em #/scan — a lib job-facets.js da v1.128.0 agora está integrada na UI de varredura (antes só lógica). Um novo dropdown Nível classifica o título de cada vaga em lead/staff/sênior/pleno/júnior/estagiário (JobFacets.seniorityFromTitle) e se autopreenche com o que há nos resultados —como a faceta de País—; títulos sem palavra de nível sempre passam. Persiste em buscas salvas, Redefinir e Aplicar. A tabela ganha uma coluna Nível (badge) e uma coluna Idade sem tokens (hoje / Nd, de JobFacets.daysSince). 12 chaves i18n ×17, +3 testes → 2069.

[1.128.0] — 2026-07-29

Adicionado

  • Quatro soluções portadas do próprio web app do pai (../web/, Next.js), reimplementadas em JS vanilla/ESM, sem novas dependências: (1) server/lib/states.mjstemplates/states.yml ao vivo como vocabulário canônico de status do tracker (com fallback para CI) — elimina o re-sync manual da whitelist a cada release; POST dobra aliases (espanhol/legado) ao rótulo canônico, o funnel do GET agrupa por status canônico; (2) logos de empresa em linhas com host ATS via domainFromName() (~90 marcas→domínio) antes do avatar de letra; (3) score-tone.js — tom de pontuação de 4 níveis (≥4.2/3.8/3.0 + fallback de letra); (4) job-facets.js — facetas seniority/source/days. +21 testes.

Notas

  • Não portado (só conceito): a camada de ações agênticas do pai (actions/registry.ts + api/assistant/route.ts) — blueprint para quando docs-fab virar copiloto. Sem novas fontes (registro 70), sem mudanças i18n/help.

[1.127.0] — 2026-07-29

Adicionado

  • Três novas fontes de varredura (paridade com career-ops v1.23.0) — o registro agora traz 70 adaptadores (65 EN + 5 RU): Flowxtra (agregador global sem auth), VDAB (API por palavra-chave do serviço público de emprego flamengo) e iCIMS (portais careers-<tenant>.icims.com, distinto de jibeapply). Além disso Cursor volta ao roster de CLIs (parent #2115): cli-detect agora detecta cursor (10 ferramentas) e o roster é restaurado em help/README/config ×17.

Corrigido

  • agenticjobs migrou de scraping HTML para a API REST (#2167); Greenhouse recupera a cidade do endpoint /offices quando location.name é só um modelo de trabalho (#2104); paridade do role-matcher (#1933/#2164/#2009: prefixo MTS, base product, dobra de acentos, desacordo sub-baseline).

Notas

  • Não portado. A maior parte da v1.23.0 é superfície CLI/dashboard sem uso no web-ui (batch-tailor, discover-ats, modos NL/PT, temas de PDF, dashboard Go, updater/doctor); os scripts relayados não exigem mudança. VERSION do pai → 1.23.0.

[1.126.1] — 2026-07-25

Corrigido

  • Dois pontos de desvio do roster de CLI que o resync da v1.126.0 perdeu — (1) o intro da aba API keys de #/config (config.providerModelNote, i18n ×17) listava só 7 CLIs — agora Antigravity e Grok Build são inseridos após OpenCode; (2) uma segunda linha de tabela comparativa no guia de ajuda (×17) e o help do site (construído no CI) ainda diziam Inside Claude Code / Codex / Cursor / Gemini CLI — o conjunto obsoleto com Cursor — agora o roster completo. Ambos usavam separadores barra/ponto-médio que os padrões da varredura v1.126.0 não cobriam. Snapshot i18n regenerado; suíte permanece em 1969.

[1.126.0] — 2026-07-25

Adicionado

  • A aba de ferramentas AI CLI agora detecta as 8 CLIs de primeira classe do career-ops — o roster de #/config foi sincronizado com o docs/SUPPORTED_CLIS.md do pai: server/lib/routes/cli-detect.mjs ganha Grok Build CLI (grok) e Kimi CLI (kimi), e a Antigravity agora testa primeiro seu binário canônico agy. A varredura de PATH somente-leitura agora reporta 9 ferramentas; continua sem executar nenhum binário encontrado.

Alterado

  • Ressincronização da documentação com career-ops.org/docs — cada superfície de docs foi reconciliada com as páginas vivas do pai (as 31 lidas). O roster canônico de assistentes IA (help ×17 + README ×17) lista agora as 8 CLIs de primeira classe — Claude Code, Codex, OpenCode, Antigravity CLI, Grok Build CLI, Qwen Code, Kimi, GitHub Copilot CLI — mais Gemini CLI (wrapper legado). Os pacotes de ajuda mantêm sua estrutura de 29 H2 / 105 H3.

[1.125.4] — 2026-07-23

Alterado

  • dependências do site (dependabot #151–#153) — sharp 0.34.5→0.35.3, svgo 4.0.1→4.0.2, fast-uri (dev) 3.1.3→3.1.4 em site/; build Astro verde, sem impacto em SPA/servidor.

Notas

  • Varredura de paridade com o pai (career-ops 37d17ec..254764a, pós-v1.22.0) — nada a portar: o guard de linha errada do set-status (#2108) é só CLI (no web-ui as linhas do tracker são selecionadas explicitamente e nenhuma rota chama set-status.mjs), o Risk Summary dos modos localizados (#2109) toca arquivos modes/<lang>/ que o web-ui nunca lê (apenas modes/*.md de nível superior), a verificação de manifesto do update-system (#2111) é só do atualizador, e o resto são docs do pai (README turco, SIGNATURES ×4, SCRIPTS.md, acentos es). O VERSION do pai continua 1.22.0parentVersion inalterado.

[1.125.3] — 2026-07-23

Corrigido

  • Prompts LLM em dinamarquês e hindi respondiam em inglês (reportado por usuário) — LOCALE_NAMES e os cinco blocos de SCAFFOLD_STRINGS em server/lib/prompts.mjs nunca foram estendidos para da nem hi, então resolveLocale() caía para en e todo prompt de IA — deep research (ao vivo e manual), modos, avaliação, entrevista, networking, CV Studio — perdia sua diretiva # Output language nesses dois idiomas. Ambos agora são de primeira classe: diretiva de idioma + scaffolding localizado. O gate de regressão em tests/locale-scaffold.test.mjs agora varre a lista canônica de 17 locales em vez de 12 fixos, e um novo gate estrutural reprova qualquer chave de scaffold que caia para o inglês — um futuro locale que esqueça prompts.mjs não pode mais ser lançado (+12 testes, suíte agora 1969).

[1.125.2] — 2026-07-22

Corrigido

  • Deep research com Gemini: HTTP 502 (MALFORMED_FUNCTION_CALL) (#145, contribuição de @Alien10140) — o prompt ao vivo de /api/deep mandava o modelo «Use WebFetch / WebSearch» e salvar o relatório em arquivo, mas provedores de API headless não têm canal de ferramentas; o Gemini respondia com uma chamada de função em vez de texto, aparecendo como um 502 vazio. buildDeepPrompt e bundleProjectContext agora aceitam a flag headless: execuções ao vivo (Anthropic/Gemini/cascata de fallback) recebem um prompt sem ferramentas que escreve o relatório a partir do contexto embutido, enquanto o prompt de copiar-e-colar para o Claude Code mantém as instruções de ferramentas. +1 teste em tests/critical-fixes.test.mjs.

Alterado

  • Padrões do Gemini atualizados além do descontinuado gemini-2.0-flash (#144, contribuição de @Alien10140) — o dropdown de Configuração, o fallback do servidor em gemini.mjs (que divergia em silêncio da dica), a cadeia de fallback do OpenRouter, config.geminiModelHint ×17 e o guia de ajuda ×17 agora nomeiam gemini-3.6-flash. O novo gate anti-deriva tests/gemini-default-model.test.mjs (+5 testes) fixa todas as superfícies no mesmo literal — a suíte chega a 1957 testes.

[1.125.1] — 2026-07-21

Corrigido

  • SuccessFactors: tenants RMK multimarca mantêm seu caminho de marca (pai #2099, pós-v1.22.0) — holdings que operam várias marcas adquiridas a partir de uma única instância RMK compartilhada as diferenciam por um segmento de caminho (careers.nemetschek.com/Bluebeam/ vs …/Vectorworks/); o adaptador costumava colapsar a URL configurada para sua origem, escaneando silenciosamente as vagas da marca pai. O endpoint agora preserva o prefixo da marca, removendo apenas um segmento final /search/ ou /tile-search-results/ para que nada nunca se duplique sobre si mesmo; tenants de domínio único permanecem byte a byte inalterados. Novo helper exportado resolveTenantBase + 1 bloco de teste portado em tests/sources-successfactors.test.mjs.

[1.125.0] — 2026-07-21

Adicionado

  • cvstart.org: seção “Fontes de vagas” na landing — uma nova seção entre as capturas de tela e o comparativo lista todas as 67 fontes de scan como chips clicáveis (62 boards/ATS em inglês + os 5 boards russos sob seu próprio subtítulo), cada um linkando para o site público da fonte. A lista é sincronizada com o registry de adaptadores ao vivo no momento do build (sync-assets.mjsfacts.sources), de forma que nunca pode ficar dessincronizada do app; um mapa de links curado em Sources.astro é protegido pela nova tests/site-sources.test.mjs. A navegação do cabeçalho ganhou uma âncora Fontes; 4 novas chaves i18n do site ×17. Também foi corrigida a lista inLanguage do JSON-LD da landing, à qual ainda faltava hi.

[1.124.0] — 2026-07-21

Adicionado

  • Cinco fontes de scan (paridade com o pai v1.22.0, #1808/#1572/#2024/#2055) — Welcome to the Jungle (API JSON de todo o board), Agentic Engineering Jobs (board de engenharia agentic/IA), Jobvite (ATS por tenant sem autenticação), Gem (ATS por tenant) e Alibaba Group (API JSON de carreiras, padrão Meituan/Tencent). Cada uma é um par source + adaptador fixado por host e isolado para CI; o registry agora traz 67 adaptadores (62 EN + 5 RU); o fallback do dropdown Source de #/scan e seu gate de deriva foram atualizados; cinco novas suítes tests/sources-{wttj,agenticjobs,jobvite,gem,alibaba}.test.mjs.

Corrigido

  • Arbeitsagentur: remoto em todo o território somente quando homeofficetyp é VOLLSTAENDIG (pai #1981) — a consulta homeoffice=nv_true também retorna vagas híbridas, então a passagem de verificação de remoto agora confirma cada resultado no endpoint de detalhes da vaga em pequenos lotes e falha de forma segura (um erro de consulta mantém a cidade real da vaga, para que os filtros de localização continuem valendo).
  • SmartRecruiters: URLs públicas de vagas construídas sem /postings/ (pai #2047) — os links agora levam à página pública da vaga em vez de um 404, para tenants cujo site público omite esse segmento.

Notas

  • O pai v1.22.0 também trouxe mudanças do lado da CLI que a web UI não invoca via shell ou já cobre de outra forma: o template de CV zh-CN + tipografia do PDF, o modo /expand, ajustes de cache de prompt do provedor (Gemini/OpenAI/Ollama), o detalhamento de tokens por etapa (a web UI tem seu próprio medidor de uso), a serialização por trava de escrita do tracker (a web UI roteia gravações por withFileLock desde a v1.21), as flags de CLI visa_filter e data-de-publicação absoluta do scan (a web UI tem seu próprio filtro de idade “Publicada em até”) e a semeadura de deduplicação de fontes já vistas (o scanner da web UI mantém sua própria dedup de histórico de scan).

[1.123.0] — 2026-07-17

Adicionado

  • Source de scan Oracle Recruiting Cloud (paridade com o pai v1.21.0, #1929) — a API REST recruitingCEJobRequisitions sem autenticação dos sites de carreira Oracle Fusion/ORC (JPMorgan Chase, Oracle, BNY Mellon, American Express, Honeywell, …): host fixado em *.fa[.<região>][.ocs].oraclecloud.com, o número do site resolvido a partir do careers_url de cada empresa rastreada, paginação por offset com um teto rígido de páginas, e cabeçalhos com aparência de navegador cientes de WAF. O registry agora traz 62 adaptadores (57 EN + 5 RU); o fallback do dropdown Source de #/scan e seu gate de deriva foram atualizados; nova suíte CI-isolada tests/sources-oraclecloud.test.mjs.

Corrigido

  • Detector de repostagens: títulos base permanecem distintos de vagas irmãs com sufixo de especialização (pai #1922) — “Senior Analytics Engineer” não agrupa mais com “Senior Analytics Engineer, People Analytics”: quando os tokens de um título são um subconjunto estrito dos tokens do outro e o token extra é uma especialização real (não uma palavra de base), os dois passam a ser tratados como vagas postadas separadamente. Anotações de repostagem (“(Repost)”, “relisted”) agora são tratadas como ruído semântico via stopwords. +2 asserções em tests/detect-reposts.test.mjs.

Notas

  • O pai v1.21.0 também trouxe mudanças do lado da CLI que a web UI não invoca via shell ou já cobre de outra forma: o aviso de reaplicação para empresas repetidas (a web UI já tem o cooldown de reaplicação desde a v1.84.0), as flags --format/--report da carta de apresentação, os modos de prompt de e-mail de red-flag / panel-intel / no-show da entrevista, os sinais de confiança do scan e a persistência de saúde dos portais (a web UI roda seu próprio scanner em processo com trust-validator e a página de saúde dos Portais), e as extensões de estatísticas/salary-gap (relayadas somente leitura e com fail-soft).

[1.122.0] — 2026-07-16

Adicionado

  • Hindi (हिन्दी) — o 17º idioma — dicionário completo da interface (~1.110 chaves), o guia de ajuda embutido no app na íntegra (paridade de 29 H2 / 105 H3), README.hi.md, um novo CHANGELOG.hi.md (começando na v1.122.0, seguindo o precedente de de/it/tr), a landing do cvstart.org + as páginas de Metodologia/Licença/Changelog/Ajuda, o seletor de idioma (🇮🇳), a detecção automática do idioma do navegador e uma captura de tela do dashboard localizada. Todos os gates de paridade ×16 agora rodam ×17: paridade do dicionário i18n + snapshot, gates de H2/H3 da ajuda, paridade do CHANGELOG, check-i18n do site e a varredura de locales do Playwright.

[1.121.0] — 2026-07-16

Adicionado

  • cvstart.org: páginas de Metodologia, Licença e Changelog — a landing ganhou três novas seções em todos os 16 idiomas, ao lado do bloco de Comparativo já existente: /methodology/ (a rubrica de pontuação em seis dimensões de 0,0 a 5,0, o limiar de 4,0 para candidatura e as regras que o sistema nunca segue — um resumo localizado de career-ops.org/methodology), /license/ (o texto canônico da MIT com o ponteiro para o NOTICE.md) e /changelog/ (este mesmo arquivo, renderizado por locale a partir dos 16 CHANGELOGs traduzidos do repositório). Nova entrada Metodologia no cabeçalho e links de Recursos no rodapé; o sync-assets.mjs agora sincroniza o CHANGELOG ×16 e a LICENSE para o site no momento do build, para que as páginas nunca fiquem dessincronizadas do repositório.
  • Links para a metodologia em toda a documentação — o README (nos 16 idiomas), a lista canônica do §1 do guia de ajuda embutido no app (nos 16 idiomas) e a wiki agora linkam career-ops.org/methodology (além do FAQ e do glossário), ao lado dos guias já existentes de career-ops.org/docs.

Alterado

  • Banner de release e badges do README atualizados (testes 1850, release v1.121.0) — o banner ainda anunciava a v1.119.5.

[1.120.0] — 2026-07-16

Adicionado

  • O Manifesto CareerOps (paridade com o pai v1.20.0) — o projeto pai lançou o Manifesto CareerOps (MANIFESTO.md · career-ops.org/manifesto) e agora o expõe no seu README, no updater e no dashboard em Go. A web-ui segue o mesmo caminho: um novo link no rodapé da barra lateral abre a página do manifesto (nova chave i18n footer.manifesto nos 16 locales), o guia de ajuda embutido no app ganhou o §29 “O Manifesto CareerOps” nos 16 idiomas, o README explica o que é o manifesto e como assiná-lo, e o rodapé da landing cvstart.org também tem um link para ele.

Notas

  • O pai v1.20.0 também corrigiu a supressão de habilidades conhecidas no modo direcionado do upskill, silenciou o dotenv para que o stdout de scan --json continue analisável, e corrigiu o template HTML de CV para que o cabeçalho de um cargo permaneça junto com seus marcadores — superfícies do lado da CLI que a web-ui não invoca via shell; nenhuma mudança de código foi necessária na web-ui.

[1.119.5] — 2026-07-13

Corrigido

  • O botão de idioma da landing não quebra mais — com as bandeiras da v1.119.2 o rótulo do seletor no cabeçalho (ex.: «🇷🇺 Русский») podia quebrar em até três linhas em larguras estreitas de desktop; o rótulo do seletor e todas as opções do dropdown agora usam whitespace-nowrap — bandeira + endônimo sempre em uma linha. A lista de idiomas do rodapé passou de uma grade rígida de duas colunas para uma linha com quebra de itens de linha única — «🇧🇷 Português (Brasil)» também não quebra mais no meio do nome.

[1.119.4] — 2026-07-13

Alterado

  • LICENSE nomeia o autor — a linha de copyright agora diz: Sergei Emelianov (Fighter90) https://sergey-cv.com and career-ops-ui contributors (texto MIT canônico intacto). Um novo NOTICE.md detalha o licenciamento: quem detém o copyright, o que exatamente a concessão MIT cobre (código, docs, traduções, a landing, a wiki), o que NÃO cobre (seus dados em runtime, o projeto pai, o conteúdo dos job boards, marcas registradas), a tabela de componentes de terceiros (express/js-yaml — MIT; Astro/Tailwind — MIT; as fontes Figtree e JetBrains Mono — SIL OFL 1.1; sharp — Apache-2.0) e uma linha de atribuição opcional.

[1.119.3] — 2026-07-13

Adicionado

  • SECURITY.md — a política de segurança para a qual o CONTRIBUTING apontava agora existe: versões suportadas, fluxo de reporte privado (o repositório tem o private vulnerability reporting do GitHub habilitado — aba Security → «Report a vulnerability»), o modelo de ameaças para um app de usuário único em localhost (no escopo: XSS via vagas hostis / SSRF / path traversal / vazamento de segredos / enfraquecimento de CSP; fora do escopo: DoS no próprio localhost e problemas do projeto pai) e a linha de base de hardening para revisores.

[1.119.2] — 2026-07-13

Adicionado

  • CONTRIBUTING.md — o guia do contribuidor para o qual a landing e o README sempre apontaram agora existe: instalação, mapa do projeto, as regras rígidas de segurança/no-build, níveis de teste, o passo a passo dos «dois registries» para adicionar uma fonte de escaneamento, o contrato i18n ×16, convenções de commits/PR e o processo de release.
  • Bandeiras de idiomas na landing — o seletor de idiomas do cvstart.org, a grade de idiomas do rodapé e o banner «leia no seu idioma» agora mostram a bandeira de cada locale ao lado do seu endônimo (o mesmo conjunto de indicadores regionais do <select> de idioma do app; degrada para letras de região onde faltam glifos de bandeira).
  • Correções do rodapé da landing — o link morto de Discussions (recurso não habilitado no repositório) agora aponta para a wiki do projeto, e o rodapé credita o autor: Sergei Emelianov (sergey-cv.com · LinkedIn).

[1.119.1] — 2026-07-13

Corrigido

  • O filtro de fontes do #/scan alcançou o registry — a lista estática FALLBACK_SOURCES por trás do dropdown Source (usada apenas quando GET /api/scan/sources está inacessível) ficou silenciosamente defasada desde a v1.87.0: faltavam 20 provedores no 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 e os novos Meituan / Tencent). Sincronizada com os 61 e agora protegida por um teste de deriva que quebra a CI quando a lista do cliente diverge do registry do servidor (valores E rótulos). +1 teste (1845).

[1.119.0] — 2026-07-13

Paridade com o career-ops pai v1.19.0 + renovação da landing cvstart.org.

Adicionado

  • 2 novos provedores de escaneamento — Meituan (zhaopin.meituan.com) e Tencent (careers.tencent.com): as APIs JSON públicas sem autenticação dos boards tech chineses, detectadas pelo host ou selecionadas por um provider: explícito, com busca server-side por palavra-chave, paginação e deduplicação por URL — agora 61 adapters (56 EN + 5 RU). +20 testes (1844).
  • Bloco de contribuidores na landing — o cvstart.org mostra os avatares de todos que contribuíram com código (API /contributors do GitHub em tempo de build, bots filtrados), localizado nos 16 idiomas, com link para o grafo completo de contribuidores.
  • Contador de estrelas do GitHub ao vivo na landing — o badge do cabeçalho agora se atualiza no cliente a partir da API do GitHub a cada visita (o snapshot de build fica como fallback), e uma reconstrução semanal agendada do Pages mantém o snapshot e a lista de contribuidores frescos; as chamadas de API no CI são autenticadas por token.

Corrigido

  • As requisições Workday CXS levam cabeçalhos de navegador (pai #1813) — tenants atrás do Cloudflare (visto ao vivo: geico) respondem 500 a requisições sem UA/accept-language/origin/referer comuns; o fetcher agora deriva o origin e o slug do site da própria URL CXS. As requisições do Glints ganharam o mesmo UA de navegador + origin/referer, ambos da constante compartilhada BROWSER_LIKE_USER_AGENT em http-json.mjs.

[1.118.4] — 2026-07-10

Corrigido

  • As varreduras do hh.ru retornavam 0 resultados a partir de um IP russo (links de subdomínio regional) — a partir de um IP residencial russo, o hh.ru redireciona a busca (302) para um subdomínio regional (sochi.hh.ru, spb.hh.ru, …) e devolve os links de vagas nesse subdomínio. O parser procurava o link do título pelo host fixo https://hh.ru/vacancy/ e não casava com nenhum dos regionais, então uma varredura totalmente funcional registrava 0 em silêncio. Agora aceita qualquer host *.hh.ru (anúncios em adsrv.hh.ru/click?… continuam excluídos — não têm caminho /vacancy/<id>) e canonicaliza cada URL de resultado para https://hh.ru/vacancy/<id>. Verificado ao vivo: 17 vagas reais são parseadas de uma página sochi.hh.ru que antes dava 0. +1 teste (1824).

[1.118.3] — 2026-07-10

Corrigido

  • hh.ru retornava 0 resultados em silêncio (interstitial de verificação de VPN) — o hh.ru agora redireciona com 302 as redes que marca como VPN/proxy (IPs de datacenter) para um interstitial /vpncheeck (“VPN мешает работе сайта”) que responde HTTP 200 sem nenhum cartão de vaga, então o scan reportava 0 sem erro algum. O scanner agora detecta o redirecionamento pela URL final da resposta, desativa o hh.ru pelo resto da execução e imprime uma dica honesta: o tráfego precisa realmente sair por um IP residencial — um VPN/proxy em nível de sistema pode continuar ativo mesmo com o botão do navegador desligado. +1 teste (1823).

[1.118.2] — 2026-07-10

Manutenção

  • Follow-up do landing (#118)site/README.md reconciliado com o Astro 7 (o upgrade de segurança do #116), import sem uso removido e +4 guardas executáveis para os scripts de build do landing: o gate de paridade i18n comprovadamente falha com um dicionário quebrado e o sync-assets nunca escreve fora de site/ — suíte 1822. Dois alertas do CodeQL resolvidos (um corrigido na fonte, outro dispensado como comportamento intencional de build).

[1.118.1] — 2026-07-10

Corrigido

  • Escaneio do hh.ru fora da Rússia — o hh.ru agora devolve HTTP 451 (bloqueio legal regional) a IPs não russos nas páginas públicas de busca. O scanner trata o 451 como o 403: após o primeiro bloqueio o hh.ru é desativado pelo resto da execução com uma mensagem honesta no log apontando para IP russo / saída VPN, sem desperdiçar as consultas restantes nem as demais fontes RU. Ajuda §7 atualizada nos 16 idiomas. +1 teste (1818).

[1.118.0] — 2026-07-09

Pacote de paridade com o career-ops pai v1.18.0.

Adicionado

  • 9 novos provedores de escaneamento — Cornerstone OnDemand (csod), Phenom (phenom), Radancy (radancy), Deutsche Bahn (deutschebahn), EchoJobs (echojobs), TKMS (tkms), Heckler & Koch (hecklerkoch), Rheinmetall (rheinmetall), LaraJobs (larajobs) — agora 54 adaptadores. O adaptador do Lever também detecta boards do tenant EU (jobs.eu.lever.co).
  • Status Hired no tracker (paridade com o states.yml do pai): ofertas aceitas ganham estado canônico próprio, um badge comemorativo e um banner de «vaga conquistada» no #/tracker; o funil e as conversões o contam como tendo avançado por todas as etapas.
  • Aba Histórico em #/stats — relay somente leitura do stats.mjs do pai (resumo histórico do tracker, taxas do funil acumulado, totais do scanner, cobertura de portais) mais observações de remuneração do salary-gap.mjs (desejado vs anunciado vs real, por candidatura). Novas rotas GET /api/stats/lifetime e GET /api/stats/salary-gap — shell-outs de zero tokens, degradação segura {available:false} sem o projeto pai.
  • 28 novas chaves i18n nos 16 idiomas; guia de ajuda §14/§26 atualizado em todos.

Testes

  • +38 testes unitários (três suítes de paridade de provedores + rotas de relay/status) — 1817 no total.

[1.117.2] — 2026-07-06

Correção de tracker vazio para os shell-outs de paridade. Os scripts do pai saem com código 1 e um JSON {error} estruturado quando o tracker ainda não tem candidaturas; o quadro de follow-up e a aba de padrões mostravam isso como «script-error». Ambas as rotas agora o transmitem como um estado vazio saudável (available:true, empty:true) e a UI mostra sua mensagem honesta de «nada ainda». Verificado ao vivo contra um pai real.

Novo: nenhum.

[1.117.1] — 2026-07-06

Endurecimento do v1.117.0 (triagem CodeQL). Os três endpoints shell-out (GET /api/followup, POST /api/followup/seed, GET /api/stats/patterns) agora carregam o limitador por IP compartilhado (geram um processo filho por requisição; no-op em loopback). A extração de texto por URL do Adicionar ao CV remove tags até ponto fixo e depois apaga todo </> restante — saneamento comprovadamente completo para texto de prompt LLM. Sem mudanças para entradas válidas.

Novo: nenhum.

[1.117.0] — 2026-07-06

Pacote de paridade com o pai — seis capacidades do career-ops pai trazidas à UI. (1) Quadro de cadência de follow-up em #/followup com urgência por candidatura (🔴/🟠/🟡/🔵) do followup-cadence.mjs, mais o botão Semear datas (followup-seed.mjs --backfill). (2) Padrões de rejeição: quarta aba de Estatísticas que executa analyze-patterns.mjs (somente leitura) — mistura de resultados, recomendações e taxa de avanço por fornecedor ATS. (3) Adicionar ao CV: um cartão do CV Studio transforma uma URL ou texto colado em tópicos ATS baseados SOMENTE nessa fonte (apenas sugestões, sem gravações; o fetch de URL é protegido contra SSRF). (4) 4 novos provedores de scan — beesite, HigherEdJobs (RSS), JibeApply (iCIMS), softgarden — o registro agora tem 50 adaptadores (45 EN + 5 RU), todos no dropdown do Scan. (5) Etapa de pré-varredura de desqualificadores no checklist do Apply. (6) Runner reconcile (/api/run/reconcile). Rotas shell-out degradam com honestidade sem os scripts do pai.

  • Novo módulo server/lib/routes/followup.mjs (31.º) + novas rotas + 8 arquivos source/adapter. Testes: 6 + 7 novos; suíte 1737 → 1750. 41 chaves i18n ×16. Ajuda §13/§17/§24/§26 ampliada ×16.

Novo: 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 refeito + primeiro teste ponta a ponta de widgets. O medidor de uso de IA (v1.114.0) foi corrigido e fixado corretamente: agora fica fixado no fim da barra lateral esquerda (toda a largura, com a mesma superfície) e reserva embaixo um espaço igual à sua altura para que o menu nunca seja coberto — a navegação e o rodapé de versão sempre rolam livres acima. Ele atualiza ao vivo (a cada 15 s, ao focar a aba e ao mudar de rota), e cada linha de janela mostra agora <tokens> · <custo estimado> real (as barras escalam contra a janela de 30 dias) em vez de uma “parcela” sempre em 100%. Além disso: uma barreira typeof durável no importador de CV fecha na origem o falso positivo recorrente de type-confusion do CodeQL, e um novo teste ponta a ponta do Playwright exercita ambos os widgets persistentes num navegador real.

  • public/js/lib/usage-hud.js + app.css, server/lib/cv-import.mjs. Testes: tests/playwright-widgets.mjs (2 E2E) + tests/usage-hud.test.mjs (10). Ajuda §6 ampliada ×16.

Novo: nenhum.

[1.115.0] — 2026-07-06

Acabamento de design (conservador, marca coral mantida). Uma passada leve de refinamento sobre o sistema de design compartilhado — sem reestruturar, sem mudar a paleta. Os cartões de métricas do painel agora se elevam e ganham uma borda coral ao passar o mouse (como os blocos de ação rápida); os cartões de conteúdo se elevam um pouco; os botões primary / dark / danger ganham uma sombra em repouso e uma leve elevação ao passar o mouse; números grandes se alinham com tabular-nums; e os controles interativos recebem um halo coral suave atrás do anel de teclado de 2px. Todo movimento respeita prefers-reduced-motion, e o halo é restrito aos controles — nunca um *:focus-visible global.

  • Somente CSS (public/css/app.css); sem mudanças de marcação, i18n, rotas ou CSP. Testes: tests/design-polish-v1115.test.mjs (5). Verificado ao vivo com Playwright.

Novo: nenhum.

[1.114.0] — 2026-07-06

Medidor de uso e custo de IA na barra lateral (canto inferior esquerdo). Uma seção USO compacta agora fica no fim da barra lateral (um cartão fixo no canto inferior esquerdo se não houver barra lateral; inferior direito em RTL) em cada página. Mostra seu uso de tokens LLM em janelas de 24h / 7d / 30d — cada uma como <tokens> · <parcela%> com uma barra verde (parcela do total histórico) — mais um rodapé com o custo estimado de 24h. Os dados são o resumo somente leitura GET /api/usage de data/llm-usage.jsonl (só local), a mesma fonte da página #/usage; o custo é uma estimativa e execuções no modo manual são grátis e não contadas. Recolhível — o cabeçalho alterna e o estado persiste.

  • Novo widget cliente public/js/lib/usage-hud.js carregado de index.html, montado na barra lateral acima do rodapé de versão (alternativa de canto fixo). Seguro para CSP; com tema e espelho RTL. Sem nova rota de servidor. Testes: tests/usage-hud.test.mjs (8). 3 novas chaves i18n ×16.

Novo: nenhum.

[1.113.0] — 2026-07-06

Assistente flutuante “Pergunte à ajuda” em cada página. Um botão de chat com um robô e gradiente agora flutua no canto inferior direito (inferior esquerdo em RTL) de cada página. Toque para abrir um chat compacto que responde a perguntas de uso baseando-se SOMENTE no guia de ajuda no seu idioma — o mesmo endpoint da página #/docs-assistant (POST /api/docs-assistant/ask), então nunca lê seu CV, perfil ou rastreador. Ao vivo com uma chave LLM; sem chave → um prompt pronto. O cabeçalho mostra um avatar de robô + status online; chips iniciam perguntas comuns; Esc ou clique fora fecha; oculta-se na página #/docs-assistant.

  • Novo widget cliente public/js/lib/docs-fab.js montado globalmente a partir de index.html; seguro para CSP; estilos com tema e espelho RTL em app.css. Sem nova rota de servidor. Testes: tests/docs-fab.test.mjs (8). 6 novas chaves i18n ×16. Ajuda §1 ampliada no lugar.

Novo: nenhum.

[1.112.0] — 2026-07-06

Consolidação de docs e QA. Sem mudança de código visível. O documento de convenções SDD (docs/sdd/CONVENTIONS.md) é atualizado para os 30 módulos de rota atuais (era 24) e a base de testes atual; o prompt de QA definitivo de todo o projeto (qa/QA-REGRESSION-PROMPT.md) é consolidado — mecânica de release atualizada (v1.111, parentVersion 1.17.0, publicação disparada pelo release), a tabela de adições §14 corrigida (Excluir do Scan reetiquetado v1.109.0) e ampliada com o fechamento do CodeQL de v1.111 — para valer sozinho como o único prompt de regressão de toda a funcionalidade. Adiciona um teste de cobertura para o ramo de upload superdimensionado.

Novo: nenhum.

[1.111.0] — 2026-07-06

Segurança — fechamento do backlog do CodeQL. Três reforços de defesa em profundidade que fecham os achados de análise estática restantes na origem em vez de descartá-los. stripDangerousMarkdown agora escapa o < de qualquer abertura de tag perigosa truncada (uma carga terminando em <script/<iframe/…), de modo que sua saída não contém nenhuma tag perigosa viva. A importação de CV lê o tamanho do buffer enviado por meio de uma coerção explícita com Number() — uma barreira contra confusão de tipos. As linhas de papel dos modos agora são strings de template interpoladas com String.replace em vez de funções armazenadas, removendo por completo a chamada de despacho dinâmico. Sem mudança visível para o usuário.

  • server/lib/security.mjs, server/lib/cv-import.mjs, server/lib/prompts.mjs. Testes: tests/security-hardening-v1111.test.mjs (7) + teste de guarda v1108 atualizado. Sem mudanças de i18n/ajuda/rotas.

Novo: nenhum.

[1.110.0] — 2026-07-06

Atualização de docs e QA (todos os idiomas). Sem mudança de código. O prompt de QA do projeto todo é atualizado para v1.109.0 com um novo §14 (v1.98→v1.109), e os prompts perenes de UX-audit e design-export ganham a superfície atual. Cada parágrafo de ajuda adicionado em v1.100–v1.109 agora está traduzido para os 16 idiomas.

Novo: nenhum.

[1.109.0] — 2026-07-06

Filtro Excluir no Scan + visão geral do pipeline (paridade de layout com a web principal). Em #/scan, a caixa Pesquisar trata vírgulas como OU (“cargos a encontrar”) e um novo campo Excluir oculta qualquer linha cuja empresa/cargo/local contenha alguma palavra separada por vírgula (ex. senior, staff); ambos são lembrados nas suas buscas salvas. Em #/pipeline, uma faixa de visão geral compacta mostra seu pipeline num relance — N na caixa, N rastreadas e as contagens de Applied / Responded / Interview / Offer do tracker, cada chip liga a #/tracker.

  • Somente cliente (sem nova rota/gravações). public/js/views/scan.js + public/js/views/pipeline.js. Testes: tests/scan-pipeline-ui-v1109.test.mjs (2). 4 novas chaves i18n ×16. Ajuda §7 + §8 ampliadas no lugar.

Novo: nenhum.

[1.108.0] — 2026-07-06

Reforço de segurança (triagem do CodeQL, rodada 2). Mais três achados de baixa severidade corrigidos: o construtor de prompts resolve a linha de papel do idioma por chave própria + typeof === function para que um idioma adulterado não despache a um método de protótipo (unvalidated-dynamic-method-call); o slug do nome do arquivo PDF é limitado a 200 caracteres antes do regex para que uma entrada só de hifens não retroceda (ReDoS polinomial); e a importação de documentos coage um filename array (cabeçalho repetido) para string (type-confusion). Sem mudança de comportamento 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). Em v1.106–v1.108 o backlog de análise estática foi de 167 a ~14, com cada achado realmente relevante para segurança corrigido e o restante (falsos positivos protegidos/sanitizados + lint de nível nota) descartado com justificativa.

Novo: nenhum.

[1.107.0] — 2026-07-06

Reforço do sanitizador (defesa em profundidade XSS em repouso). stripDangerousMarkdown — que neutraliza HTML perigoso no markdown de CV/vaga armazenado para que qualquer consumidor que ignore o cliente com escape-ao-renderizar continue seguro — agora executa sua limpeza de tags até um ponto fixo (repetir até estabilizar) para que uma remoção que reforme um payload (ex. <scr<script></script>ipt>) seja capturada, corresponde a tags de fechamento de script/style/etc. com lixo no final (</script foo>) e remove um abridor executável não fechado. O comportamento para markdown válido não muda — só remove mais.

  • server/lib/security.mjs: laço de ponto fixo (limitado a 8 passagens) + padrões de fechamento [^>]*> + remoção de abridor não fechado. +3 casos de regressão em tests/cv-xss-bypasses.test.mjs. O limite XSS autoritativo continua sendo o escape na saída (UI.md); isto reforça a garantia em repouso e fecha os achados do CodeQL correspondentes.

Novo: nenhum.

[1.106.0] — 2026-07-06

Reforço de segurança (triagem do CodeQL). Corrigidos três achados reais (embora de baixa severidade): o caminho de erro de renderização agora escapa a mensagem de erro antes de chegar ao DOM (um erro do servidor pode refletir entrada do usuário, então é tratado como não confiável — limite XSS), e as gravações de propriedades de perfil/config rejeitam as chaves __proto__ / constructor / prototype (proteções contra poluição de protótipo por precaução — as chaves vêm de specs de campos fixos, não de entrada bruta). A maioria dos alertas restantes são falsos positivos sobre leituras/gravações legítimas do scanner em data/* e sobre rotas que já têm o limitador próprio; foram descartados com justificativa.

  • public/js/router.js escapa err.message com UI.escapeHtml antes de innerHTML; server/lib/routes/content.mjs e server/lib/routes/config.mjs protegem as chaves de protótipo. Sem mudança de comportamento para entrada válida. Testes: tests/security-hardening-v1106.test.mjs (3). Sem novas chaves i18n.

Novo: nenhum.

[1.105.0] — 2026-07-06

Página de uso e custo de IA. Uma nova página Uso de IA (barra lateral, ao lado de Saúde) mostra quantos tokens você gastou em gerações de IA ao vivo — avaliações, relatórios, chats — detalhado por provedor nas últimas 24 horas, 7 dias, 30 dias e todo o período, com um custo estimado em USD. Cada chamada ao vivo anexa um pequeno registro {provider, in, out} a data/llm-usage.jsonl (nada é enviado a lugar nenhum); execuções sem chave (modo manual) não custam nada e não são registradas.

  • Novo módulo de rota (30º) server/lib/routes/usage.mjsGET /api/usage (agregações somente leitura) + server/lib/llm-usage.mjs (recordUsage normaliza as formas de uso de Anthropic/OpenAI/Gemini e anexa em best-effort; readUsage/aggregate agregam por janela 24h/7d/30d/tudo × provedor) + server/lib/llm-pricing.mjs (uma tabela editável de preços por provedor $/1M tokens — tokens são exatos, dólares são preços de lista aproximados que você pode corrigir; nunca cobrados). O registro é conectado nos pontos de despacho (runActiveProvider + routes/llm.mjs).
  • Nova visão public/js/views/usage.js (#/usage, abas de janela). Testes: tests/usage-routes.test.mjs. 17 novas chaves i18n ×16 (usage.* + nav.usage). Ajuda §6 ampliada no lugar.

Novo: 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 na tabela de varredura (preservando privacidade). Um novo botão em Aparência nas ConfiguraçõesMostrar logos de empresa na tabela de varredura (desativado por padrão) — desenha o logo de cada empresa ao lado do nome em #/scan. O logo é o favicon da empresa obtido do próprio domínio e com proxy no servidor (GET /api/logo), de modo que nenhum serviço de logos de terceiros descobre quais empregadores você vê. Vagas em um portal compartilhado (Greenhouse, Lever, Ashby, …) mostram um selo com uma letra em vez do ícone do portal, e qualquer logo que falhe ao carregar recai no mesmo selo.

  • Novo módulo de rota (29º) server/lib/routes/logos.mjsGET /api/logo?domain=. Valida o domínio (sem esquema/caminho/loopback), busca /favicon.ico pelo safeGet seguro contra SSRF (um novo modo binary retorna os bytes crus + content-type; fixação de DNS, validação de redirecionamentos e limite de tamanho inalterados), faz sniff de assinatura de imagem para nunca servir uma página HTML de erro como imagem, faz cache de acertos e erros em um LRU em memória e não grava nada em disco.
  • Nova lib cliente public/js/lib/company-logo.js (window.CompanyLogo): desativada por padrão via flag em localStorage; ignora hosts ATS compartilhados em favor de um avatar-letra determinístico; recuo img.onerror seguro para CSP. Testes: tests/logo-routes.test.mjs. 5 novas chaves i18n ×16 (appear.*). Ajuda §2 ampliada no lugar.

Novo: server/lib/routes/logos.mjs; public/js/lib/company-logo.js.

[1.103.0] — 2026-07-06

Configurações: “Ferramentas CLI de IA” — quais estão instaladas. o career-ops é baseado no Claude Code mas funciona com qualquer CLI de agente no padrão aberto de skills. Uma nova aba Ferramentas CLI de IA em Configurações (#/config) mostra quais — Claude Code, Codex, Gemini CLI, OpenCode, GitHub Copilot CLI, Qwen, Antigravity — estão instaladas na máquina que executa o servidor, e seus caminhos. É uma varredura somente leitura do PATH: só verifica se cada binário existe e nunca o executa (sem --version, sem execução), não grava nada e não toca dados do usuário.

  • Novo módulo de rota (28º) server/lib/routes/cli-detect.mjsGET /api/cli-detect. A detecção resolve o caminho de um binário a partir de uma allowlist fixa de 7 entradas em process.env.PATH (shims .cmd/.exe/.bat no Windows; bit de execução no POSIX); um arquivo hostil no PATH nunca pode ser executado por esta rota.
  • Nova aba “Ferramentas CLI de IA” em public/js/views/config.js (carregamento tardio, com deep-link via #/config?tab=cli). Testes: tests/cli-detect-routes.test.mjs. 8 novas chaves i18n ×16 (cli.* + config.tabCli). Ajuda §2 ampliada no lugar.

Novo: server/lib/routes/cli-detect.mjs.

[1.102.0] — 2026-07-05

“Perguntar ao guia” — um chat fundamentado sobre o guia de ajuda integrado. Nova página Perguntar ao guia 💬 (barra lateral, sob Ajuda): digite uma pergunta como “Como escaneio portais de vagas?” e receba uma resposta extraída apenas do guia de ajuda do app no seu idioma — mostra quais seções usou e nunca lê seu currículo, perfil ou busca de emprego. É sobre como usar o app, não sobre você. Com chave LLM responde ao vivo; sem chave entrega um prompt pronto, já preenchido com as seções de ajuda relevantes.

  • Novo módulo de rota (27º) server/lib/routes/docs-assistant.mjsPOST /api/docs-assistant/ask. Recuperação sem dependências: o guia do seu idioma é dividido em suas seções ## e pontuado por sobreposição de palavras-chave com sua pergunta; as melhores são incluídas e o modelo deve responder a partir delas ou dizer que o guia não cobre (sem recursos/rotas inventados). Cascata de provedores compartilhada, recuo manual, com limite de taxa, sem gravações, não lê dados do usuário.
  • Nova visão public/js/views/docs-assistant.js. Testes: tests/docs-assistant-routes.test.mjs. 14 novas chaves i18n ×16 (docs.* + nav.docsAssistant). Ajuda §1 ampliada no lugar.

Novo: server/lib/routes/docs-assistant.mjs; public/js/views/docs-assistant.js.

[1.101.0] — 2026-07-05

CV Studio: adapte seu currículo + escreva uma carta de apresentação para uma vaga específica, com verificação estilo recrutador. Novo cartão Adaptar a uma vaga em #/cv-studio: cole uma descrição de vaga (e, opcionalmente, um cargo/título-alvo) e o CV Studio produz um currículo adaptado a essa vaga mais uma carta de apresentação correspondente, depois passa ambos por uma verificação antes de entregar — error bloqueia (corrigido antes de você ver), warn aconselha. A mecânica é destilada da prática de coaching de carreira em regras genéricas — o recrutador lê em segundos, então o relevante vai ao topo, o título combina com o cargo da vaga, os resultados trazem números específicos e a carta é um teaser curto com uma única ponte “requisito ↔ seu fato correspondente”. Baseia-se apenas no seu currículo, perfil e two-pager e nunca inventa — sem empresas, cargos ou histórico embutidos.

  • Novo endpoint POST /api/cv-studio/tailor (amplia o módulo cv-studio existente — sem 27º módulo): buildTailorPrompt + verificação genérica TAILOR_INSTRUCTIONS, baseada em bundleProjectContext, cascata de provedores compartilhada, recuo manual sem chave, com limite de taxa, sem gravações. O resultado exporta para Markdown / PDF / DOCX pela barra compartilhada report-export.js.
  • Testes: +3 em tests/cv-studio-routes.test.mjs. 10 novas chaves i18n ×16 (cvs.tailor*). Referência genérica docs/prompts/resume-cover.md. Ajuda §24 ampliada no lugar.

Novo: docs/prompts/resume-cover.md.

[1.100.0] — 2026-07-05

Two-pager: autopreenchimento por IA a partir do seu CV + Pré-visualização + exportar para PDF/DOCX/Markdown. O two-pager (#/two-pager) registra o que você realmente quer da sua próxima vaga, mas antes era preciso redigir cada campo à mão ou copiar um prompt para outra ferramenta. Agora o ✨ assistente de preenchimento por IA roda ao vivo com o provedor configurado: lê apenas seu CV + perfil (via bundleProjectContext, sem inventar nada), redige todos os campos (quem sou / gosto / indispensáveis / detesto / limites / inegociáveis / ambiente-alvo) e preenche o formulário para você revisar, editar e salvar. Sem chave de API, volta ao modal de copiar-o-prompt, como antes. Um novo botão 👁 Pré-visualizar e exportar renderiza o two-pager como documento com uma barra Baixar .md / Salvar como PDF / Salvar como DOCX / Copiar.

  • Exportação .docx sem dependências. Novo server/lib/docx.mjs que gera um .docx Office Open XML mínimo mas válido (um ZIP DEFLATE das quatro partes OOXML, com CRC-32 por entrada) — sem nova dependência (as deps continuam express + js-yaml). Nova rota POST /api/export/docx (server/lib/routes/export.mjs, o 26º módulo de rotas; sem estado, limitado a 200 KB, sem gravações / sem LLM / sem fetch de URL). Integrado ao public/js/lib/report-export.js compartilhado, então o relatório de mercado, o plano de carreira e a orientação profissional também ganham exportação para DOCX.
  • O autopreenchimento ao vivo usa a cascata de provedores compartilhada (runActiveProvider / providerAvailable); o YAML retornado é parseado e ajustado de volta ao formato limitado do two-pager (parseYamlFields + normalizeTwoPager) — chaves desconhecidas descartadas, arrays/strings limitados. Modo manual preservado.
  • Testes: tests/export-routes.test.mjs. 4 novas chaves i18n ×16 (export.saveDocx, twoPager.preview, twoPager.aiFilling, twoPager.aiFilled).

Novo: server/lib/docx.mjs; server/lib/routes/export.mjs.

[1.99.0] — 2026-07-05

Página de saúde dos portais (#/portals). O scanner observa um conjunto de empresas em portals.yml; um slug de ATS pode quebrar silenciosamente e aquele empregador some de todos os scans futuros. A nova página Portals lista cada empresa observada e, ao clicar Check portal health, sonda cada careers_url através do safeGet com DNS fixado (à prova de SSRF) e sinaliza as mortas (um 404 = descartada em silêncio) — somente leitura. Também reforça o relator de bugs da v1.98.0 após a revisão: o buffer de erros agora captura falhas de rede do fetch, e o limpador oculta chaves de provedor sem rótulo.

Novo: server/lib/routes/portals.mjs; public/js/views/portals.js.

[1.98.0] — 2026-07-05

Relator de bugs integrado (paridade com o web web-v0.2.0 do projeto pai). Um botão 🐞 Report a bug na gaveta de notificações reúne um instantâneo de diagnóstico com piso de privacidade — versões, sua tela, navegador, um resumo de verificações do /api/health e os últimos 20 erros de um novo buffer circular do cliente — mais uma impressão digital de deduplicação determinística (co-web-<base36>), permite revisar o Markdown exato e então abre uma issue do GitHub pré-preenchida. Nada é enviado automaticamente; nunca leva seu CV, perfil, respostas, URLs de vagas ou chaves. Novas libs logbuf.js + bug-report.js; 11 chaves i18n ×16; tests/bug-report.test.mjs.

Novo: public/js/lib/logbuf.js; public/js/lib/bug-report.js.

[1.97.1] — 2026-07-05

Endurecimento guiado por revisão e paridade de documentação (continuação da v1.97.0). Uma varredura dos logs de revisão de IA revelou correções reais:

  • fit-score.js (selo de encaixe do scanner). O salaryFloor() não promove mais uma taxa inferior à anual a um piso anual falso — “at least 500 EUR/day”, “$80/hr”, “6000 monthly” agora retornam null em vez de um fator eliminatório de 500k/80k. A correspondência de países agora é por palavra inteira (\b…\b), de modo que “Germany” não corresponde mais ao adjetivo “German” (nem “Nigeria” dentro de “Nigerian”) nem dispara uma falsa violação de indispensável-em-outro-lugar. +3 testes em tests/fit-score.test.mjs.
  • Paridade de documentação. Cada README localizado agora anuncia 16 idiomas de forma consistente — a contagem/lista da linha de Ajuda (×13) e a prosa da seção de Localização mais a nota “adicione a chave a todos os N arquivos” (×8) ainda estavam nas contagens anteriores à v1.85 (8/9). A contagem de adaptadores da ajuda integrada §17 é corrigida para 46 adaptadores — 41 em inglês + 5 em russo em todos os 16 pacotes.

Nenhuma mudança de comportamento além da heurística do selo de encaixe; nenhuma nova rota, chave ou adição de i18n.

[1.97.0] — 2026-07-05

Fonte de scanner Dassault Systèmes + uma varredura de qualidade em três frentes.

  • Nova fonte de escaneamento — Dassault Systèmes (paridade com o career-ops principal, #1498). server/lib/sources/dassault.mjs + server/lib/portals/adapters/dassault.mjs espelham o provedor de “busca por cartões” Exalead de custo zero em tokens do projeto principal (o feed público por trás de 3ds.com/careers/jobs). É um único endpoint global, então é selecionado por provedor (provider: dassault) ou detectado automaticamente a partir de um host 3ds.com, com o host fixado contra SSRF em www.3ds.com via redirect:'error'. O XML é analisado sem DOM (mapas <Meta> por <Hit>), a cidade/país são extraídas da string de categoria localizada, e as vagas só são mantidas quando sua URL pública está em *.3ds.com. O registro agora traz 46 adaptadores (41 EN + 5 RU); a contagem de ALL_ADAPTERS, as asserções de id ordenado e do conjunto EN de /api/scan/sources passam de 40 → 41. Suíte tests/sources-dassault.test.mjs (10 casos).
  • Correções de robustez portadas do projeto principal. O parser do Avature agora tolera duas variantes de marcação de tenant em produção (article--result com um sufixo de índice de posição + uma âncora de título de JobDetail sem classe, #1541); o Get on Board protege contra um published_at 0/negativo (nada mais de datas espúrias de 1970); o SuccessFactors limita a última página para que ela não ultrapasse MAX_JOBS (#1528).
  • Correções de auditoria do servidor. O safe-fetch não trava mais diante de uma resposta acima do limite — o caminho de limite de tamanho agora resolve a promessa diretamente em vez de esperar por um evento 'end' que um stream destruído nunca emite (corrige as buscas de páginas grandes em /api/pipeline/preview + auto-pipeline). O registro de atividade SSE stream.* voltou a ser alcançável (a verificação de /api/stream/ foi movida acima da guarda geral de “pular GET”).
  • Correções de auditoria da SPA. O alternador de abas de #/stats protege contra uma corrida de renderização assíncrona — o resultado de uma aba lenta não pode mais sobrescrever uma aba mais nova para a qual o usuário já alternou. As confirmações de exclusão do mock interview e do networking agora passam um título + corpo adequados (nada mais de diálogo com corpo vazio).
  • Correções de tradução. Valores de dicionário sem tradução corrigidos — ucraniano config.modes* (Adaptive Framing / Exit Narrative / Location Policy), russo eval.jdLbl (“Job Description”), italiano dash.quick.contactoSub (“referral” → “segnalazione”) — além da localização do texto fixo inglês **16 locales** nos CHANGELOGs de ru/uk/ja/ko/zh-CN/zh-TW.

Novo: server/lib/sources/dassault.mjs; server/lib/portals/adapters/dassault.mjs.

[1.96.0] — 2026-07-04

Orientação de carreira (Epic 27). Uma nova página #/orientation responde à pergunta “quais direções realmente combinam comigo?” — a leitura que você teria de um teste vocacional, mas inferida a partir do seu próprio CV e perfil em vez de um questionário. Clique em Gerar perfil e o modelo devolve seus vetores de carreira com melhor ajuste (quais dos oito arquétipos — Funcionalista, Administrador, Comunicador, Especialista, Analista, Inovador, Gestor, Empreendedor — combinam, com evidências), uma inclinação de tipo profissional, funções recomendadas, pontos fortes profissionais ligados ao seu CV, tendências de estilo de trabalho e recomendações de desenvolvimento. É uma reflexão de IA sobre como o seu CV é lido — não um teste psicométrico: nunca inventa conquistas nem informa pontuações numéricas como se fossem medidas. Exporte-o para Markdown ou PDF; nada é gravado no disco.

  • Nova rota server/lib/routes/orientation.mjs (24.º módulo de rotas) — POST /api/orientation/generate constrói o prompt do perfil a partir de CV+perfil+two-pager+memória via a cascata compartilhada de provedores, com um fallback manual de copiar e colar e sem escrita de arquivos.
  • Reutiliza report-export.js para Markdown/PDF/cópia, dentro do grupo de navegação Desenvolvimento.
  • Testes: tests/orientation-routes.test.mjs (enquadramento de reflexão / sem pontuações fabricadas, modo manual semeado com CV/perfil). 7 novas chaves i18n ×16 idiomas, Ajuda §28 ×16.

Novo: #/orientation; server/lib/routes/orientation.mjs.

[1.95.0] — 2026-07-04

Plano de carreira (Epic 26). Uma nova página #/career-plan transforma o seu CV e o seu perfil em um plano de desenvolvimento concreto e personalizado. Escolha um horizonte (6/12/24 meses) e um foco opcional, e o modelo — lendo o seu CV, o seu perfil, o seu two-pager e a sua nota de memória — redige um retrato do ponto de partida, uma SWOT de forças/crescimento, metas como SMART / OKR / WOOP, trajetórias alternativas, um plano de competências técnicas e comportamentais, um roteiro mês a mês, métodos de acompanhamento do progresso, armadilhas e alavancas de apoio. Planeja adiante a partir do que os seus materiais realmente mostram e nunca inventa fatos sobre o seu histórico. Edite-o em linha, Salve-o na camada de usuário (config/career-plan.md) e exporte-o para Markdown ou PDF.

  • Nova rota server/lib/routes/career-plan.mjs (23.º módulo de rotas) — GET/PUT /api/career-plan (escreve config/career-plan.md) + POST /api/career-plan/generate (cascata compartilhada de provedores, modo manual como fallback, sem fabricação). PATHS.careerPlan.
  • Reutiliza o helper compartilhado report-export.js (v1.94.0) para Markdown/PDF/cópia, e um novo grupo de navegação Crescimento.
  • Testes: tests/career-plan-routes.test.mjs (limitação, ida e volta GET/PUT, prompt semeado com CV/perfil de acordo com o horizonte). 20 novas chaves i18n nos 16 locales, Ajuda §27 nos 16.

Novo: #/career-plan; server/lib/routes/career-plan.mjs; PATHS.careerPlan.

[1.94.0] — 2026-07-04

Estatísticas, reformuladas (Epic 25). A página #/stats agora é uma seção de Estatísticas com três abas, com gráficos reais e muito mais dados. Uma nova aba de Relatório de mercado pede ao modelo uma análise salarial e do mercado de trabalho dos seus cargos-alvo na região e na moeda que você escolher — sumário executivo, salário por nível com percentis P10/P25/P75/P90, principais empregadores, uma tabela de competências em demanda, frequência de benefícios, a divisão presencial/híbrido/remoto, tendências de 12–24 meses e orientação de negociação. Cada número é rotulado como uma estimativa direcional a partir do conhecimento do modelo, nunca apresentado como dados extraídos. Uma nova aba de Meu pipeline plota o seu próprio tracker: distribuição de pontuações, funil de status, principais empresas e cargos, candidaturas ao longo do tempo e taxas de conversão. A visão original de cargo-alvo (vagas/salário por país + tendência de snapshots salvos) passa para uma terceira aba, agora com um seletor de moeda e uma visão geral de publicações por cargo.

  • Exporte qualquer relatório para Markdown ou PDF, ou copie-o — via o helper compartilhado report-export.js (download de blob Markdown; PDF através do runner de PDF em linha existente).
  • Nova rota server/lib/routes/market.mjs (22.º módulo de rotas) — POST /api/stats/market constrói um prompt de análise de mercado a partir do seu CV/perfil (de modo que conhece os seus cargos-alvo), da região e da moeda, executa-o pela cascata compartilhada de provedores e recorre a um prompt para copiar e colar quando não há chave. Sem escritas em disco.
  • Testes: tests/market-routes.test.mjs (limitação de região/moeda, prompt com rótulo de honestidade, modo manual semeado com CV/perfil). 36 novas chaves i18n nos 16 locales, Ajuda §26 nos 16.

Novo: #/stats reformulada em abas; server/lib/routes/market.mjs; public/js/lib/report-export.js.

[1.93.0] — 2026-07-04

Camada de memória (Epic 24). Uma nova página #/memory guarda uma nota curta e editável de “lembre-se disto sobre mim” que o assistente mantém em mente em cada tarefa:

  • Uma nota, em todo lugar — como ela é embutida em bundleProjectContext, a nota alcança automaticamente cada requisição de IA (avaliar, mock interview, networking, CV Studio) em todos os provedores. Escreva-a uma vez; ela orienta tudo.
  • Orientação, não fatos — captura suas preferências e como você gosta de trabalhar (tom, formato, deal-breakers, cadência), nunca novas afirmações factuais sobre a sua experiência — essas continuam vivendo apenas no seu CV, perfil e two-pager. Salva na camada do usuário em config/memory.md, nunca sobrescrita por atualizações.
  • Sugerir a partir dos seus dadosPOST /api/memory/suggest garimpa o seu próprio tracker de candidaturas em busca de padrões de comportamento e rascunha tópicos para você revisar e editar. Ele lê o seu tracker; nunca inventa fatos, e não faz nenhuma chamada ao vivo.

Novo: server/lib/routes/memory.mjs (21.º módulo de rotas — GET/PUT /api/memory + POST /api/memory/suggest), public/js/views/memory.js, PATHS.memory, e um bloco config/memory.md adicionado a bundleProjectContext. 11 novas chaves i18n nos 16 locales. Testes: tests/memory-routes.test.mjs.

[1.92.0] — 2026-07-04

CV Studio (Epic 21). Uma nova página #/cv-studio dá ao seu CV três ferramentas honestas e majoritariamente locais:

  • Diagnóstico de currículo — uma pontuação determinística de 0–100 com explicações por verificação (impacto quantificado, verbos fracos, jargões, tamanho, seções essenciais, informações de contato). Puramente no lado do cliente (window.CvDiagnostics) — sem LLM, nada inventado, cada achado explicado para que você decida o que mudar.
  • Máscara de privacidade — oculta dados pessoais (e-mail, telefone, links/handles, endereço e, opcionalmente, seu nome → iniciais) antes de compartilhar seu CV como amostra ou captura de tela. Roda inteiramente no navegador (window.CvPrivacy); relata exatamente o que ocultou e nunca armazena o original.
  • Torne-o humano / correspondência de voz — cole uma linha ou parágrafo rígido e reescreva-o na sua voz, ancorado no servidor em voice-dna.md e writing-samples/. Barreira rígida: pode reordenar, enxugar e revocalizar, mas nunca introduz um fato, uma métrica ou uma conquista que não esteja já no texto. Roda ao vivo através da cascata compartilhada de provedores, ou devolve um prompt para copiar e colar sem chave.

Novo: server/lib/routes/cv-studio.mjs (20.º módulo de rotas — 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 novas chaves i18n nos 16 locales. Testes: tests/cv-diagnostics.test.mjs, tests/cv-studio-routes.test.mjs. (A galeria de modelos, a exportação para Word e o arquivo de PDF da vaga ficam como trabalho de acompanhamento do CV Studio.)

[1.91.0] — 2026-07-04

Networking e pesquisa profunda de empresas (Epic 16). Uma nova página #/networking transforma uma empresa em um plano acionável para conseguir uma entrevista, ancorado no seu CV, perfil e two-pager:

  • Dossiê da empresa — um resumo enxuto do que a empresa faz, sinais recentes dignos de citação e ganchos de “por que eu encaixo” extraídos da sua trajetória real.
  • Quem contatar — 3–5 personas-alvo (hiring manager, recrutador interno, um IC sênior da equipe, um contato caloroso/de ex-alunos) com uma string de busca do LinkedIn concreta para encontrar cada um. Nunca inventa nomes reais.
  • A via de apresentação mais calorosa — a rota calorosa mais realista para a sua trajetória (empregador/escola/comunidade em comum, um caminho de segundo grau ou uma mensagem direta a frio de alto sinal) e por quê.
  • Rascunhos de contato — mensagens curtas e específicas para as principais personas, ancoradas nos seus pontos de prova reais.
  • Ao vivo ou manual — roda ao vivo através da cascata compartilhada de provedores com qualquer chave, ou devolve um prompt pronto para copiar e colar (fallback honesto, nada inventado). Salvar plano persiste um plano finalizado na camada do usuário (networking/net-{company}-{role}-{date}.md); a página lista, abre e exclui os planos salvos.

Novo: server/lib/routes/networking.mjs (19.º módulo de rotas), public/js/views/networking.js, PATHS.networkingDir. Reutiliza a cascata server/lib/llm-dispatch.mjs da v1.90.0. 24 novas chaves i18n nos 16 locales. Testes: tests/networking-routes.test.mjs.

[1.90.0] — 2026-07-04

Mock Interview 2.0 (Epic 15). Uma nova página #/mock-interview transforma seu CV, perfil, two-pager e banco de histórias em um ensaio de entrevista turno a turno:

  • Prática conversacional — defina um cargo-alvo (+ empresa / JD opcionais) e o entrevistador abre com uma pergunta focada. Cada resposta que você envia recebe uma réplica estruturada: Feedback (pontos fortes + a lacuna STAR+R), uma Pontuação (N/5) e uma Próxima pergunta que sonda a parte mais fraca da sua última resposta. Ancorada no servidor aos seus materiais reais — nunca inventa experiência que você não tem.
  • Consciente do banco de históriasinterview-prep/story-bank.md é embutido no prompt (mesmo nível de confiança que cv.md), de modo que o feedback pode apontá-lo para as suas próprias melhores histórias.
  • Ao vivo ou manual — com uma chave de provedor, o turno roda ao vivo pela cascata compartilhada de provedores (Anthropic → Gemini → OpenAI → Qwen → OpenRouter → GitHub Models); sem chave você obtém um prompt pronto para copiar e colar (fallback honesto, sem respostas inventadas).
  • Sessões salvas — clique em Save transcript para persistir uma entrevista concluída na camada de usuário (interview-prep/mock-{company}-{role}-{date}.md); a página lista, abre e exclui as sessões salvas.

Novo: server/lib/routes/interview.mjs (18.º módulo de rotas), public/js/views/mock-interview.js, server/lib/llm-dispatch.mjs (cascata compartilhada de provedores), PATHS.storyBank, bundleProjectContext({ extraFiles }). 30 novas chaves i18n nos 16 locales. Testes: tests/interview-routes.test.mjs.

[1.89.0] — 2026-07-04

Ajuste candidato-mercado — o two-pager (Epic 14). Uma nova página #/two-pager permite capturar o que você realmente quer da sua próxima vaga, inspirada no “Mnookin two-pager” de Never Search Alone:

  • Construtor guiado — uma narrativa em primeira pessoa “Quem eu sou”, uma nota “Ambiente-alvo” e cinco editores de listas de chips: loves, must-haves, hates, deal-breakers e non-negotiables. Salvo na camada de usuário do projeto pai (config/two-pager.yml) via PUT /api/two-pager — nunca sobrescrito por atualizações do sistema.
  • Assistente de preenchimento com IA (POST /api/two-pager/draft) — monta um prompt Mnookin pronto para rodar com seu CV + perfil embutidos, para você executar em qualquer LLM e colar de volta. Ele só usa seus próprios materiais; nada é inventado.
  • Selo de encaixe-com-o-que-você-quer — cada vaga em #/scan agora exibe uma pontuação de encaixe ◎ N (no lado do cliente, via window.FitScore) que confronta o tipo de trabalho, país, piso salarial e realocação da vaga com o seu two-pager. Honesto por design: quando uma vaga não fornece sinal comparável, nenhum selo é exibido (nunca um número inventado). Violações de deal-breakers pesam mais do que desagrados leves.
  • Alimenta cada avaliação — o two-pager salvo é embutido em bundleProjectContext, de modo que todas as avaliações LLM subsequentes combinam suas preferências declaradas com o encaixe CV-vs-vaga.

Novo: server/lib/routes/two-pager.mjs, public/js/views/two-pager.js, public/js/lib/fit-score.js, PATHS.twoPager. 27 novas chaves i18n em todos os 16 locales. Testes: tests/two-pager-routes.test.mjs, tests/fit-score.test.mjs.

[1.88.0] — 2026-07-04

Polimento do issue #29 — lacunas de i18n no Escaneamento + higiene da API.

  • Localizadas as últimas strings de Escaneamento codificadas (roadmap v1.69.4): as pílulas de resumo por fonte (N novas / M correspondentes), os avisos N novas vagas e o selo reloc agora fluem por t() — 4 novas chaves (scan.pillNew, scan.pillMatching, scan.newOffers, scan.relocBadge) em todos os 16 locales. Usuários que não falam inglês não veem mais texto solto em inglês no fluxo principal de escaneamento.
  • Desabilitado o cabeçalho X-Powered-By (roadmap v1.69.5): app.disable('x-powered-by') em createApp() — o servidor não anuncia mais o Express. (O restante desse épico já havia sido entregue: parentVersion remove seu comentário de release-please, o alternador de tema em modo claro, o fechamento de modais ao mudar de rota e a localização de “Score” (rep.score) nos Relatórios.)

Testes: tests/scan-i18n-gaps.test.mjs + uma asserção de ausência de X-Powered-By em tests/security-headers.test.mjs.

[1.87.0] — 2026-07-04

4 novos provedores de escaneamento sem autenticação (paridade com o career-ops pai v1.16.0). O registro do scanner cresce de 41 → 45 adaptadores (40 EN + 5 RU) — todos públicos, sem autenticação, com host fixado, redirect:'error' (seguro contra SSRF), cada um com um teste isolado para CI:

  • Get on Board (getonbrd) — JSON:API público de todo o portal (tecnologia LATAM/remoto), selecionado por provedor, paginado. server/lib/sources/getonbrd.mjs.
  • Amazon (amazon) — JSON de busca público do amazon.jobs, detectado por host ou provider: amazon, paginado por offset. server/lib/sources/amazon.mjs.
  • Avature (avature) — ATS *.avature.net por inquilino, analisado a partir de HTML, detectado por host ou provider: avature. server/lib/sources/avature.mjs.
  • SAP SuccessFactors (successfactors) — lista de blocos RMK por inquilino (*.successfactors.eu/.com, jobs2web.com), analisada a partir de HTML. server/lib/sources/successfactors.mjs.

Cada um inclui um sources/<slug>.mjs (com meta autodescoberto → menu suspenso #/scan) e um portals/adapters/<slug>.mjs em ALL_ADAPTERS (a regra dos dois registros) + tests/sources-<slug>.test.mjs. A contagem de ALL_ADAPTERS e as asserções de id ordenado e do conjunto EN de /api/scan/sources subiram de 36→40; GET /api/scan/sources agora lista 45.

[1.86.0] — 2026-07-03

Estatísticas por cargos-alvo (#/stats) — estatísticas de vagas e salários de mercado para OS SEUS cargos-alvo. Uma nova página de Análise lê os seus cargos-alvo do perfil (config/profile.yml → não codificados) e as vagas do último escaneamento, e então mostra, por cargo e país:

  • Vagas por país e salário mediano por país (USD) — agregados no cliente (public/js/lib/role-stats.js, reutilizando window.Countries) a partir dos dados esparsos que os scanners já coletam. Salários em qualquer moeda são normalizados para USD por meio de uma tabela FX explicitamente aproximada, com uma ressalva sobre o tamanho da amostra — nunca fabricados.
  • Filtros de cargo e país e gráficos de barras e de tendência com SVG inline feitos à mão (sem novas dependências, seguros para CSP — apenas addEventListener).
  • Salvar instantâneo (POST /api/stats/snapshot) persiste o agregado atual em data/role-stats.jsonl; o gráfico de tendência (GET /api/stats/trend) acompanha a contagem de vagas ao longo do tempo — a visão de “dinâmica”. Híbrido honesto: os instantâneos vêm de dados de escaneamento locais, atualizados sob demanda.
  • Totalmente localizado em todos os 16 locales (26 novas chaves i18n).

Novo: server/lib/routes/stats.mjs (16.º módulo de rotas), public/js/lib/role-stats.js, public/js/views/stats.js, PATHS.roleStats; testes role-stats.test.mjs (7) + stats-routes.test.mjs (5).

[1.85.0] — 2026-07-03

Locales alemão, italiano e turco (paridade de locales com career-ops pai v1.16.0). A interface agora é distribuída em 16 idiomasde 🇩🇪, it 🇮🇹 e tr 🇹🇷 juntam-se aos 13 existentes.

  • Tradução completa da interface — todas as 730 chaves i18n traduzidas em public/js/lib/locales/i18n-dict.{de,it,tr}.js; o seletor de idioma lista Deutsch / Italiano / Türkçe e a detecção automática do idioma do navegador reconhece de/it/tr (public/js/lib/i18n.js).
  • Guia de ajuda integradodocs/help/{de,it,tr}.md traduzidos (estrutura completa de 19 H2 / 75 H3), servidos por GET /api/help/:lang.
  • DocumentaçãoREADME.{de,it,tr}.md e CHANGELOG.{de,it,tr}.md adicionados; a verificação de paridade de locales do CHANGELOG agora cobre 15 locales não EN.
  • Andaime de promptsserver/lib/prompts.mjs (LOCALE_NAMES + SCAFFOLD_STRINGS) localizado para os três novos locales, de modo que a saída do LLM segue o idioma da interface.

Todas as verificações de paridade (i18n-locale-files, i18n-coverage, check-changelog-parity, lang-switcher-rtl) estendidas ao conjunto de 16 locales.

[1.84.0] — 2026-06-30

Cooldown de recandidata­tura + compensação em pipeline.md (paridade com career-ops pai v1.15.0). Duas melhorias no scanner:

  • Cooldown de recandidatura (#1201): o scan EN agora ignora vagas em empresas às quais você se candidatou recentemente, para que os resultados fiquem focados em NOVAS vagas. Configure janelas por empresa em config/profile.yml sob re_apply_windows: (last_apply_date, same_role_days, applied_to: [roles], cross_role_bucket opcional); a correspondência de empresa é insensível a pontuação e baseada em limites de palavra (server/lib/cooldown.mjs). Desativado quando a chave está ausente; o log de varredura exibe Cooldown skipped: N.
  • Compensação em pipeline.md (#1017): as vagas varridas agora persistem seu salário como coluna opcional ao final (url | <salary>) em data/pipeline.md. A URL continua sendo a chave de deduplicação (a coluna | comp é removida na leitura), a célula é sanitizada (sem injeção de linhas/colunas, fórmulas iniciais neutralizadas) e pipelines com apenas URL permanecem retrocompatíveis.

Inclui tests/cooldown.test.mjs + testes de compensação de pipeline. O número de fontes permanece em 41 (ambas são melhorias de lógica de varredura, não novos boards).

[1.83.0] — 2026-06-30

Detector de repostagens / vagas fantasma (paridade com career-ops pai v1.15.0). Um novo painel 🔁 Repostagens / vagas fantasma em #/scan sinaliza clusters empresa+vaga que foram relançados sob URLs diferentes em uma janela móvel de 90 dias — sinal de pipelines obsoletas e vagas fantasma. Sustentado por um comparador difuso de títulos de vaga (server/lib/role-matcher.mjs) e um detector somente leitura (server/lib/detect-reposts.mjs) sobre data/scan-history.tsv, exposto via GET /api/scan/reposts. Além disso: parentVersion em /api/health agora reporta apenas o semver (o comentário # x-release-please-version do release-please é removido). Inclui tests/detect-reposts.test.mjs. O número de fontes permanece em 41 — reposts é uma funcionalidade de análise, não um novo board.

[1.82.0] — 2026-06-30

Fonte de varredura NoDesk (paridade com career-ops v1.15.0). O feed RSS de vagas remotas do NoDesk agora é uma fonte de primeira classe — adicione uma entrada provider: nodesk e ela aparece no menu Source de #/scan (41 adaptadores no total: 36 EN + 5 RU). Host fixado em nodesk.co com redirect:'error' (anti-SSRF); títulos divididos por Role at Company (NoDesk não tem tag de localização, então a localização fica vazia); todas as linhas são remotas. Inclui uma suite CI isolada tests/sources-nodesk.test.mjs; suite de testes unitários completa no verde com 1523.

[1.81.0] — 2026-06-29

Paridade com o career-ops pai — 13 novas fontes de varredura de bolsas de emprego. Porta o último lote de provedores do main do Fighter90/career-ops para o scanner em processo. APIs públicas universais (selecionadas por provedor): Arbeitnow, Himalayas, Jobicy, Landing.jobs, 4 Day Week, The Muse, The Hub, Jobspresso (RSS) e Hacker News “Who is hiring?” (Algolia em dois passos). Bolsas polonesas (detectadas por host ou provider:): JustJoin.it e NoFluffJobs (busca POST). ATS por tenant (autodetectados de careers_url): Pinpoint (<slug>.pinpointhq.com/postings.json) e Rippling (ats.rippling.com/<slug>api.rippling.com). Cada fonte é fixada por host com redirect:'error' (anti-SSRF) e selecionável no menu Source de #/scan — o registro conta agora com 40 adaptadores de scanner (35 EN + 5 RU). Adiciona 13 suítes de testes CI isoladas por fonte; suite de testes unitários completa no verde com 1513 testes.

[1.80.0] — 2026-06-28

Cinco melhorias de varredura (ideias do job-crawler, reimplementadas). (1) Fonte Teamtailor — sites <slug>.teamtailor.com via o feed público /jobs.rss, autodetectado de careers_url (host fixado + redirect:'error'); o registro agora traz 27 adaptadores. (2) Quarentena de fontes — uma fonte com 404/410 permanente é gravada em data/scan-quarantine.json e ignorada em varreduras posteriores (autocorrigível: nova tentativa após 14 dias). (3) Máx. por fonte — campo opcional em #/scan que limita vagas por board (∞ por padrão). (4) Publicado em — filtro de idade no cliente (24h / 7d / 30d). (5) Buscas salvas + ★ favoritos — nomeie e reutilize conjuntos de filtros e marque vagas, em localStorage com validação defensiva (cache corrompido reinicia limpo); o cache de resultados é reiniciado antes de cada varredura e preenchido ao vivo.

[1.79.0] — 2026-06-28

Fonte de varredura WeWorkRemotely (paridade com career-ops v1.14.0). O feed RSS de vagas remotas do We Work Remotely agora é uma fonte de primeira classe — adicione uma entrada provider: weworkremotely e ela aparece no seletor Source de #/scan (26 adaptadores no total). Host fixado em weworkremotely.com com redirect:'error' (anti-SSRF); títulos divididos por Company: Role. Além disso: as palavras-chave de title_filter agora são aparadas antes da verificação de comprimento (parent #1261).

[1.78.2] — 2026-06-27

Reforço de i18n e UX (correções após v1.78.1). O nome acessível do logo agora é localizado nos 13 idiomas (nav.logoHome). Enter na busca global estando já em #/scan força um re-render para não perder o termo pré-preenchido (guard de mesma rota). health.title agora é traduzido para polonês (Kondycja) e dinamarquês (Systemtilstand) — antes ficava em inglês. Testes 1235 → 1238.

[1.78.1] — 2026-06-27

Correções de UX no Scan. A tabela de resultados de #/scan agora atualiza automaticamente durante a varredura e mais uma vez ao terminar, sem recarregar. A busca global da barra superior mostra a dica Enter e, em uma consulta que não é URL, vai para #/scan com o campo pré-preenchido (antes #/tracker). O logo agora leva ao painel (início).

[1.78.0] — 2026-06-27

Filtro geográfico na página Scan — filtre resultados por país, com bandeiras. Um novo seletor País em #/scan lista cada país detectado nos seus resultados (emoji de bandeira + contagem), para manter só vagas ligadas a um país — junto ao filtro Remote/Hybrid/Onsite, permitindo buscar trabalho ligado a um país e remoto. Suportado por um novo helper countries.js que mapeia a localização em texto livre (nomes de país, aliases e ~100 cidades-chave) para um país ISO + bandeira; a detecção é conservadora e nunca adivinha.

[1.77.0] — 2026-06-27

Dinamarquês (Dansk) adicionado como o 13.º idioma da interface. Tradução completa da UI, do guia de ajuda integrado (19 H2 / 75 H3), README e CHANGELOG. O dinamarquês entra no seletor de idiomas com bandeiras; a maquinaria i18n (montador, auditoria, verificações de paridade, snapshot) agora abrange 13 locales.

[1.76.0] — 2026-06-26

Paridade com career-ops v1.13.0 — seis novas fontes, reforço do scanner e tabela de resultados sem limite.

Adicionado

  • Seis fontes ATS por inquilino — BambooHR, Breezy HR, Comeet, Personio, Recruitee, SolidJobs. Autodetectadas pelo host de careers_url (Comeet exige o api: completo); cada host é fixado com regex ancorado + redirect:'error' (anti-SSRF). Selecionáveis no dropdown Source de #/scan — o registro agora traz 25 adaptadores (20 EN + 5 RU). Adiciona fetchText para o feed XML do Personio.
  • trust_filter — pontuação de confiança opcional (0–100, nível high/medium/low, flags), apenas anota. Linhas abaixo de high recebem um selo ⚠ neutro em #/scan; nada é descartado.
  • Arbeitsagentur remoteMatch + remoteMaxPages — detecção de remoto por config: title, filter (homeoffice=nv_true no servidor + paginação) ou off.

Alterado

  • Sem limite de resultados. MAX_STORED_RESULTS (2000) foi removido — todas as correspondências são armazenadas e a tabela #/scan as pagina (200/pág.).
  • Robustez do filtro de título — siglas curtas (COO, SDR…) casam por limites de palavra; config title_filter malformada não quebra mais o scan. Ambos os scanners.

Testes

  • +32 casos (1190 → 1222): sources-ats-providers, title-filter, arbeitsagentur-remote, trust-validator e um guard scan-result-cap reescrito («sem limite»).

[1.75.2] — 2026-06-19

docs: paridade documental completa para os agregadores do scanner da v1.75.0 em todos os 12 idiomas. Sem mudança de código — alinha a documentação voltada ao usuário com as sete fontes que chegaram na v1.75.0:

  • Guia de ajuda (12 idiomas). §5 ganha um bloco content_filter (gating por palavras-chave de descrição/trecho, irmão de location_filter) e uma nota sobre agregadores; §7 lista as sete novas fontes na varredura de um clique e na enumeração completa do menu suspenso Source; a contagem de adaptadores do §17 é corrigida do obsoleto “11 adapters” para “19 adapters — 14 English + 5 Russian”. Nenhum cabeçalho ##/### foi adicionado, então a estrutura fechada de 19 H2 / 75 H3 permanece inalterada.
  • README (9 idiomas completos). Nova bala “Aggregator boards (v1.75.0)” sob as fontes de varredura, mais o selo de versão atualizado para v1.75.2. (Os README abreviados pl/uk/ar não têm lista por fonte e ficam intencionalmente intocados ali.)
  • Documentação de referência. docs/portals-examples.md ganha uma seção “Aggregator boards” de copiar e colar com blocos de configuração provider: / <provider>: precisos para as sete; docs/PROJECT.md atualizado para 19 adapters; docs/sdd/CONVENTIONS.md documenta a distinção dos dois registros (sources/registry.mjs para o menu suspenso versus portals/registry.mjs para o fetching), a seleção de agregador baseada em provider: encadeada como opts.company, o sanitizador de escrita de varredura (scan-sanitize.mjs) e a contagem de testes da v1.75.1 (1190).
  • QA. Adicionado qa/QA-REGRESSION-PROMPT-v1.75.2-FULL.md — o driver de portão de lançamento de superfície completa, renovado para o ciclo de agregadores de varredura da v1.75.x.

[1.75.1] — 2026-06-19

fix(scan): polimento de robustez nas fontes baseadas em configuração da v1.75.0. Três pequenas correções de endurecimento vindas da revisão pós-lançamento (sem mudança de comportamento para uma varredura saudável):

  • Pausas de paginação cientes de abort. As pausas de cortesia entre páginas do Glints (300 ms) e do Jobstreet/SEEK (200 ms) agora se resolvem imediatamente quando o AbortSignal da varredura dispara, por meio de um novo helper delay(ms, signal) em server/lib/http-json.mjs, de modo que um cliente desconectado não consiga manter uma varredura paginada aberta por uma pausa extra.
  • Erro descritivo para respostas não JSON. O fetchJson agora envolve um corpo 2xx não JSON (p. ex. uma página HTML de manutenção servida com status 200) como non-JSON 2xx response from <url> em vez de expor um SyntaxError cru, de modo que o log de erros por fonte do scanner nomeie o endpoint que se comporta mal.
  • Normalização de escrita de varredura mais forte. O normalizeScanScalar agora colapsa a tabulação vertical, o avanço de página e os separadores Unicode de linha/parágrafo (\v \f U+2028 U+2029) além de \r \n \t — um superconjunto estrito, de modo que nenhum separador de registro/linha que uma planilha ou um visualizador possa respeitar sobreviva em scan-history.tsv.

[1.75.0] — 2026-06-19

feat(scan): porta a paridade com o career-ops pai v1.12.0 — sete novas fontes de vagas, filtragem de conteúdo e correções de segurança/qualidade. A web-ui executa seus próprios scanners em processo (não delega ao scan.mjs do pai), portanto as mudanças de provedor e de varredura da v1.12.0 do pai não se propagam automaticamente — esta versão reimplementa as aplicáveis conforme o contrato de adaptadores da web-ui.

  • Sete novas fontes de scanner. Três agregadores remotos de alcance global — RemoteOK, Remotive, Working Nomads — encaixam no padrão autodescoberto server/lib/sources/*.mjs (selecionados com provider: remoteok / remotive / workingnomads). Quatro agregadores regionais orientados por configuração — careers da IBM, Arbeitsagentur (Agência Federal de Emprego alemã), Glints (Sudeste Asiático), Jobstreet / SEEK — leem um bloco de configuração <provider>: por entrada; o en-scanner agora encaminha a entrada de empresa resolvida até cada fetcher para que possam lê-la. Todas as sete aparecem automaticamente no menu suspenso de fontes de #/scan.
  • content_filter (pai #974). Bloco opcional de portals.yml (listas de palavras-chave positive / negative) que filtra uma vaga pelo texto de sua descrição/trecho — espelha a semântica de location_filter; vagas sem descrição sempre passam. Integrado em ambos os scanners EN e RU.
  • Endurecimento da escrita de varredura (pai #1098). Os metadados de feeds externos agora são sanitizados antes de aterrissar em data/scan-history.tsv e data/pipeline.md: caracteres de controle são colapsados (uma quebra de linha em empresa/título não pode mais injetar uma linha TSV) e um = + - @ inicial é neutralizado contra injeção de fórmulas de planilha.
  • secondaryLocations do Ashby (pai #1073). A fonte Ashby agora dobra o rótulo de região de cada localização secundária mais addressLocality / addressCountry postal na string de localização (deduplicada), de modo que uma vaga elegível para a UE cujo rótulo principal diga, p. ex., “Canada” apareça para o location_filter.
  • Validação da forma do relatório de avaliação (pai #819). Os provedores em processo de /api/evaluate (Anthropic / OpenAI / Qwen / OpenRouter / GitHub Models) agora marcam um relatório A–G / SCORE_SUMMARY malformado como um array warnings não fatal; o caminho de avaliação do Gemini já herda a proteção do gemini-eval.mjs do pai.
  • docs: Antigravity CLI adicionado às listas de assistentes suportados nos 12 READMEs (mapeia para o provedor Gemini).

Herdado de graça do git pull do pai (a web-ui delega a estes): fallback de fontes CJK para PDF em japonês (#1053), fontes PDF compatíveis com ATS (#1074), proteção CJK para LaTeX (#1054), correções de tracker/merge/followup/dashboard e os modos chineses modes/zh (a web-ui lista os modos dinamicamente).


[1.74.3] — 2026-06-18

docs(parent-source): aponta o repositório pai career-ops para o fork Fighter90/career-ops. A web-ui agora referencia o fork do mantenedor como projeto pai em todos os pontos onde é uma fonte real: o padrão CAREER_OPS_REPO do instalador bin/setup.sh, cada link de git clone / “em cima de” / onboarding nos 12 READMEs, e a documentação dos agentes (CLAUDE.md, AGENTS.md, GEMINI.md, .github/copilot-instructions.md, docs/). O crédito ao autor santifer (e o aviso de UI não oficial) permanece inalterado — apenas as URLs de origem/clonagem mudaram. tests/sh-files.test.mjs agora verifica que o instalador clona o fork.


[1.74.2] — 2026-06-17

fix(health): expor GITHUB_MODELS_API_KEY como uma verificação opcional em #/health e em /api/status/providers. O provedor GitHub Models da v1.74.0 era configurável em #/config, mas não tinha linha na página de Saúde e estava ausente da superfície de provedores keysConfigured. Foi adicionada a verificação opcional (com a mesma redação “set / unset (manual mode)” dos outros cinco provedores de avaliação ao vivo) e github (+ seu GITHUB_MODELS_MODEL) a /api/status/providers, de modo que o roteamento de provedor ativo e a página de Saúde agora refletem os seis. O teste de linha de saúde de tests/api.test.mjs foi estendido para os seis provedores.


[1.74.1] — 2026-06-17

docs + test: seção “Instalar um assistente de IA” do README; cobertura completa de ramificações para o conector Gemini. Foi adicionada uma tabela de instalação/login ao README — links de instalação para Claude Code / Gemini CLI / Codex / Qwen Code / OpenCode / GitHub Copilot CLI + o mapeamento de provedor #/config de cada um + “faça login antes de continuar” (espelha o Início Rápido do career-ops.org/docs; esclarece que a web-ui é a alternativa independente, sem necessidade de CLI). O novo tests/gemini-connector.test.mjs (8 casos) cobre cada ramificação de runGemini — sem chave, sucesso, erro de API, conclusão vazia/bloqueada, corpo malformado, timeout, erro de rede, hasGeminiKey — levando server/lib/gemini.mjs a 100% de instruções. Cobertura geral: 96% linhas / 88% ramificações / 96% funções. Suite 1126 → 1134.


[1.74.0] — 2026-06-17

feat(llm): GitHub Models (Copilot) como o 6.º provedor + alinhamento canônico de 6 assistentes. career-ops.org/docs lista seis assistentes de codificação com IA — Claude Code, Gemini CLI, Codex, Qwen Code, OpenCode, GitHub Copilot CLI. A web-ui agora oferece suporte a todos os seis: cinco mapeiam para provedores ativos existentes (Anthropic / Gemini / OpenAI / Qwen / OpenRouter), e o GitHub Copilot CLI ganha um conector dedicado ao GitHub Models — runGitHubModels (OpenAI-compatible; um PAT do GitHub com o escopo models), configurável em #/config (GITHUB_MODELS_API_KEY + GITHUB_MODELS_MODEL) e selecionável via LLM_PROVIDER=github; 6.º na ordem auto. Os pacotes de ajuda e os READMEs agora listam os seis canônicos (Qwen CLI foi renomeado para Qwen Code; Gemini CLI + GitHub Copilot CLI foram adicionados), e o README inclui uma tabela completa de referência de modos e links de adaptadores de portais para career-ops.org/docs, de modo que cada funcionalidade possa ser rastreada até o projeto pai. tests/llm-provider-context.test.mjs estende a matriz de limite de busca para todos os seis provedores (cv.md + profile.yml embutidos + artefato retornado); novas chaves GITHUB_MODELS_* adicionadas a todos os 12 dicionários de idiomas. Suite 1125 → 1126.


[1.73.0] — 2026-06-17

feat(llm): conector Gemini genérico + contexto de CV/perfil verificado em todos os provedores. Adicionado server/lib/gemini.mjs (runGemini) — um cliente Gemini generateContent sem dependências externas que retorna o mesmo formato {markdown, usage, error} que os clientes compatíveis com Anthropic / OpenAI. Correção: /api/mode/:slug e /api/deep anteriormente roteavam seus prompts pelo gemini-eval.mjs, voltado exclusivamente para ofertas, fazendo com que o Gemini Run live retornasse uma avaliação em vez do artefato solicitado (carta de apresentação, contato, resumo). Agora chamam runGemini com bundleProjectContext, de modo que cv.md + config/profile.yml são embutidos inline para o Gemini exatamente como em qualquer outro provedor — cartas e resumos ficam detalhados e personalizados. O novo tests/llm-provider-context.test.mjs simula o limite HTTP de cada provedor e verifica que os cinco (Anthropic / Gemini / OpenAI / Qwen / OpenRouter) embutem cv.md + profile.yml inline e retornam o artefato (matriz mode + deep + evaluate, 9 casos). /api/evaluate mantém seu gemini-eval.mjs ajustado para ofertas. Suite 1116 → 1125.


[1.72.0] — 2026-06-17

feat(modes): Run live agora retorna o artefato final diretamente (contrato de saída de disparo único). Os templates pai modes/<slug>.md foram escritos para sessões interativas do Claude Code — vários (cover, contacto, …) pausam para fazer perguntas de esclarecimento antes de produzir o resultado, o que fazia o Run live da interface web emitir um questionário em vez do artefato. buildModePrompt agora envolve cada modo em um contrato de saída não interativo: realiza a análise (detalhamento da vaga, notas sobre a empresa, palavras-chave ATS, lacunas perfil↔vaga, escolhas de tom/ângulo) silenciosamente, escolhe padrões sensatos de cv.md / config/profile.yml para tudo o que o template normalmente perguntaria, e gera apenas o artefato final — encerrado com um lembrete por modo «output ONLY {the cover letter / outreach message / …}». Assim, clicar em Run live em #/cover agora retorna a própria carta de apresentação; a mesma correção se aplica a todos os modos genéricos (cover, contacto, interview-prep, project, training, followup, patterns) em todos os 12 idiomas (o artefato é redigido no idioma da interface via diretiva de localidade). Suite 1103 → 1116.


[1.71.2] — 2026-06-17

docs(i18n): publica a revisão de consistência da documentação. O bloco “Translations of this guide” de cada README agora lista todos os 11 idiomas irmãos (anteriormente alguns omitiam English/Français ou se vinculavam a si mesmos), com a linha em branco antes da quebra de seção restaurada. O prompt completo de regressão QA é renomeado para a versão atual, e a documentação (CLAUDE.md, CONVENTIONS, LOCALIZATION, PROJECT-CONTEXT) é sincronizada com a versão atual e a contagem de testes (1103). Sem alterações no código ou comportamento — somente documentação, portanto as traduções de ajuda/UI e todos os recursos de 1.70.0–1.71.1 permanecem inalterados.


[1.71.1] — 2026-06-17

fix(i18n): o guia de ajuda integrado agora está completamente traduzido em todos os 12 idiomas. Adicionados docs/help/{pl,uk,ar}.md (cada um contendo a estrutura validada de 19 H2 / 75 H3) para que #/help sirva um pacote nativo em polonês, ucraniano e árabe em vez de recorrer ao inglês — GET /api/help/{pl,uk,ar} agora retornam sua própria localidade. Integrado em todas as verificações de ajuda (help-ui, help.test, help-ru-config-section, canonical-docs-coverage). Também foram concluídas todas as listas de tradução nos 12 idiomas: o bloco «Translations of this guide» do README (9 READMEs), os cabeçalhos «Translations:» dos CHANGELOGs localizados (8 arquivos) e os contadores de documentação desatualizados foram atualizados. Suite 1100 → 1103.


[1.71.0] — 2026-06-16

feat(cover): gere um PDF de carta de apresentação diretamente em #/cover. O modo cover (adicionado na v1.70.0) produz o texto da carta; o resultado agora oferece um botão Generate PDF que o renderiza por meio do pipeline compartilhado de markdown→PDF inline (POST /api/stream/pdf/inlinegenerate-pdf.mjs), o mesmo caminho que o interview-prep usa. Agora você pode produzir a carta e gerar um PDF sem sair do SPA.

test/docs: endurecimento da revisão da v1.70.0. Adicionada cobertura CI-isolada para o modo cover (lista de permissões + montagem de prompt), o seletor <select> de bandeiras + RTL árabe (dirFor/<html dir>), top.langLabel em cada locale, o cabeamento do PDF de carta de apresentação, e a diretiva de locale de prompts.mjs + scaffolding para fr/pl/uk/ar. Referências obsoletas «todos os 8» → 12 locales atualizadas em docs/sdd/CONVENTIONS.md e no prompt de regressão QA do projeto completo.


[1.70.0] — 2026-06-16

feat(i18n): três novos idiomas de interface — polonês (pl), ucraniano (uk) e árabe (ar, com suporte completo a RTL) — elevando a SPA para 12 idiomas, correspondendo a todos os idiomas do README do projeto pai career-ops. Cada novo idioma inclui um dicionário completo de 697 chaves (public/js/lib/locales/i18n-dict.{pl,uk,ar}.js), validado pelas suites existentes de paridade / cobertura / sem-vazamento-latino / sem-dados-pessoais. O árabe adiciona suporte genuíno da direita para a esquerda: i18n.js define <html dir="rtl"> para idiomas RTL e um bloco [dir="rtl"] com escopo em app.css espelha o chrome (barra lateral, gaveta de notificações, tabelas e citações markdown, espaçamento inline) — os idiomas LTR permanecem byte a byte inalterados. Nova chave top.langLabel (×12) nomeia o seletor para leitores de tela.

feat(ui): o seletor de idioma <select> com ícone de bandeira substitui a fileira de botões que quebrava linha. Com 12 idiomas, a antiga fileira .lang-btn quebrava para três linhas na barra lateral; um <select> nativo (cada opção prefixada com um emoji de bandeira) escala de forma limpa, é compatível com teclado e leitor de tela por padrão, e permanece seguro com CSP (manipulador de mudança via addEventListener, sem JS inline). As bandeiras degradam para letras de região onde a plataforma não possui glifos de bandeira, portanto o rótulo do idioma é sempre o identificador principal.

feat(cover): porta o modo de carta de apresentação do projeto pai (career-ops v1.10.0 + saudação de v1.11.0) para a SPA. Nova página #/cover no grupo de navegação de Candidatura, construída sobre o executor de modos genérico: descrição da vaga + empresa/cargo + uma saudação opcional → uma carta personalizada gerada a partir de cv.md / modes/_profile.md. Adicionado cover à MODE_ALLOWLIST do servidor e um bloco i18n cover.* (×12 idiomas).

chore(compat): rastreamento do projeto pai career-ops v1.11.0. Verificado que o contrato de leitura/escrita está intacto — data/applications.md continua sendo a fonte da verdade em markdown (o índice de rastreamento SQLite do v1.11.0 é um cache derivado), as colunas do rastreador ainda são mapeadas por cabeçalho. parentVersion agora reporta 1.11.0.

fix(i18n): fecha uma lacuna latente onde o francês (adicionado na v1.61.0) estava ausente de server/lib/prompts.mjs em LOCALE_NAMES e SCAFFOLD_STRINGS — chamadas LLM em francês silenciosamente usavam saída em inglês e scaffolding em inglês. fr/pl/uk/ar estão agora todos conectados ao caminho de locale do prompt.

Acompanhamentos conhecidos: o guia de ajuda integrado (docs/help/) faz fallback para inglês em pl/uk/ar (o chrome da interface em si está totalmente localizado); o onboarding interativo de entrevista do projeto pai, a descoberta reversa de ATS e os novos provedores de scan ainda não estão disponíveis na SPA.


[1.69.2] — 2026-06-12

fix(test): corrige um vazamento de isolamento de testes que deixava npm test sobrescrever seus config/profile.yml e data/scan-history.tsv reais. tests/critical-fixes.test.mjs importava prompts.mjs (→ paths.mjs) no topo do arquivo, então PROJECT_ROOT resolvia para o diretório pai real antes de before() definir CAREER_OPS_ROOT como um diretório temporário — e PUT /api/profile vazava a fixture “Acceptance Test” para o seu perfil real a cada execução. Correção: carregar prompts.mjs via import() dinâmico dentro de before(). Novo tests/test-root-isolation.test.mjs (2 casos) protege toda a suíte contra esse padrão. Sem mudança de código de produção. Suíte 1084 → 1086.


[1.69.1] — 2026-06-12

fix(scan): #/scan não trunca mais silenciosamente varreduras regionais grandes. O conjunto exibido por região estava limitado a 500 (uma varredura RU real de 1352 vagas correspondentes mostrava apenas 500; 852 ocultas — o sintoma “2000 escaneadas, ~600 exibidas”). Ambos os scanners agora usam uma constante compartilhada e substituível por ambiente MAX_STORED_RESULTS (padrão 2000, substituível via SCAN_MAX_RESULTS). Apenas exibição: as adições a pipeline.md / scan-history.tsv já usavam o conjunto sem corte. fix(health/ui): os cartões de verificação do #/health não transbordam mais. Um nome/valor longo colidia com o botão Fix → e o selo de status; a linha agora encolhe e quebra via .health-check-row. Novos testes scan-result-cap + health-card-overflow. Suíte 1079 → 1084.


[1.69.0] — 2026-06-12

feat(scan): autodescoberta de adaptadores do scanner (P-14) — basta colocar um .mjs em server/lib/sources/ para registrar uma nova fonte. Antes da v1.69, a lista de fontes em server/lib/sources/registry.mjs era um array estático mantido à mão: adicionar um adaptador exigia editar tanto <id>.mjs quanto registry.mjs. Fecha a metade pendente do item P-14 do roadmap (docs/ROADMAP.md). Agora cada *.mjs de server/lib/sources/ é carregado dinamicamente no boot do módulo; cada adaptador declara sua identidade através de um bloco autodescritivo export const meta = { value, label, region, configKey? }. Os 12 adaptadores incluídos (ashby / greenhouse / lever / rss / smartrecruiters / workable / workday + geekjob / getmatch / habr / hh / trudvsem) ganharam um meta; registry.mjs usa readdirSync + import() dinâmico resolvido via top-level await (padrão ESM Node 18+). A API pública (SOURCES, SOURCES_BY_REGION, RU_CONFIG_KEYS, getRegionalSources) não muda: todos os imports existentes continuam funcionando. A validação rejeita meta malformados e registra um console.warn por arquivo problemático. Novo tests/sources-registry-discovery.test.mjs com 14 casos. Suíte 1065 → 1079.


[1.68.2] — 2026-06-07

fix(bin): os verbos da CLI via npx / npm link estavam quebrados — o caminho do bin agora é resolvido através de links simbólicos. npm e npx expõem career-ops-ui como um link simbólico em node_modules/.bin/, onde o antigo dirname "${BASH_SOURCE[0]}" apontava para .bin em vez da raiz do pacote — então npx career-ops-ui init executava node node_modules/scripts/init.mjs e falhava com MODULE_NOT_FOUND (execuções locais após npm install não eram afetadas, o que escondia o bug). Agora bin/career-ops-ui.sh e bin/start.sh canonizam SCRIPT_DIR através da cadeia de links (laço readlink + cd -P), de modo que cada verbo funciona a partir do repo, via npm link e via npx. Adiciona um bloqueio de regressão em tests/sh-files.test.mjs que executa um verbo através de um link simbólico no estilo .bin. Suíte 1065/1065.


[1.68.1] — 2026-05-29

fix(scan): timeout de fetch por fonte 10s → 60s. O fail-fast de 10s da v1.67.1 também cortava quadros Ashby lentos mas vivos que só precisavam de mais tempo. Eleva o padrão para um minuto para que eles respondam. Trade-off: uma fonte realmente morta/travada agora ocupa um slot de concorrência pelos 60s inteiros (varredura de pior caso mais lenta), e os que travam cronicamente (Perplexity, Supabase, Resend, …) provavelmente ainda expiram — um ajuste por fonte / menor concorrência do Ashby resolveria de verdade. Override com SCAN_FETCH_TIMEOUT_MS. Suíte 1063/1063.


[1.68.0] — 2026-05-29

feat(scan): painel de filtros de resultados redesenhado — campos rotulados, botão Aplicar, opção Presencial e um filtro de salário que funciona. Cada filtro em #/scan agora é um campo rotulado (rótulo acima do controle, não um placeholder): Buscar · Tipo · Salário de · Salário até · Fonte · Escopo. Um botão Aplicar explícito (além de Limpar, e Enter em qualquer campo) reexecuta o filtro; uma dica na página explica como funciona. A faixa salarial agora filtra de verdade — ao definir um valor de/até, vagas cuja remuneração fica fora da faixa e vagas sem salário informado são descartadas (sobreposição de faixas; moeda ignorada). O filtro de Tipo ganha uma opção Presencial ao lado de Remoto / Híbrido / Realocação. Novas chaves i18n ×9; salaryInRange agora estrito; suíte 1063/1063.


[1.67.1] — 2026-05-29

fix(scan): timeout de fetch por fonte 30s → 10s (fail-fast). A elevação para 30s da v1.67.0 recuperou só ~metade dos quadros Ashby lentos; o resto (Perplexity, Supabase, Resend, DeepL, Ramp, …) trava independentemente do deadline, então um timeout maior só estagnava cada varredura esperando slots mortos. 10s falha rápido nos travamentos crônicos e mantém as varreduras responsivas. Override com SCAN_FETCH_TIMEOUT_MS. Suíte 1060/1060.


[1.67.0] — 2026-05-29

feat(scan): filtro de faixa salarial (de / até) em #/scan, e um timeout de fetch por fonte maior. A tabela de resultados ganha dois campos numéricos — salário de / até — ao lado dos filtros de texto e remoto. O salário em texto livre de cada linha (от 100 000 до 200 000 ₽, 120000-150000 USD, $120K–$150K, …) é parseado para uma faixa numérica e comparado com semântica de faixas sobrepostas; linhas sem salário publicado são mantidas, então o filtro estreita a lista em vez de esvaziá-la (a comparação ignora a moeda — sem conversão cambial). Também eleva o timeout de fetch por fonte de 15s → 30s (override: SCAN_FETCH_TIMEOUT_MS): os payloads includeCompensation do Ashby costumavam levar >15s sob concorrência ×8, então ~30 quadros Ashby expiravam a cada varredura. Novos window.Skills.parseSalaryRange/salaryInRange + i18n ×9; 13 novos testes; suíte 1060/1060.


[1.66.0] — 2026-05-28

feat(scan): as fontes RU agora percorrem TODAS as páginas, não só a primeira. hh.ru, Habr Career e Trudvsem paginavam apenas os primeiros ~50 resultados por consulta; agora seguem a paginação até o fim — &page=N para hh.ru/Habr, offset/meta.total para Trudvsem — deduplicando entre páginas e parando quando uma página não traz nada novo (ou no limite de segurança de 50 páginas). Uma consulta como “Backend разработчик” retorna agora o conjunto completo (ex.: hh.ru PHP 17 → 55+ em 3 páginas; Trudvsem retorna os 72). Cada página mantém o timeout + AbortSignal. 4 novos testes; suíte 1045/1045.


[1.65.0] — 2026-05-28

feat(scan): o hh.ru agora é coletado do seu site público em vez da API JSON — funciona de qualquer IP, sem proxy. api.hh.ru passou a retornar 403 forbidden a qualquer cliente programático independentemente de IP ou User-Agent (bloqueio anti-bot de borda). O site (hh.ru/search/vacancy) serve resultados completos a qualquer cliente tipo navegador, então o adaptador agora parseia esse HTML (como o Habr Career). Remove a variável HH_PROXY de 1.64.0 e a dependência undici — sem proxy, chave ou User-Agent. Testes reescritos para o parser HTML; suíte 1041/1041.


[1.64.0] — 2026-05-27

feat(scan): roteia a requisição ao hh.ru por um proxy russo via HH_PROXY. O hh.ru bloqueia sua API por IP, não por User-Agent — então HH_USER_AGENT sozinho nunca removia um 403 de um nó de saída fora da Rússia. Defina HH_PROXY com a URL de um proxy russo HTTP/HTTPS (ex.: http://user:pass@ru-host:port) e apenas a requisição ao hh.ru passa por ele; as demais fontes mantêm a conexão direta. Construído sobre o ProxyAgent do undici (nova dependência de runtime); o dispatcher é omitido quando HH_PROXY não está definido. 3 novos testes; suíte 1041/1041.


[1.63.2] — 2026-05-27

feat(scan): % de progresso ao vivo + detalhe por fonte no console do #/scan. A barra agora é determinada — os scanners emitem eventos de progresso (EN: por empresa; RU: por consulta) via SSE, e a barra enche com um rótulo “Scanning… NN%” (faixa animada só até o primeiro evento). A primeira falha de cada fonte (timeout / 403 / rede) é registrada em detalhe no console; repetições são suprimidas. 1 teste novo; suíte 1040/1040.


[1.63.1] — 2026-05-27

style(scan): barra de progresso de #/scan mais visível. O indicador agora tem um rótulo visível “Scanning…” e a barra passou para 8px (antes 4px finos), bem perceptível durante o scan. Sem mudança de comportamento.


[1.63.0] — 2026-05-27

feat(scan): timeout por requisição + barra de progresso em #/scan. As requisições de fontes não tinham prazo, então uma fonte travada (ex.: api.hh.ru de um IP bloqueado) podia travar todo o scan. O novo server/lib/fetch-timeout.mjs envolve o fetchImpl dos scanners (makeTimeoutFetch, padrão 15s, via SCAN_FETCH_TIMEOUT_MS); uma fonte expirada é registrada como erro não fatal e o scan continua. #/scan mostra uma barra de progresso durante o scan (scan.progress nas 9 localidades). 7 testes novos; suíte 1039/1039.


[1.62.3] — 2026-05-27

docs: instalação esclarecida (career-ops-ui roda dentro de career-ops/web-ui/) + solução de problemas do init, nas 9 localidades. Seção de instalação reescrita em Option 1 (um curl) / Option 2 (clonar a UI dentro de um projeto career-ops existente como web-ui) + verbos CLI + configuração do provedor + bloco Troubleshooting init. Nota de estrutura aninhada adicionada ao /help §1 Setup; resumo de toda a linha v1.62.* no README. Apenas documentação; sem mudança de código.


[1.62.2] — 2026-05-27

fix(help): o filtro de #/help agora é de texto completo (encontra subseções H3 como RSS). O filtro de busca/TOC da página de ajuda antes correspondia apenas a títulos de seção H2, então a documentação RSS da v1.62.x (um H3 sob §5 Portals & sources) não era encontrada. Agora o corpo de cada seção é indexado no filtro, então buscar p. ex. “RSS” mostra §5. Apenas no cliente; sem mudança de API.


[1.62.1] — 2026-05-27

feat(scan): RSS no filtro de fontes + correção de localização do RSS. O menu suspenso de filtro de fontes em #/scan agora inclui RSS (adicionado a server/lib/sources/registry.mjs + a lista de fallback do SPA), então resultados de portais RSS (LaraJobs, WeWorkRemotely, …) são filtráveis como qualquer fonte ATS. O adaptador RSS não mapeia mais a tag <category> do feed para location — essas tags faziam o location_filter descartar erroneamente vagas remotas; agora location fica vazio e os feeds passam pelo filtro de localização. Tooltips/rótulos do botão de scan e a string da lista de fontes atualizados nas 9 localidades (Workable / SmartRecruiters / Workday / RSS). Snapshot i18n e teste do endpoint de fontes (6 → 7 EN) atualizados.


[1.62.0] — 2026-05-27

feat(scan): adaptador RSS genérico para portais de vagas fora de ATS. Um novo adaptador rss (server/lib/portals/adapters/rss.mjs + server/lib/sources/rss.mjs) permite ao scanner extrair vagas de qualquer feed RSS — LaraJobs, WeWorkRemotely, RemoteOK, golangprojects e outros portais fora de Greenhouse/Ashby/Lever. Sem novas dependências: a análise do feed é baseada em regex com suporte a CDATA e entidades HTML (títulos/empresas sem tags, code points astrais decodificados com segurança). Ativado por empresa via provider: rss / rss: / feed_url: em portals.yml, sem interceptar empresas já associadas a ATS. ALL_ADAPTERS cresce 6 → 7. 29 novos testes; documentado nas 9 localidades do README.


[1.61.1] — 2026-05-22

fix(i18n): localiza o title + aria-label do alternador de tema nos 9 idiomas (MINOR-001). O botão de tema claro/escuro (#theme-toggle) tinha title="Toggle theme" e aria-label="Toggle theme" fixos em index.html — o tooltip e o texto para leitores de tela nunca eram traduzidos, em nenhum idioma. Nova chave top.themeToggle + um handler data-i18n-title em applyI18n() (espelho do fix de aria-label da busca v1.58.15) localizam ambos os atributos na inicialização e a cada troca de idioma. Travado por tests/playwright-theme-toggle-i18n.mjs (9 idiomas + troca em runtime) e dois guards estáticos. Único achado LOW do sign-off da v1.61.0. (MINOR-001)


[1.61.0] — 2026-05-22

feat(i18n): adiciona o francês como 9.º idioma da interface. Novo dicionário por idioma public/js/lib/locales/i18n-dict.fr.js (window.__I18N_DICT_FR), com paridade completa de 668 chaves com o inglês; novo pacote de ajuda docs/help/fr.md (19 H2 / 73 H3, paridade estrutural exata com en). fr fica registrado no seletor de idioma e na autodetecção do navegador (i18n.js), no montador (i18n-dict.js), no index.html (tag <script> antes do montador), no snapshot de teste e em todas as listas de locales dos testes. A tabela de tradução inicial veio do PR #9 (contribuição da comunidade). Sem mudança de lógica: t() e todas as views permanecem inalterados. 1001 / 1001 testes unitários; o sweep de locales do Playwright cresce para 9 subtests. (FR-LOCALE)


[1.60.0] — 2026-05-22

refactor(i18n): divide o megaarquivo de 8 colunas em arquivos por idioma (I18N-SPLIT). O dicionário de traduções ficava em um único public/js/lib/i18n-dict.js; agora há um arquivo por idioma em public/js/lib/locales/ mais i18n-dict.aliases.js, para que um tradutor edite um único idioma isoladamente (padrão i18next / OpenWA). i18n-dict.js passou a ser um montador que reconstrói exatamente o mesmo window.__I18N_DICT, então t() e todas as views não mudam. Carregado de forma síncrona via <script src> — sem build, sem fetch. Um snapshot prova que a migração é sem perdas (678 chaves). Ferramentas e ~25 testes adaptados; novos tests/i18n-locale-files.test.mjs e tests/playwright-locale-sweep.mjs (cada página × 8 idiomas no Chromium real). 994 → 1000 unitários · 62 → 70 Playwright. Sem mudança de comportamento. (I18N-SPLIT)


[1.59.13] — 2026-05-21

fix(i18n): colapsar chaves duplicadas reais com @alias + limpeza final de dados pessoais. Nome real removido de fixtures e relatórios QA (→ Jane Doe); LICENSE/package.json → handle Fighter90. Mecanismo @alias colapsa as 10 chaves idênticas nos 8 locales; nav.config/config.title NÃO são fundidas (divergem em espanhol). 991 → 994 testes. (I18N-CL3)


[1.59.12] — 2026-05-21

fix(i18n): limpeza de i18n-dict.js — pré-fr (I18N-CL1, I18N-CL2, I18N-CL4). Removido dado pessoal em training.coursePh (→ placeholder genérico), followup.lastPh virou dica de formato (não data fixa), adicionado npm run audit:i18n. Grupos de valores duplicados são intencionais (papéis de UI distintos). (I18N-CL1, I18N-CL2, I18N-CL4)


[1.59.11] — 2026-05-21

fix(test): v1.59.11 — a suite e2e-comprehensive agora passa 23/23 (era 11/23). Causa raiz: page.goto(baseUrl + '/#/X') é um no-op para mudanças só de hash no Playwright. Novo helper goRoute(hash) faz bounce por about:blank antes de cada goto. (e2e-harness-r1)


[1.59.10] — 2026-05-21

fix(api): NEW-F1-sub-r1 (v1.59.10) — middleware de .. cru movido para cima de todas as rotas /api. O de v1.59.8 ficava depois de app.all e nunca disparava. Agora roda antes da normalização do 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 comportamental do scroll-spy do Help TOC. Sexto ciclo. v1.59.9 adiciona marcador, paint inicial síncrono, re-compute com duplo rAF, listener de resize, e limpeza completa em 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). Exceção de doutrina autorizada pelo relatório FINAL-REGRESSION-v1.59.7. UX-A5-r3: #/help troca IntersectionObserver por listener scroll com rAF throttling. NEW-F1-sub: middleware rejeita .. cru em /api/* com 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 envia Cache-Control: no-store. O CV é o artefato principal do usuário; sempre revalidar. (NEW-D3-cache)


[1.59.6] — 2026-05-20

feat(a11y): NEW-D2-motion (v1.59.6) — respeito a prefers-reduced-motion: reduce. Novo bloco @media neutraliza animações, transições e scroll-behavior. (NEW-D2-motion)


[1.59.5] — 2026-05-20

fix(api): NEW-F1 (v1.59.5) — /api/* desconhecido retorna 404 JSON em todos os verbos. app.getapp.all. (NEW-F1)


[1.59.4] — 2026-05-20

fix(ui): NEW-OR1 (v1.59.4) — chip Active/Keys em #/config agora livre de races. Constrói nós antes do swap, token in-flight, cache do último estado bom. (NEW-OR1)


[1.59.3] — 2026-05-20

fix(ux): UX-A5-r2 (v1.59.3) — scroll-spy em #/help reforçado. rootMargin alargado de 10 % para 25 % + estado inicial calculado no mount. (UX-A5-r2)


[1.59.2] — 2026-05-20

fix(ui): v1.59.2 — chip Active/Keys: contagem correta, nome do provedor capitalizado, sem sobreposição. (post-v1.59.1 hotfix)


[1.59.1] — 2026-05-20

fix(test): v1.59.1 — guard NEW-D1 aceita o copy ES polido pelo UX-A11. Regex relaxada. (v1.59.1)


[1.59.0] — 2026-05-20

feat(ui): UX-A14 (v1.59.0) — Passe de auditoria mobile (≤ 420 px). Cinco correções num novo bloco @media (max-width: 420px). (UX-A14)


[1.58.65] — 2026-05-20

test(ui): UX-A2 (v1.58.65) — teste de bloqueio do field-form estruturado de Modes. Novo teste que protege a implementação v1.54.3 contra regressões. (UX-A2)


[1.58.64] — 2026-05-20

fix(i18n): UX-A11 (v1.58.64) — polimento do copy pt-BR. eval.subtitle agora usa aderência do CV, Pontuação, cabeçalho, relatório. (UX-A11)


[1.58.63] — 2026-05-20

fix(ui): UX-A15 (v1.58.63) — tile Pipeline do Dashboard com peso visual primário. O tile Pipeline agora se destaca com borda de destaque, ícone maior e label em negrito. (UX-A15)


[1.58.62] — 2026-05-20

feat(ui): UX-A9 (v1.58.62) — chip sticky de resumo na aba API keys. #/config → API keys agora exibe no topo um chip sticky com o provedor ativo e a contagem de chaves configuradas. (UX-A9)


[1.58.61] — 2026-05-20

docs(readme): UX-A8 (v1.58.61) — seção de limpeza na primeira execução adicionada nos 8 READMEs. Agora documentamos o passo make clean-test-fixtures para limpar as duas URLs fixture QA antes do primeiro scan. (UX-A8)


[1.58.60] — 2026-05-20

feat(ui): UX-A12 (v1.58.60) — Drawer de notificações com Limpar tudo + dispensar por entrada. Novo botão global e × por entrada no painel de notificações. (UX-A12)


[1.58.59] — 2026-05-20

feat(ui): UX-A13 (v1.58.59) — CTA acionável «Fix →» em linhas de saúde com falha. Linhas com FAIL/OPTIONAL agora exibem um botão ghost que leva diretamente à aba de configuração correspondente. (UX-A13)


[1.58.58] — 2026-05-20

fix(ux): UX-A10 (v1.58.58) — proteção contra perder edição não salva em #/cv. Agora beforeunload (fechamento do navegador) e hashchange (navegação SPA) exibem confirmação localizada antes de sair com buffer sujo. (UX-A10)


[1.58.57] — 2026-05-20

test(ui): UX-A7 (v1.58.57) — bloqueio de regressão sobre o contrato de auto-refresh do cost-line. Novo teste estático garante que o evento providers-changed é despachado, assinado, e que todas as views de advisor chamam UI.providerCostHint. (UX-A7)


[1.58.56] — 2026-05-20

fix(a11y): UX-A4 (v1.58.56) — .lang-btn atinge tamanho mínimo de alvo táctil WCAG 2.5.8. Antes os botões de idioma mediam 23–25 px de altura, abaixo do piso de 24×24 px. Agora min-height: 28px + min-width: 28px garantem conformidade WCAG 2.2 AA. (UX-A4)


[1.58.55] — 2026-05-20

feat(ui): UX-A3 (v1.58.55) — chip de provedor ativo no Dashboard. O hero de #/dashboard agora mostra qual provedor LLM está ativo (⚡ Live evals: Anthropic claude-sonnet-4-6 ou 📋 Manual prompt mode). Atualiza automaticamente ao mudar LLM_PROVIDER em #/config e ao recuperar foco na aba. (UX-A3)


[1.58.54] — 2026-05-20

fix(ux): UX-A1 (v1.58.54) — aviso defensivo de estrutura no brief de Deep. Quando o brief salvo não tem ao menos 3 das 6 seções canônicas (Company snapshot / Engineering culture / Recent news / Glassdoor / Interview process / Negotiation leverage), public/js/views/deep.js insere um aviso não bloqueante com link para a referência. Mitigação na UI; a correção no prompt fica no projeto pai. (UX-A1)


[1.58.53] — 2026-05-20

fix(ux): UX-A6 — todo saved-card flui pelo helper único renderSavedCard(). Garante estrutura <span>+<time> em qualquer caminho de render. 948 → 949 unitários. (UX-A6)


[1.58.52] — 2026-05-20

fix(ux): UX-A5 — scroll-spy do TOC em #/help agora dispara corretamente. Regressão de v1.58.45 (setTimeout(0) era muito cedo). Fix: refs diretos a headings + double requestAnimationFrame. 947 → 948 unitários. (UX-A5)


[1.58.51] — 2026-05-20

chore(docs): v1.58.51 — limpeza final do ciclo v1.58.37 → v1.58.50 (14 releases). Sem mudanças de código. qa/ reorganizado (tudo versionado em archive/v158-cycle/); 6 perennials na raiz. REGRESSION-FINAL §13 documenta cada invariante v1.58.37→.50. Baseline sem mudanças (947/947). (housekeeping)


[1.58.50] — 2026-05-20

docs: DOC-1 — qa/REGRESSION-FINAL.md ganha §5a documentando que os corpos de erro do servidor são inglês-por-política. Encerra NEW-D4 como not-a-finding. Fecha a fila v1.58.37 → v1.58.50 de FIX-PROMPT-FINAL-EXHAUSTIVE.md (14 releases). 946 → 947 unitários. (DOC-1)


[1.58.49] — 2026-05-20

chore(tooling): TOOL-1 — make clean-test-fixtures + script para remover linhas example.com de data/pipeline.md do projeto pai. Suporta --dry-run. 4 testes CI-isolated. 942 → 946 unitários. (TOOL-1)


[1.58.48] — 2026-05-20

fix(ux/onboarding): UX-D-B — banner global em #/dashboard quando o perfil ainda usa o template padrão. Novo profileFixtureBanner() exibe .hero-banner--warning ao detectar Profile customized: false em /api/health. Novas chaves i18n onboarding.fixtureWarning + onboarding.fixProfile × 8. 941 → 942 unitários. (UX-D-B)


[1.58.47] — 2026-05-20

fix(ux/naming): UX-D-C — botão “Quick scan” da barra superior renomeado para Abrir Scan (apenas navega, não inicia scan). Atualizado em 8 idiomas. 940 → 941 unitários. (UX-D-C)


[1.58.46] — 2026-05-20

fix(ux): UX-D-D — checklist de #/apply substitui {company}-{role} por slugs derivados de URL/JD. Antes os placeholders apareciam literalmente. Novas funções extractSlugs + substitutePlaceholders reconhecem Greenhouse/Lever/Ashby/Workable/SmartRecruiters/Workday. Fallback [company]/[role]. 939 → 940 unitários. (UX-D-D)


[1.58.45] — 2026-05-20

fix(ux): UX-D-K — scroll-spy no TOC de #/help destaca a seção atual. IntersectionObserver aplica .toc-current ao link cujo H2 está na faixa de leitura. 938 → 939 unitários. (UX-D-K)


[1.58.44] — 2026-05-20

fix(ux): UX-D-L — brief aberto em Saved-research de #/deep ganha botão × inline para fechar. Antes não havia forma de fechar sem rolar ou navegar. Nova chave deep.closeBrief × 8. 937 → 938 unitários. (UX-D-L)


[1.58.43] — 2026-05-20

fix(ux): UX-D-F — submit vazio em #/evaluate exibe toast localizado distinto. Antes confundia vazio com “muito curto”. Nova chave eval.emptyJd × 8. 936 → 937 unitários. (UX-D-F)


[1.58.42] — 2026-05-20

fix(ux): UX-D-J — paridade do chip ETA em todas as páginas de advisor. Antes só #/auto. Agora também #/evaluate, #/deep e as 5 mode-pages exibem ⏱ ~30s (nova chave advisor.eta × 8). 935 → 936 unitários. (UX-D-J)


[1.58.41] — 2026-05-20

fix(ux/truthfulness): UX-D-I — cost-hint refetcha em foco de aba + evento providers-changed. Antes só carregava uma vez; agora re-fetch via visibilitychange + CustomEvent que #/config dispara ao salvar. 934 → 935 unitários. (UX-D-I)


[1.58.40] — 2026-05-20

fix(ux/docs): UX-D-H — regression-lock garantindo que toda URL career-ops.org/docs/... visível continue clicável. Novo tests/external-doc-links.test.mjs valida views/.js e docs/help/.md. 932 → 934 unitários. (UX-D-H)


[1.58.39] — 2026-05-20

fix(ux): NEW-D2 — botão Refresh no header do painel com feedback explícito. Distinto do Refresh do banner; refetch in-place sem reload. 2 novas chaves i18n. 931 → 932 unitários. (NEW-D2)


[1.58.38] — 2026-05-20

fix(a11y): NEW-D3 (WCAG 4.1.2) — input de busca do #/tracker ganha aria-label localizado distinto do placeholder. Antes apenas placeholder; SR não anunciava o propósito. Nova chave track.searchAria × 8 idiomas, distinta do placeholder. 930 → 931 unitários. (NEW-D3)


[1.58.37] — 2026-05-20

fix(i18n): NEW-D1 — H1 de #/pipeline localizado em es/pt-BR/ru + 2 fugas RU corrigidas. pipe.title em pt-BR agora Pipeline de vagas; novo tests/i18n-no-latin-leaks.test.mjs também pegou ru.contacto.title e ru.health.title. 928 → 930 unitários. (NEW-D1)


[1.58.36] — 2026-05-20

chore(docs): v1.58.36 — sweep completo de housekeeping no fechamento do ciclo v1.58.x. Sem mudanças de código. (1) qa/: 3 snapshots versionados (REGRESSION-END-TO-END-v1.58.16/33/35.md) movidos para qa/archive/v158-cycle/. (2) REGRESSION-FINAL.md ganha §12 com todos os invariantes v1.58.4 → v1.58.35. (3) UX-AUDIT-PROMPT.md estendido com 30 linhas. (4) docs/architecture/ atualizado (FRONTEND drawer, TESTING totais 928/62/20/23). (5) CLAUDE.md ganha seção “Lições difíceis do ciclo v1.58.x”. (6) README ×8 com nova linha “Notificações 🔔” + contagem de testes corrigida. Baseline sem mudanças. (housekeeping)


[1.58.35] — 2026-05-20

fix(ui): v1.58.35 — drawer de notificações não abre mais sozinho + nova §18 “Notificações” na ajuda (reportado pelo usuário). Bug v1.58.34: .notif-drawer { display: flex } vencia o [hidden] { display: none } do UA. Fix com .notif-drawer[hidden] { display: none } explícito + drawer só abre ao clicar no sino. Nova §18 nas 8 traduções da ajuda. 927 → 928 unitários. (reportado pelo usuário)


[1.58.34] — 2026-05-20

feat(ui): v1.58.34 — Drawer de notificações (fecha U-13 completamente). Sobre a captura da v1.58.33: nova API UI.onToast(fn), sino 🔔 na top-bar com badge de não lidos, drawer à direita com título/vazio/itens localizados (notif.* × 8). Esc + fechar + clique no sino fecham. 926 → 927 unitários. (U-13 follow-up)


[1.58.33] — 2026-05-20

fix(ux): U-13 + U-14 + U-15 — diário de toasts (cap 50 + UI.getToastHistory()) + regra de segurança .page-header h1 + p + indicador de alterações não salvas em #/cv. Fecha o ciclo v1.58.x. Nova chave i18n cv.unsaved × 8 idiomas. 925 → 926 unitários. (U-13/U-14/U-15)


[1.58.32] — 2026-05-20

fix(ux): U-12 — input do filtro do TOC de ajuda ganha min-width: 16ch para os placeholders KO/JA não cortarem. Nova classe .help-toc__filter. 924 → 925 unitários. (U-12)


[1.58.31] — 2026-05-20

fix(ux): U-11 — o cabeçalho Legitimacy do Tracker agora exibe um chip ⓘ com tooltip localizado explicando a escala (Alta/Atenção/Suspeita). Nova chave i18n track.col.legitimacy.help × 8 idiomas. 923 → 924 unitários. (U-11)


[1.58.30] — 2026-05-20

fix(ux): U-10 — botões Normalize / Dedup / Merge do Tracker desabilitados quando data/applications.md está vazio. Tooltip localizado (track.fixEmpty × 8 idiomas) explica o porquê. 922 → 923 unitários. (U-10)


[1.58.29] — 2026-05-20

fix(ux): U-9 — a linha contador ↔ filtro de #/pipeline empilha verticalmente em viewports estreitas. Nova classe .pipeline-controls + @media (max-width: 720px) estica o filtro a 100% da largura. 921 → 922 unitários. (U-9)


[1.58.28] — 2026-05-20

fix(ux): U-8 — o bloco do prompt gerado fica colapsado por padrão nas 7 páginas de modo. Envolvido em <details class="prompt-block">; sumário mostra “Show prompt (N lines)” localizado (prompt.show / prompt.lines × 8). Copy + Run-live continuam visíveis. 920 → 921 unitários. (U-8)


[1.58.27] — 2026-05-20

fix(ux): U-7 — divisores ASCII === de verify-pipeline.mjs removidos do modal de resultado. Regex ^={10,}$ aplicada no handler antes de renderizar. 919 → 920 unitários. (U-7)


[1.58.26] — 2026-05-20

fix(ux): U-6 — o chip “Active companies N/M” em #/scan explica N vs M via tooltip + aria-label. Nova chave scan.activeCo.help × 8 idiomas. 918 → 919 unitários. (U-6)


[1.58.25] — 2026-05-20

fix(ux/ia): U-5 — Dashboard deduplica CTAs (botão Open Pipeline do header e tile Scan all sources removidos). Sidebar e hero já cobrem ambas as rotas; o 4× Pipeline / 4× Scan da QA v1.58.3 fica 2× cada. 917 → 918 unitários. (U-5)


[1.58.24] — 2026-05-20

fix(ux): U-4 — toasts de erro escondem o postfix “(MÉTODO /caminho · HTTP NNN)” em um <details> recolhido. O detalhe técnico segue no DOM (invariante BUG-006), mas o título fica limpo. Nova chave i18n toast.details × 8 idiomas. 916 → 917 unitários. (U-4)


[1.58.23] — 2026-05-20

fix(ux): U-3 — placeholder de lastContact em #/followup agora é calculado como hoje − 14 dias. Antes era o ISO congelado 2026-04-21 que envelhecia mal; agora é computado em render via new Date() + setDate(getDate() - 14). 915 → 916 unitários. (U-3)


[1.58.22] — 2026-05-20

fix(ux): U-2 — o H1 de #/auto não quebra mais em 2 linhas por causa do inicial. O emoji é movido para um <span class="page-icon" aria-hidden="true"> separado; .page-header--icon usa display: grid com coluna dedicada para o ícone. 914 → 915 unitários. (U-2)


[1.58.21] — 2026-05-20

fix(ux): U-1 — H1 + subtítulo do #/cv agora batem com as demais páginas (supersede UX-9 v1.56.0 by design). Chip .cv-breadcrumb removido; header usa <h1 class="page-title"> + <p class="page-subtitle">. Invariante de único <h1> preservado. 913 → 914 unitários. (U-1)


[1.58.20] — 2026-05-20

fix(i18n/platform): I-6 — o atalho do rodapé mostra ⌘K no Mac e Ctrl+K nos demais sistemas, com o verbo localizado. Antes era o literal CTRL+K — search em todas as plataformas/idiomas. top.langhint agora usa {hotkey} — buscar; applyFooterHotkey() substitui {hotkey} pela combinação nativa via navigator.platform. 915 → 916 unitários. (I-6)


[1.58.19] — 2026-05-20

fix(i18n): I-4 — #/followup em russo já não vaza cadence / follow-up. Strings RU do modo followup (H1, hints) continham cadence, follow-up, scope, timeline. Substituídas por equivalentes russos nativos. 914 → 915 unitários. (I-4)


[1.58.18] — 2026-05-20

fix(i18n): I-3 — itens 2/5/13/14 do TOC da ajuda sem vazamento de inglês em locales não latinos. Antes da correção, alguns bundles ainda traziam ## 2. App settings & API keys, ## 5. Portals & Sources, ## 13. Mode prompts, ## 14. Apply checklist. Agora totalmente localizados nos 8 idiomas. 913 → 914 unitários. (I-3)


[1.58.17] — 2026-05-20

fix(i18n): I-2 — datas do Saved-research agora usam Intl.RelativeTimeFormat por locale. O helper formatRelative() em public/js/views/deep.js retornava today / 1d ago / Nd ago em inglês em todos os idiomas. Substituído por Intl.RelativeTimeFormat(I18n.getLang(), { numeric: 'auto' }) — string nativa do browser para “hoje/ontem/há N dias”. Datas > 7 dias caem em Intl.DateTimeFormat(locale, { dateStyle: 'medium' }). 912 → 913 unitários. (I-2)


[1.58.16] — 2026-05-20

fix(ui): tremor de hover nos botões da marca (reportado pelo usuário). Causa: o fundo padrão de .btn-primary / .btn-danger era um linear-gradient(...) e o :hover o trocava por uma cor sólida. CSS não interpola gradiente↔sólido, então a transição de 180 ms estalava e o usuário via um flash branco/rosa. Correção em public/css/app.css: manter o gradiente no hover e escurecer com filter: brightness(0.92)filter interpola limpamente em qualquer browser. A lista de transition de .btn recebe filter var(--transition) para animar o escurecimento. 911 → 912 unitários. (reportado pelo usuário)


[1.58.15] — 2026-05-20

fix(a11y/i18n): I-1 — aria-label e <label> visualmente oculto da busca da barra superior agora localizados. Antes, leitores de tela em qualquer idioma ouviam o aria-label em inglês. Novo hook genérico data-i18n-aria-label em public/js/app.jsapplyI18n() atualiza aria-label a cada troca de idioma, simétrico ao tratamento de data-i18n / data-i18n-placeholder. Duas novas chaves i18n (top.search.aria, top.search.label) nos 8 idiomas. O hook é reutilizável por qualquer controle futuro. 910 → 911 unitários. (I-1)


[1.58.14] — 2026-05-20

fix(ux): M-9 — botão Atualizar do banner de conexão agora dá feedback (antes era reload silencioso). Até v1.58.13 o handler chamava location.reload() direto. Agora exibe o toast Atualizando…, marca sessionStorage['refreshedToast'], desativa o botão para evitar dupla clique, e atrasa o reload em 200 ms para o toast aparecer. No próximo boot, app.js detecta a marca e emite o toast de sucesso Atualizado. 2 novas chaves i18n (common.refreshing, common.refreshed) nos 8 idiomas. 909 → 910 unitários. (M-9)


[1.58.13] — 2026-05-20

fix(ux): M-8 — o checklist de #/apply agora é interativo. Antes de v1.58.13, “▶ Gerar checklist” mostrava os itens 0…7 como bloco monoespaçado <pre> — apenas leitura, sem possibilidade de marcar. Agora cada item é um <input type="checkbox"> real, envolvido em <label> (área de clique ≥44 px, WCAG 2.5.5). O estado é persistido por URL em localStorage['applyChecklist:'+slug] — marca 3 itens, recarrega, os 3 continuam marcados. Botões: Copiar não marcados (copia itens em aberto como - markdown) e Redefinir. 5 novas chaves i18n (apply.checklist.copyUnchecked, resetBtn, copied, copyFailed, reset) em todos os 8 idiomas. Fallback defensivo caso o parser não encontre itens. 908 → 909 unitários. (M-8)


[1.58.12] — 2026-05-20

fix(ux): M-7 — a linha de custo agora segue o provedor ativo (OpenRouter não cai mais em um número fabricado). UI.providerCostHint() já consultava /api/status/providers, mas os mapas em public/js/api.js listavam apenas anthropic/gemini/openai/qwen. Após v1.57.0 com OpenRouter como 5º provedor, ele caía no fallback genérico de 0,03 e mostrava o literal openrouter em minúsculas. Agora EST inclui openrouter: null (o router escolhe o modelo — custo variável), e o ramo === null emite cost varies (router picks) localizado em vez do falso ~$0.03/eval. NAME adiciona openrouter: 'OpenRouter'. Nova chave i18n cost.varies nos 8 idiomas. 907 → 908 unitários. (M-7)


[1.58.11] — 2026-05-20

fix(ux): M-4 — espaçamento entre título e data no card de pesquisa salva agora é CSS estrutural (antes margem inline). A regressão MASTER de v1.58.3 confirmou que alguns cards mostravam software-engineer-generaltoday (sem espaço entre título e data) enquanto outros estavam OK — o código anterior dependia de style="margin-left: 8px" entre dois <span> soltos, que colapsava em certas entradas. Correção em public/js/views/deep.js — substitui os dois <span> por .saved-card__title + um <time class="saved-card__date" datetime="…"> semântico, envolvidos por um container flex .saved-card. O espaçamento agora é controlado por gap: var(--space-2, 8px), não colapsa (e ganha semântica a11y/SEO com <time>). 906 → 907 unitários. (M-4)


[1.58.10] — 2026-05-20

fix(ux): M-2 — descartar o toast de progresso antes de abrir qualquer modal de resultado. Clicar em sync-check em #/cv deixava o toast “Running cv-sync-check.mjs…” no canto inferior direito enquanto o modal de resultado abria — ambos disputando atenção e em telas estreitas sobrepostos. Os botões Doctor / verify-pipeline da página Health já chamavam UI.dismissToast() antes de UI.modal(); o sync-check de cv.js era o único ponto que omitia. Correção em public/js/api.jsUI.modal() agora chama dismissToast() como primeira instrução executável (defesa em profundidade no limite). As strings hardcoded em inglês de cv.js foram localizadas via t('cv.syncCheckRunning') / t('cv.syncCheck') (invariante BUG-008: título do modal == rótulo localizado do botão). Duas novas chaves i18n adicionadas nos 8 idiomas. 905 → 906 unitários. (M-2)


[1.58.9] — 2026-05-20

fix(a11y): M-1 — restabelecer um anel :focus-visible visível nos campos de formulário (WCAG 2.4.7 Nível AA). A regressão MASTER de v1.58.3 confirmou que getComputedStyle(focusedInput) retornava outline: rgb(255,255,255) none 1.5px — a palavra-chave none colapsava o anel para 0 px em cada campo. Causa raiz: as regras base .input, .textarea, .select { outline: none } e .searchbar input { outline: none } tinham maior especificidade do que *:focus-visible global e silenciosamente eliminavam o anel de foco do teclado em 88 elementos por página. Correção em public/css/app.css — regras explícitas .input:focus-visible/.textarea:focus-visible/.select:focus-visible e .searchbar input:focus-visible com outline: 2px solid var(--rausch) + sombra translúcida; o foco de mouse (:focus) permanece limpo. 904 → 905 unitários (guarda estática); Playwright 60 → 61 (Tab-traversal). (M-1)


[1.58.8] — 2026-05-20

feat(health): exibir OPENAI_API_KEY / QWEN_API_KEY / OPENROUTER_API_KEY em #/health (análogo a GEMINI_API_KEY). v1.57.0 adicionou OpenRouter como 5º provedor live-eval; v1.55.3 (UX-2) trouxe o onboarding de 4 provedores. A página #/health mostrava apenas GEMINI_API_KEY e ANTHROPIC_API_KEY — os outros três ficavam invisíveis embora /api/status/providers já os roteasse. Pedido do usuário: estender o padrão “set / unset (manual mode)” a todos os provedores headless. server/lib/routes/health.mjs agora adiciona três linhas opcionais conectadas ao mesmo gate isUsableKey. A view Health itera sobre body.checks, então não foi preciso alterar strings nos 8 idiomas. 903 → 904 unitários. (Pedido do usuário)


[1.58.7] — 2026-05-20

fix(security): NEW-2 — isValidJobUrl agora rejeita sintaxes pareadas de placeholders de template (${…}, {{…}}) para coincidir com a mensagem de erro. O 400 do POST /api/pipeline anuncia “contain no script or template characters”, mas a regressão MASTER de v1.58.3 confirmou que apenas <%…%> estilo ASP/EJS era de fato bloqueado (como efeito colateral do filtro [<>"'\\s]). Template literals JS (${TEST}) e Mustache/Handlebars ({{TEST}}) passavam — um descompasso regex↔mensagem. Opção A do fix-prompt (apertar a regex): novo TEMPLATE_PATTERNSem [server/lib/security.mjs](https://github.com/Fighter90/career-ops-ui/blob/main/server/lib/security.mjs) consultado viahasTemplatePlaceholder(url)antes denew URL(…). Apenas placeholders **pareados** são rejeitados ({normal}` ATS-style continua aceito). 901 → 903 unitários. (NEW-2)


[1.58.6] — 2026-05-20

fix(a11y/i18n): BUG-008-tb — o título do modal do botão Doctor na barra superior agora coincide com o rótulo localizado. A regra BUG-008 (fechada em v1.58.0) exige “título do modal == rótulo localizado do botão”. A regressão MASTER de v1.58.3 detectou que o ponto de entrada da barra superior ainda violava a invariante: clicar em Doctor abria um modal com título doctor (inglês minúsculo), independentemente do idioma. Correção em public/js/app.js:118 — substituir o literal 'doctor' por I18n.t('top.doctor', 'Doctor'). A chave top.doctor já existe nos 8 idiomas (EN Doctor · ES/pt-BR Diagnóstico · KO 진단 · JA 診断 · RU Диагностика · zh-CN 诊断 · zh-TW 診斷) e é a mesma que o botão declara via data-i18n="top.doctor". Guarda estática adicionada em tests/qa-report-fixes.test.mjs. 900 → 901 unitários; Playwright 60/60. (BUG-008-tb)


[1.58.5] — 2026-05-20

fix(ui): NEW-3 — duplo-POST do Run-live em #/followup triado como não reproduzível; bloqueado com guarda Playwright. A regressão MASTER de v1.58.3 observou (via window.fetch com monkey-patch) dois POSTs idênticos para /api/mode/followup em ~2 s após um único clique no Run live em #/followup (empresa/cargo/notas preenchidos, data deixada em branco). Seguindo a doutrina “reproduzir primeiro” do fix-prompt, a inspeção de public/js/views/mode-page.js::submit() mostra: (a) Run live e Generate prompt são <button> simples com apenas um onClick cada — não há <form> pai nem addEventListener('submit') que possa disparar duas vezes; (b) UI.withSpinner() (FIX-L1) define button.disabled = true durante a requisição, bloqueando um segundo clique físico na origem. Um novo teste Playwright em tests/playwright-smoke.mjs segue a receita exata da regressão — preenche empresa/cargo/notas, deixa a data em branco, clica no botão manual (que compartilha o submit() com o Run live) e verifica exatamente um POST /api/mode/followup em uma janela de 3 s. Seletor estável entre locais (o glifo é idêntico nos 8 idiomas) e addInitScript semeia career-ops-ui:lang=en para que um teste anterior de idioma no mesmo contexto do navegador não perturbe os seletores. Playwright 59 → 60. A observação original do QA fica como receita; nenhuma alteração de código de produção é necessária. (NEW-3)


[1.58.4] — 2026-05-19

fix(security): NEW-1 — enviar Content-Security-Policy em toda resposta (antes restrito a não-loopback). Antes da v1.58.4 o cabeçalho CSP só era adicionado quando isPubliclyExposed() era verdadeiro (HOST fora do loopback); em 127.0.0.1 tanto / quanto /api/health respondiam sem CSP, deixando o contrato escape-first de UI.md() como única defesa contra XSS. A regressão MASTER da v1.58.3 (§5) sinalizou isso como invariante stop-ship. Agora a CSP é incondicional e idêntica em toda resposta, independentemente do endereço de bind: 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'. O conjunto de diretivas não mudou em relação à política anterior (já correta para a SPA — Google Fonts na lista de permissões para o Inter), sem regressão visual ou funcional. tests/security-headers.test.mjs foi reescrito; um percurso Playwright (en/ru/ja/zh-TW × 7 rotas) verifica 0 violações de CSP. 900 unitários · Playwright 58→59 · e2e 20/20+23/23. Os próximos itens do fix-prompt são publicados como releases one-fix subsequentes. (NEW-1)


[1.58.3] — 2026-05-19

fix(deep): R-2 / FIX-C1 — remove tags de andaime ÓRFÃS / desbalanceadas do output de pesquisa. O cleanLlmMarkdown (v1.58.0) só removia blocos pareados e uma tag aberta pendente. Uma regressão profunda da v1.58.2 achou um </tool_response> órfão (e </thinking>) sem abertura que sobrevivia e renderizava literal no brief salvo de #/deep. Uma varredura conservadora final remove qualquer token de andaime solto, o XML de ferramentas Anthropic (<invoke>/<parameter>/antml:*) e blocos tool_*. Puro + idempotente; autolinks <https://…> e código preservados. FIX-C2 triado não-reproduzível (i18n.js já define <html lang> e detecta navigator.language). Ambos travados com guards. 896 → 900 unit · Playwright 58/58. Restante do fix-prompt v1.58.3 na fila como one-fix ships (doutrina: nunca em lote).


[1.58.2] — 2026-05-19

fix(i18n): I18N-011 — localiza o índice do #/help nos 7 idiomas não-EN. O TOC é gerado dos títulos ## de docs/help/<lang>.md. As seções 3/4/6/7/8/9/10/11/12 ainda tinham títulos em inglês em es/pt-BR/ko/ja/ru/zh-CN/zh-TW, então o TOC saía em inglês enquanto a sidebar estava traduzida. Cada título afetado agora é localizado com o mesmo termo da chave nav.* da sidebar (fonte única — TOC ↔ sidebar batem), preservando o número da seção e o parêntese (#/route …). EN inalterado. Fecha o único pendente i18n do QA v1.58. Somente docs; 896/896 unit · 33/33 help · Playwright 58/58.


[1.58.1] — 2026-05-19

fix(test): guard checkProfileCustomized isolado de CI (patch sobre v1.58.0). v1.58.0 passou no pre-commit (consultivo) mas falhou no ci.yml (Node 18/20/22): o teste usava import dinâmico cache-bust + reescrita de PATHS, mas paths.mjs resolve a raiz uma vez por processo. Substituído por um guard estático robusto (allow-list + regex ^(…)$/i ancorado; nome real com “test” nunca é marcado). Sem mudança de código de produção; desbloqueia publish-package.yml. 896/896 unit · Playwright 58/58. Ver qa/v158-regression/.


[1.58.0] — 2026-05-19

fix(qa): varredura de bugs do relatório QA externo + saída de pesquisa limpa e formatada. Corrigido: BUG-001 #/followup valida a data opcional como ISO YYYY-MM-DD no cliente; BUG-003 **negrito**/`código`/links agora renderizam dentro de citações em UI.md() (todas as páginas de Ajuda); BUG-005 URL duplicada no pipeline mostra «Já está na fila — ignorado»; BUG-006 mensagem de URL inválida humanizada (contexto (POST /api/pipeline · HTTP 400) mantido de propósito); BUG-007/008 o toast «Running doctor.mjs…» é descartado antes do modal (novo UI.dismissToast()), título do modal = rótulo localizado do botão; BUG-010 subtítulo no estado vazio de #/reports; BUG-002/UX-032 checkProfileCustomized() marca fixtures de teste como «não personalizado» (profile.yml/cv.md do pai intocados — regra #1); I18N-012/013 Deep research em russo realmente traduzido. Novo: cleanLlmMarkdown() remove andaime de agente (<tool_call>{…}</tool_call>, <tool_response>, <thinking> …) de #/deep e da Pesquisa salva, em todos os provedores e ao servir arquivos já salvos; alias #/outreach#/contacto (BUG-004); erro de rede do cliente localizado via I18n.t() (8 locales; os details do servidor são diagnósticos em inglês de propósito). Testes: novos tests/qa-report-fixes.test.mjs (10) e tests/llm-output.test.mjs (5); 881 → 896 unit; Playwright 58/58. Não alterado (com justificativa): BUG-009 (H1 #/cv por design, WCAG single-h1), dados do pai (parent-owned), cauda longa de i18n/UX menor no backlog. Detalhe completo em CHANGELOG.md.


[1.57.2] — 2026-05-19

fix(config): a causa REAL do «validation failed» em /#/config — o campo lang injetado pela SPA. public/js/api.js anexa automaticamente um campo lang a todos os corpos POST JSON (para as rotas LLM pegarem o idioma da UI). /api/config não é rota LLM e lang não é uma chave de configuração, então a rejeição de chaves desconhecidas do validateConfig (correta e relevante para segurança) retornava 400 em cada Salvar: validation failed — lang: not a known config key. Era só no navegador: repros com curl/in-process nunca enviavam lang, por isso v1.57.0/.1 melhoraram a mensagem mas não a causa. A rota agora remove o lang de transporte antes de validar; o filtro de escrita por KNOWN_KEYS continua descartando qualquer chave realmente desconhecida — a proteção anti-injeção é inalterada. Detectado por uma nova varredura Playwright que clica no botão Salvar real. Testes: novo tests/playwright-forms.mjs (26, integrado em npm run test:e2e:browser) sobre todos os formulários; config-endpoint com paridade de navegador. 879 → 881 unit; Playwright 32 → 58. Detalhe completo em CHANGELOG.md.


[1.57.1] — 2026-05-19

fix(ux): todo erro de API agora diz O QUE falhou, ONDE e POR QUÊ; o texto do erro de entrada é o mais descritivo possível. O servidor já retornava { error, details: ["CAMPO: motivo", …] }, mas os formulários só mostravam a linha de topo («validation failed»), então em /#/config (e em todo lugar) não dava para saber qual campo estava errado. O api.js agora incorpora os details por campo na mensagem em todo o site (uma mudança, todos os formulários se beneficiam), acrescenta o contexto da requisição (MÉTODO /caminho · HTTP NNN) (ONDE), recorre a um trecho do corpo cru em erros não-JSON e os erros de rede levam método+caminho; err.details fica exposto. As mensagens do validateConfig agora são máximamente descritivas (o que está errado e como corrigir). Chaves secretas nunca ecoam o valor digitado (só o tamanho) — uma chave real digitada errada não vaza em toast/log. A faixa de PORT agora é validada de fato (99999 é rejeitado). Em /#/config PORT e HOST vêm pré-preenchidos com os padrões reais (4317 / 127.0.0.1). Os toasts de erro ficam mais tempo (9–20 s) e quebram/rolam em vez de cortar. Testes: novo tests/config-validation-detail.test.mjs (12), 874 → 879. Detalhe completo em CHANGELOG.md.


[1.57.0] — 2026-05-19

feat(provider): OpenRouter como 5º provedor de avaliação ao vivo headless + fix(config): «validation failed» ao salvar qualquer chave API. Chaves coladas costumam chegar com uma quebra de linha final ou espaços (área de transferência do SO, botões «copiar» dos consoles dos provedores) — antes da 1.57 isso disparava o guard de quebra de linha para todos os provedores, e a regex ancorada em $ de ANTHROPIC_API_KEY rejeitava por engano chaves Anthropic reais. Agora validateConfig normaliza (faz trim) cada valor antes de validar, a rota persiste o valor já aparado (autentica em runtime, sem corromper o .env), e a checagem da Anthropic é um prefixo sk-ant- + comprimento resiliente (o piso compartilhado isUsableKey() ≥ 20 continua sendo o real «é uma chave válida?»). Quebras de linha internas continuam rejeitadas (guard de injeção no .env). OpenRouter agora é provedor de primeira classe: OPENROUTER_API_KEY em /#/config — uma chave dá acesso a mais de 300 modelos. É o último da ordem auto (Anthropic → Gemini → OpenAI → Qwen → OpenRouter), então uma configuração existente nunca é redirecionada silenciosamente; LLM_PROVIDER=openrouter o fixa. Conectado ao mesmo caminho _tailProvider() de OpenAI/Qwen em /api/evaluate, /api/deep, /api/mode/:slug; exposto em /api/status/providers + no painel de Health. Cliente compatível com OpenAI (sem novas dependências — fetch direto, timeout AbortController, a chave nunca é registrada) com os headers HTTP-Referer/X-Title recomendados. O dropdown de modelos é ao vivo: OPENROUTER_MODEL é preenchido por GET /api/openrouter/models (proxy no servidor do catálogo público da OpenRouter — mantém CSP connect-src 'self'), com lista curada de fallback e cache em memória de 10 min. Novas chaves i18n (config.openrouter*) nos 8 idiomas. Testes: novos tests/openrouter-route.test.mjs e tests/openrouter-model-selector.test.mjs; suites env-config/openai/provider-selector ampliados. 831 → 855. Detalhe completo em CHANGELOG.md.


[1.56.4] — 2026-05-19

feat(ui): UX-N2 — dica visível e ciente de plataforma de ⌘K / Ctrl K na busca global. O atalho Cmd/Ctrl+K (focar a busca) só existia no aria-label/código, então usuários videntes não o descobriam e o app parecia mais lento do que é. Agora um <kbd class="kbd-shortcut"> discreto fica no fim da pílula de busca, preenchido na inicialização a partir de data-mac/data-other por uma checagem de plataforma (navigator.platform/userAgent): ⌘K no macOS/iOS, Ctrl K no resto. É aria-hidden="true" (o aria-label existente já anuncia aos leitores de tela — o selo não deve duplicar) e pointer-events:none (decorativo). O atalho Cmd/Ctrl+K existente não muda. Sem novas chaves i18n (os glifos são universais); o selo é filho flex do .searchbar existente (sem wrapper/posição absoluta — o input já é flex:1). Testes: nova suíte estática por código isolada de CI tests/cmdk-hint-visible.test.mjs (5): o <kbd class="kbd-shortcut"> está dentro do .searchbar; é aria-hidden="true" com ambas variantes data-mac/data-other; app.js o preenche via checagem navigator; o binding (e.ctrlKey||e.metaKey)&&e.key==='k'search.focus() intacto (guard de regressão); app.css estiliza .kbd-shortcut e nunca display:none. 826 → 831. feat(ui) · test: tests/cmdk-hint-visible.test.mjs. Detalhes — CHANGELOG.md.


[1.56.3] — 2026-05-19

fix(reliability): a detecção de chaves de provedor rejeita placeholders / valores curtos demais, não apenas a string vazia. Um GEMINI_API_KEY placeholder em um .env pai era reportado como “✓ set” E selecionado como provedor ativo em vez de um ANTHROPIC_API_KEY válido. effectiveEnv() só rejeitava undefined/'', então 10 caracteres de lixo contavam como chave real: o banner de onboarding mostrava GEMINI ✓ set, GET /api/status/providers retornava activeProvider: "gemini", e toda avaliação ⚡ ao vivo teria falhado em silêncio contra uma chave morta, ignorando uma chave Anthropic válida de 108 caracteres. A nova função pura isUsableKey() (env-config.mjs) considera um segredo configurado apenas se tiver ≥ 20 caracteres (nenhuma chave suportada é mais curta — Gemini AIza… ≈ 39, Anthropic sk-ant-… ≈ 100+, OpenAI ≥ 40, Qwen ≈ 35) e não for um placeholder conhecido (your_*_here, changeme, placeholder, <…>, um único caractere repetido…). Aplicada uniformemente a hasAnthropicKey()/hasGeminiKey() (anthropic.mjs), hasOpenAIKey()/hasQwenKey() (openai.mjs) e às linhas GEMINI_API_KEY/ANTHROPIC_API_KEY de GET /api/health (movidas de process.env cru para a mesma visão effective+plausible) — a página de saúde, o endpoint de provedores e o roteador OR agora sempre concordam. selectActiveProvider() não muda; recebe um keysConfigured correto. Testes: nova suíte isolada de CI tests/key-detection-rejects-placeholder.test.mjs (5): casos unitários de isUsableKey + reprodução in-process com createApp() do cenário relatado (.env temporário com GEMINI_API_KEY de 10 caracteres + ANTHROPIC_API_KEY real) — gemini NÃO está em keysConfigured, activeProvider === "anthropic", linhas de /api/health coerentes. Quatro testes existentes de camadas effective-env tiveram stubs curtos demais alongados (o contrato não muda). 821 → 826. fix(reliability) · test: tests/key-detection-rejects-placeholder.test.mjs. Detalhes — CHANGELOG.md.


[1.56.2] — 2026-05-19

feat(a11y): UX-N1 — document.title por rota e ciente de idioma (orientação multi-aba + anúncio de mudança de página do leitor de tela). Antes as 24 rotas mantinham o <title> estático do index.html (“career-ops — command center”): abas com o mesmo nome, favoritos genéricos e o mesmo anúncio “página alterada”. O focusNewView() em public/js/router.js agora deriva o título do próprio <h1 class="page-title"> localizado da view — “View — career-ops” — então os títulos são traduzidos automaticamente (sem novas chaves i18n) e únicos por rota. Definido antes do guard do primeiro paint para a aba inicial também ter título (mesma ordem do tabindex da v1.56.0 UX-12). Recorre a career-ops — command center se a view não tiver cabeçalho. Testes: nova suíte estática por código isolada de CI tests/document-title-per-route.test.mjs (4): focusNewView atribui document.title; o título vem do <h1> (por rota + localizado, não um literal único); a atribuição precede !firstPaintDone; há um padrão de produto. 817 → 821. feat(a11y) · test: tests/document-title-per-route.test.mjs. Detalhes — CHANGELOG.md.


[1.56.1] — 2026-05-19

fix(a11y): suprime o anel de foco de marca espúrio no foco do título com tabindex="-1" gerenciado pelo router. public/js/router.js define tabindex="-1" no título da view de destino e chama .focus() a cada navegação de cliente (para o leitor de tela anunciar a nova página). Um elemento tabindex="-1" nunca é alcançável por teclado, mas a heurística :focus-visible do Chromium ainda desenhava o anel de marca global (*:focus-visible { outline: 2px solid var(--rausch) }) — um retângulo vermelho ao redor do título (ex.: “Command Center” em #/dashboard) a cada navegação, que também ficou gravado nas capturas hero images/dashboard-*.png. A correção é uma única regra com escopo [tabindex="-1"]:focus, [tabindex="-1"]:focus-visible { outline: none } (padrão de foco gerenciado do WAI-ARIA APG). O foco real de teclado em controles interativos mantém o anel global *:focus-visible (WCAG 2.4.7 intacto); o anel do skip-link não é afetado (é um <a>, não tabindex="-1", com maior especificidade). As 8 images/dashboard-*.png foram regeneradas — sem o retângulo vermelho. Testes: nova suíte estática por código isolada de CI tests/managed-focus-no-ring.test.mjs (4): o anel global *:focus-visible continua definido (WCAG 2.4.7 sem regressão); [tabindex="-1"]:focus,:focus-visibleoutline:none; a regra de supressão vem após a global (segurança de cascata); a correção tem escopo (sem *:focus{outline:none} geral). Junto com tests/dashboard-initial-focus.test.mjs. 813 → 817. fix(a11y) · test: tests/managed-focus-no-ring.test.mjs. Detalhes — CHANGELOG.md.


[1.56.0] — 2026-05-19

feat(ux): pacote de polimento LOW — UX-9 / UX-10 / UX-11 / UX-12 (uma release menor agrupada). UX-9 #/cv: o título da página é rebaixado a um chip-breadcrumb .cv-breadcrumb discreto e o subtítulo barulhento vai para o title do <h1>, para que o CV do usuário (seu nome, na pré-visualização) tenha a hierarquia visual. Invariante F-V54-A intacto — ainda exatamente um <h1>, ainda .page-title. UX-10 novo helper compartilhado UI.providerCostHint(t) ao lado de ⚡ Executar ao vivo em #/auto, #/evaluate, #/deep e cada #/<mode>; reusa GET /api/status/providers (v1.55.3): com chave mostra “Custo estimado: OpenAI gpt-5-codex · ~$0.04/eval” (ordem de grandeza, “~”); sem chave indica que ⚡ copia um prompt manual sem custo de API; fail-soft. UX-11 #/help: quando o filtro do TOC reduz a exatamente uma seção, a página rola até lá após 300ms de inatividade (debounced; nunca com 0 ou >1). UX-12 #/dashboard: no primeiro paint o <h1> fica focável (tabindex="-1") e #content permanece aria-live="polite" (anunciado no boot) sem roubar o foco (evita brigar com o skip-link, decisão v1.41.0). Novas chaves i18n cost.estimate, cost.manual ×8; novo CSS .cv-breadcrumb/.cost-hint. Testes: 4 novas suites estático-de-fonte CI-isoladas (cv-breadcrumb 3, run-cost-line 4, help-toc-autoscroll 4, dashboard-initial-focus 3); locks pré-existentes cv-single-h1/help-nav-a11y atualizados (invariantes preservados). 800 → 813. Sonda Playwright ao vivo dos 4, 0 erros de console. feat(ux) · 4 test suites. Detalhe em CHANGELOG.md.


[1.55.8] — 2026-05-19

feat(tracker): paginação do lado servidor + chips de funil clicáveis (UX-8). Servidor: GET /api/tracker ganha params opcionais ?page / ?pageSize / ?status. Sem eles, a resposta é byte a byte o { rows: [...] } legado (todos os chamadores/testes existentes intactos). Com eles retorna { rows: slice, total, page, pageSize, funnel }pageSize clampado a [1,500], page a ≥1, status filtra rows+total, e funnel é o detalhamento status→contagem de todo o histórico (independente de página ou filtro, para os chips serem sempre exatos). #/tracker: nova barra de chips de funil clicável no topo — “todos status · N · Applied · N · Interview · N …” (ordem Applied → Responded → Interview → Offer → Rejected → Discarded → Evaluated → SKIP). Clicar um chip define o filtro de Status (clicar o ativo limpa); o chip ativo é aria-pressed e destacado. Nova chave i18n track.funnelAria ×8; novo CSS .tracker-funnel/.tracker-chip/.tracker-chip--active. test: tests/tracker-server-paged.test.mjs (novo, 7 casos, CI-isolado, Express in-process em porta efêmera + applications.md temporário em CAREER_OPS_ROOT — CLAUDE.md #2/#8): back-compat (sem params ⇒ exatamente {rows}); ?page&pageSize slice + total/page/pageSize/funnel somando N; última página parcial sem sobreposição; página fora de alcance ⇒ rows vazio + total válido; ?status= filtra total/rows com funnel de todo o histórico; cap de pageSize; + lock estático-de-fonte da barra de chips. 793 → 800. feat(tracker) · test: tests/tracker-server-paged.test.mjs. Detalhe em CHANGELOG.md.


[1.55.7] — 2026-05-19

feat(pipeline): virtualização de linhas em vanilla-JS para >1000 linhas (UX-7). #/pipeline renderizava todas as linhas (filtered.forEach(list.appendChild(urlRow))) — um scan enche a fila com milhares de URLs, então milhares de nós de linha (cada um um div flex + <a> + dois botões) eram construídos sincronamente a cada tecla do filtro, saturando o DOM e a árvore de acessibilidade. Nova virtualização vanilla-JS (equivalente a react-window, sem deps): acima de VIRTUALIZE_THRESHOLD = 1000, #/pipeline vira um viewport com scroll de altura fixa (70vh) com um espaçador não encolhível (flex:0 0 auto, height = linhas × 56px) que preserva a barra de rolagem real da lista inteira, e um listener de scroll com rAF renderiza só o viewport ± um buffer de 5 linhas (~16–19 nós por vez em vez de N). Em/abaixo do limiar o render simples original é mantido byte a byte, então pipelines típicos e todos os tests/e2e existentes não são afetados. Cada linha virtualizada mantém seu aria-label ▶/✕ desambiguado por URL (F-V54-B travado por regressão). O cálculo de janela é um helper puro computeWindow(). test: tests/pipeline-virtualize.test.mjs (novo, 5 casos, CI-isolado, estático-de-fonte): limiar numérico ~1000; ramo ≤limiar mantém forEachappendChild; ramo >limiar renderiza slice(start,end) com listener de scroll rAF + espaçador; computeWindow() clampa [0,total] com ± buffer; linhas mantêm aria-labels ▶/✕. 788 → 793. Sonda Playwright ao vivo (fixture de 1200 URLs): scrollHeight≈67248, só ~16–19 nós no DOM, a janela acompanha o scroll de ponta a ponta, 0 erros de console. feat(pipeline) · test: tests/pipeline-virtualize.test.mjs. Detalhe em CHANGELOG.md.


[1.55.6] — 2026-05-19

feat(scan): recolher filtros secundários atrás de um disclosure “Filtros avançados” (UX-4). #/scan empilhava todos os filtros — texto livre, remoto/híbrido/presencial, escopo, fonte e os chips de facetas stack/nível/dinâmicos pós-scan — com peso igual, um muro de controles. Agora os filtros do dia a dia ficam visíveis (texto livre + Remoto/Híbrido/Presencial; o botão 🌐 Buscar já está separado no card de controles) e os secundários colapsam atrás de um <details class="scan-advanced"><summary>Filtros avançados</summary>: os selects Escopo + Fonte e — separadamente — o cluster de chips de facetas (que agora lidera o resultado com a tabela, não um muro de chips, e só renderiza se houver ao menos uma linha de chips). Nova chave i18n scan.advancedFilters nos 8 locais; novo estilo .scan-advanced (affordance ⚙ discreta, sem marcador, negrito ao abrir). test: tests/scan-advanced-disclosure.test.mjs (novo, 6 casos, CI-isolado, estático-de-fonte): existe <details>/<summary> com hook .scan-advanced e rótulo scan.advancedFilters; texto livre + remoto seguem visíveis; escopo + fonte dentro do disclosure; chipsContainer é <details>; .scan-advanced summary estilizado; scan.advancedFilters ×8. 782 → 788. feat(scan) · test: tests/scan-advanced-disclosure.test.mjs. Detalhe em CHANGELOG.md.


[1.55.5] — 2026-05-19

feat(dashboard): hero com os 2 CTAs P0 + dica focal de atividade recente (UX-3). #/dashboard abria com ~30 nós de peso igual — sem um “o que vem a seguir” claro. Um novo bloco .dash-hero agora fica logo abaixo do cabeçalho: as duas jornadas P0 — ✨ Auto-pipeline para uma URL e 🌐 Buscar agora — são promovidas a botões grandes .btn-hero, e uma única dica focal de atividade recente (“Última avaliação: <score><título>”, com link para o relatório; estado vazio guia no arranque a frio via dash.heroNoEval) diz ao usuário recorrente onde parou e ao novo a única ação que importa. Os dois botões primários foram removidos do cabeçalho (só fica o secundário “📋 Abrir pipeline”) para não duplicar a ação. Os contadores de status foram rebaixados de .badge proeminentes para pílulas .dash-chip discretas. Novas chaves i18n dash.lastEval, dash.heroNoEval nos 8 locais; novo CSS .dash-hero/.btn-hero/.dash-chip. test: tests/dashboard-hero.test.mjs (novo, 5 casos, CI-isolado, estático-de-fonte): .dash-hero existe e precede a grade Quick-actions; ambos CTAs P0 são .btn-hero com rotas /auto+/scan; dica focal dash.lastEval + estado vazio dash.heroNoEval; buckets usam .dash-chip; CSS existe; dash.lastEval+dash.heroNoEval ×8. 777 → 782. feat(dashboard) · test: tests/dashboard-hero.test.mjs. Detalhe em CHANGELOG.md.


[1.55.4] — 2026-05-19

feat(ux): ETA honesta do auto-pipeline ao lado de Run + Stop proeminente durante um scan (UX-6). #/auto: nova dica .auto-eta“⏱ ~1–2 min” (chave auto.eta, title via auto.etaTitle) — agora ao lado do botão Run, para que a promessa de um clique seja honesta sobre a duração antes de o usuário se comprometer; o texto casa com career-ops.org/docs (“cole uma URL → relatório completo em 1–2 minutos”). #/scan: enquanto o rastreamento de vários minutos está ativo (aria-busy), o Stop é promovido de botão fantasma de baixo contraste a botão destrutivo proeminente (novo .btn-danger — preenchido, branco sobre coral de alto contraste, peso 600). setScanRunning(running) alterna scan-stop-btn entre btn-danger (rodando) e btn-ghost (ocioso, oculto de qualquer forma), para que o usuário encontre e confie no Stop sob carga. Novas chaves i18n auto.eta, auto.etaTitle nos 8 locais; novo CSS .btn-danger/.auto-eta. test: tests/auto-eta-stop.test.mjs (novo, 4 casos, CI-isolado, estático-de-fonte): #/auto renderiza t('auto.eta') com classe .auto-eta ao lado de runBtn; auto.eta ×8; setScanRunning(running) promove Stop a btn-danger; .btn-danger existe com texto branco de alto contraste. 773 → 777. feat(ux) · test: tests/auto-eta-stop.test.mjs. Detalhe em CHANGELOG.md.


[1.55.3] — 2026-05-19

feat(onboarding): status OR de 4 provedores na tela — banner de arranque a frio + chip de provedor ativo (UX-2, ALTA). Novo endpoint somente-leitura GET /api/status/providers{ activeProvider, activeModel, keysConfigured }. keysConfigured usa a mesma visão de env efetiva dos gate sites de llm.mjs (process.env ∨ .env do pai); activeProvider é o que o OR-router escolheria — selectActiveProvider(), novo helper puro em env-config.mjs que percorre providerOrder() (um pin LLM_PROVIDER sem chave correspondente dá null). Nenhum segredo é retornado — só nomes de provedor + o id do modelo. O shell da SPA agora renderiza uma região de onboarding global (#onboarding-banner, populada por app.js, só DOM seguro para CSP): 0 chaves → banner vermelho com CTA para #/config?tab=api-keys; ≥1 chave → chip discreto com o provedor + modelo ativo. Torna o diferencial principal (“um de Anthropic / Gemini / OpenAI / Qwen, auto-ordenado”) descobrível na tela em vez de aprendido por tentativa. Novas chaves i18n onboarding.* nos 8 locais; novo CSS .onboarding-warn/.onboarding-ok. test: tests/onboarding-key-banner.test.mjs (novo, 9 casos, CI-isolado): semântica de selectActiveProvider; GET /api/status/providers em processo (porta efêmera + .env em CAREER_OPS_ROOT temporário para nunca ler a chave real do pai — CLAUDE.md #2/#8); cabeamento SPA estático + cobertura onboarding.* ×8. 764 → 773. feat(onboarding) · test: tests/onboarding-key-banner.test.mjs. Detalhe em CHANGELOG.md.


[1.55.2] — 2026-05-18

fix(cv): dar ao editor markdown de #/cv um nome acessível descritivo e autônomo (F-V55-H / UX-5). O <textarea id="cv-editor"> do editor principal de #/cv agora carrega um aria-label descritivo via a nova chave cv.editorAria“Editor markdown do CV — seu currículo profissional em formato markdown” — em vez do nome enxuto que herdava do cabeçalho visível “Markdown”. Nota: ao contrário do sintoma de F-V55-H (que só inspecionou aria-label/labels), o campo não estava sem nome — v1.47.0 (WS2 #16) já o havia vinculado via aria-labelledby → o <h3 id="cv-md-heading">Markdown</h3>, então um leitor de tela anunciava “Markdown, edição, multilinha”. v1.55.2 melhora esse enxuto “Markdown” para um rótulo autônomo. O aria-labelledby redundante é removido (seria markup morto — aria-label vence por precedência ARIA); o <h3>Markdown</h3> visível permanece para usuários videntes. WCAG 1.3.1 + 4.1.2; paralelo ao conserto batch-tsv de v1.54.5 (F-V54-C). test: tests/cv-editor-a11y.test.mjs (novo, 3 casos, CI-isolado, estático-de-fonte como auto-stepper-prerender.test.mjs): #cv-editor se nomeia via t('cv.editorAria', …) com fallback não vazio; cv.editorAria presente e não vazio nos 8 locais; sem aria-labelledby redundante no elemento. 761 → 764. fix(cv) · test: tests/cv-editor-a11y.test.mjs. Detalhe em CHANGELOG.md.


[1.55.1] — 2026-05-18

fix(auto): pré-renderizar o stepper de 5 etapas do pipeline ao montar #/auto (F-V55-E / UX-1, obs. sênior S-4 reaberta). #/auto agora mostra o esquema documentado de cinco etapas — validar → buscar → avaliar → salvar relatório → adicionar ao tracker — no instante em que a tela monta, em vez de ficar em branco até o primeiro evento SSE. Antes <ol class="auto-stepper"> era criado display:none e renderStepper() só era alcançado a partir de setStep() / run(), então um usuário em arranque a frio nunca via o pipeline que os docs prometem antes de clicar Run. O stepper agora é visível ao montar com as cinco etapas no estado pending e carrega um aria-label (auto.stepperAria) para que a tecnologia assistiva anuncie a região. Fecha F-V55-E (lente a11y/garantia estática) e UX-1 (lente fidelidade de promessa) — mesma correção, ambas as lentes. test: tests/auto-stepper-prerender.test.mjs (novo, 4 casos, CI-isolado, estático-de-fonte como router.test.mjs): o array STEPS são exatamente as 5 etapas canônicas em ordem; stepperEl não é display:none ao montar e carrega auto.stepperAria; uma chamada renderStepper() de escopo de montagem precede function setStep(; auto.stepperAria presente nos 8 locais. 757 → 761. fix(auto) · test: tests/auto-stepper-prerender.test.mjs. Detalhe em CHANGELOG.md.


[1.55.0] — 2026-05-18

feat(llm): a live-eval headless funciona via “OR” — Anthropic | Gemini | OpenAI | Qwen, autoselecionada conforme qual chave estiver definida. A pedido do usuário, a ⚡ live-eval do web-ui agora funciona com qualquer chave de API que estiver definida, não só Anthropic/Gemini. LLM_PROVIDER ganha openai e qwen; auto (padrão) usa o primeiro provedor cuja chave esteja presente, preferindo Anthropic → Gemini → OpenAI → Qwen. Um valor explícito fixa um; um provedor forçado sem chave ainda cai no caminho do prompt manual. Novo server/lib/openai.mjs — um cliente Chat Completions compatível com OpenAI e sem dependências (mesmo padrão HTTPS direto seguro de anthropic.mjs: timeout AbortController, chave nunca logada, resolução de chave effectiveEnv() para que uma chave do .env do pai funcione sem reinício). Um núcleo (runOpenAICompatible) sustenta runOpenAI (api.openai.com) e runQwen (modo compatível com OpenAI da Alibaba DashScope; sobrescreva o endpoint com QWEN_BASE_URL no .env cru para o host da China continental). Sem SDKs, sem execução arbitrária de CLI — o projeto pai continua agnóstico de CLI (Claude Code · Codex · Gemini · OpenCode · Qwen · Copilot · Kimi); isto só estende o caminho headless por chave de API. A cauda OpenAI/Qwen está cabeada em todas as superfícies de eval: /api/evaluate, /api/deep, /api/mode/:slug e o SSE de /api/auto-pipeline — consultada após as ramificações Anthropic (inline) + Gemini (subprocesso) para preservar a preferência auto, com o mesmo inlining de contexto empacotado que a Anthropic usa. env-config.mjs: QWEN_API_KEY (secreto) + QWEN_MODEL (não secreto) adicionados a KNOWN_KEYS/KEY_GROUPS.core; LLM_PROVIDERS e providerOrder() estendidos; OPENAI_API_KEY agora é uma chave de provedor headless de primeira classe (antes só armazenada). Aba de chaves do #/config: o select LLM_PROVIDER ganha openai/qwen; novos campos QWEN_API_KEY + QWEN_MODEL (lista curada qwen-max/qwen-plus/qwen-turbo/qwen2.5-*); uma nova nota no topo da aba explica o pai agnóstico de CLI vs a eval headless do web-ui e a ordem OR. Novas chaves i18n nos 8 locais. test: tests/openai.test.mjs (novo, 9 casos, CI-isolado): sucesso OpenAI/Qwen + conteúdo em array de blocos, auth Bearer, endpoints padrão e sobrescrito por QWEN_BASE_URL, 4xx/5xx/malformado, clamp de max_tokens, timeout, detecção de chave effectiveEnv, canário de não-vazamento de chave. tests/provider-selector.test.mjs atualizado para a superfície providerOrder/LLM_PROVIDERS/SECRET de v1.55.0 + o cabeamento da cauda OpenAI/Qwen. 748 → 757. feat(llm) · test: tests/openai.test.mjs · test: tests/provider-selector.test.mjs. Detalhe em CHANGELOG.md.


[1.54.10] — 2026-05-18

fix(auto-pipeline): higiene de desconexão do cliente SSE — eliminar o job e2e do Playwright instável. O job e2e do Playwright ficava vermelho de forma intermitente (32/32 testes individuais passam, mas not ok 2 - tests/playwright-smoke.mjs): fechar uma página enquanto o stream SSE de #/auto estava em andamento fazia o próximo res.write() do servidor ser rejeitado com EPIPE/"aborted", e —sem um listener 'error' na resposta— o Node escalava isso para uma uncaughtException que o node:test reportava como “asynchronous activity after the test ended”. openSse() em auto-pipeline.mjs agora registra um res.on('error') no-op e protege send() com res.writableEnded || res.destroyed (envolvido em try/catch) — um cliente que sumiu é esperado, não excepcional. Isto é higiene SSE de produção correta, não apenas uma correção de teste. tests/playwright-smoke.mjs: o teste do Cmd+K usava uma URL externa real (https://example.com/jobs/123) mas só esperava o modal aparecer, então closePage() abortava o safeGet() em andamento do servidor depois que o teste terminava. Agora ele espera o pipeline atingir um estado terminal (para que o fetch resolva normalmente antes do fechamento). Um helper compartilhado closePage() (window.stop() e então fechar) e o hook after com server.closeAllConnections() permanecem como defesa em profundidade. Verificado: 8/8 execuções verdes consecutivas (6× node --test + 2× browser-smoke), antes ~1-em-2 vermelho. tests/auto-pipeline.test.mjs +1 caso estático fixando o contrato de higiene de desconexão de openSse (listener res.on('error') + guarda writableEnded||destroyed + escritas envolvidas em try). 747 → 748. fix(auto-pipeline) · test: tests/auto-pipeline.test.mjs. Detalhe em CHANGELOG.md.


[1.54.9] — 2026-05-18

fix(llm): honrar as chaves LLM do .env do projeto pai em tempo de requisição — parar de rotear erroneamente para um provedor obsoleto/inválido. A avaliação ao vivo podia falhar com “Gemini API error: API key not valid” mesmo quando ANTHROPIC_API_KEY era o provedor configurado. Causa raiz: hasAnthropicKey() / hasGeminiKey() (e a busca de chave/modelo de runAnthropic) liam apenas o snapshot de process.env do boot. Se a chave Anthropic fosse adicionada ao .env do pai depois que o servidor iniciou, o processo em execução nunca a via → a detecção de Anthropic era falsa, e a avaliação caía para qualquer chave obsoleta que de fato estivesse em process.env (frequentemente um GEMINI_API_KEY antigo e inválido). O caminho de execução do Gemini (um subprocesso Node do pai) já lia o .env vivo do pai, então os dois provedores resolviam chaves de forma inconsistente. Novo effectiveEnv(key, envFilePath) em env-config.mjs: um valor não vazio de process.env vence (cobre exports de shell e o live-apply em POST /api/config); caso contrário consulta-se o .env atual do pai. anthropic.mjs agora resolve ANTHROPIC_API_KEY, ANTHROPIC_MODEL e a verificação da chave Gemini através dele, de modo que uma chave definida no .env do pai é honrada sem reiniciar o servidor e a DETECÇÃO de chave sempre coincide com a chave que a requisição realmente ENVIA. A ordem dos provedores não muda (auto → Anthropic-depois-Gemini); isto só corrige a detecção. As chaves nunca são logadas nem retornadas (o teste de não-vazamento REVIEW-B4 continua passando). tests/anthropic.test.mjs reescrito para ser CI-isolado (temp CAREER_OPS_ROOT, import dinâmico) com 2 casos novos que reproduzem o bug exato (chave só no .env do pai → detectada; runAnthropic envia a chave + modelo do .env do pai quando process.env está indefinido). tests/env-config.test.mjs +3 casos effectiveEnv (precedência de process.env, fallback ao .env incl. string-vazia-como-indefinida, arquivo-ausente / chave-ausente / sem-caminho → undefined) — 100% do novo ramo. 742 → 747. fix(llm) · test: tests/anthropic.test.mjs · test: tests/env-config.test.mjs. Detalhe em CHANGELOG.md.


[1.54.8] — 2026-05-18

feat(config): o formulário por campos de Modes sempre renderiza o esquema canônico (mesmo em um arquivo vazio/stub) com a orientação de campos do career-ops.org. O formulário por campos de Modes da v1.54.3 só renderizava campos para seções ## já existentes — então em um modes/_profile.md recém-criado, vazio ou fora do esquema (p. ex. o stub comum de 1 linha) ele caía em “No ## sections found — use the raw editor below.” e o usuário não recebia campos. A pedido do usuário (“разбей по полям … описание полей возьми из career-ops.org/docs”), o formulário agora sempre renderiza os 5 campos canônicos na ordem documentada (Target Roles, Adaptive Framing, Exit Narrative, Comp Targets, Location Policy), pré-preenchidos a partir do arquivo quando presentes e vazios-mas-editáveis quando não — de modo que um perfil totalmente novo pode ser preenchido inteiramente pelo formulário. Cada campo exibe uma descrição obtida do §Step-5 do Quick Start canônico do career-ops.org (o que colocar em Target Roles / Adaptive Framing / Exit Narrative / Comp Targets / Location Policy), conectada via aria-describedby para leitores de tela. Tolerante a variantes de cabeçalho: o ## Your Target Roles (etc.) do template mapeia para o mesmo campo canônico que ## Target Roles, de modo que nem o template nem a convenção do scaffold do servidor quebram o formulário. collect() agora é um payload etiquetado: uma mesclagem { sections } não destrutiva quando os cabeçalhos renderizados batem exatamente com os do arquivo (preâmbulo + seções intocadas + personalizadas sobrevivem byte-estáveis), ou uma reconstrução de arquivo completo { markdown } que inicializa/normaliza um documento conforme ao esquema quando o arquivo não o tinha. O caminho de reconstrução é protegido por confirmação em config.js (substitui o arquivo pai — invariante de salvamento destrutivo WS2 #4), preserva o preâmbulo existente (ou um padrão documentado) e mantém as seções não canônicas verbatim. 6 novas chaves i18n (config.modesDescTargetRolesconfig.modesDescLocationPolicy + config.modesFormRebuildBody) nos 8 locales. tests/modes-form.test.mjs reescrito para o contrato v1.54.8: esquema + ordem canônica, fiação de payload/confirmação do config.js, presença da descrição de cada campo proveniente da documentação nos 8 locales, tolerância canonicalKey “Your X”, estabilidade do round-trip de listas, a garantia de bootstrap-sempre-renderiza, e o collect() etiquetado seções-vs-markdown com segurança de dados. Verificado ao vivo contra o arquivo stub real do pai (5 campos + descrições aparecem, 0 erros de console) e um fixture stub isolado (preencher → salvamento protegido por confirmação → as 5 seções canônicas persistidas). feat(config) · test: tests/modes-form.test.mjs. Detalhe em CHANGELOG.md.


[1.54.7] — 2026-05-18

fix: W-001 — assets de código/estilo + o shell da SPA eram servidos com Cache-Control: no-store (higiene de deploy). A SPA carrega api.js / router.js / cada view via <script src> simples sem query string de versão, e não há etapa de build (sem content hashing), então após um deploy um navegador podia continuar servindo um bundle antigo cacheado por horas → 404 de cache obsoleto em rotas com query string (observado ao vivo durante a regressão v1.29.2; corrida de regressão W-001). server/index.mjs agora define Cache-Control: no-store em .js / .mjs / .css / .html via o hook setHeaders do express.static, e explicitamente no catch-all do shell da SPA (que usa sendFile e contorna setHeaders), de modo que o navegador sempre revalida o código que conduz o roteamento. Assets estáticos não-código mantêm o caching padrão do express.static. As cabeçalhos de segurança (CSP / nosniff / frame-deny / referrer-policy) não mudam — verificado pela suíte security-headers existente (8 casos) rodando em verde ao lado do novo teste. +1 arquivo de testes tests/asset-cache-control.test.mjs — 4 casos (assets JS no-store, CSS no-store, index.html estático no-store, shell de rota profunda do catch-all da SPA no-store), inicializando o app real contra um CAREER_OPS_ROOT isolado. Mais um conserto de teardown flaky em tests/playwright-smoke.mjs (commit test(e2e) à parte): o teste de fumaça SSE do auto-pipeline agora cancela o reader + aborta o fetch num finally e o hook after força o fechamento de sockets persistentes, eliminando o “Error: aborted” pós-teardown que avermelhava o job Playwright e2e de v1.54.6. 738 → 742. fix · test: tests/asset-cache-control.test.mjs. Detalhe em CHANGELOG.md.


[1.54.6] — 2026-05-18

fix(a11y): S-7 — o botão back-to-top de #/help carrega a classe seletora canônica back-to-top. O botão flutuante back-to-top de #/help funcionava corretamente (verificado ao vivo) mas sua lista de classes (btn btn-primary help-back-top) ficava fora da convenção do seletor .back-to-top que o teste da spec §2 #28 mira — um seletor mais estrito teria flutuado (corrida de regressão S-7, “vitória fácil”). O botão agora carrega também a classe canônica back-to-top. Puramente aditivo e um no-op de CSS: help-back-top (o hook CSS existente) não muda e back-to-top não tem regra CSS — é apenas uma alça estável de teste/automação. Verificado ao vivo: document.querySelector('.back-to-top') resolve o botão, aria-label intacto, 0 erros de console. O caso #12 existente em tests/help-nav-a11y.test.mjs foi estendido com uma asserção de que a lista de classes do botão back-to-top inclui o seletor canônico back-to-top (sem arquivo novo). fix(a11y) · test: tests/help-nav-a11y.test.mjs. Detalhe em CHANGELOG.md.


[1.54.5] — 2026-05-18

fix(a11y): F-V54-C — o editor TSV de #/batch tem um nome acessível. O <textarea> TSV de #/batch tinha uma dica cabeada via aria-describedby mas nenhum nome acessível — sem <label htmlFor>, sem aria-label/aria-labelledby (corrida de regressão F-V54-C; WCAG 1.3.1 Info & Relationships / 4.1.2 Name, Role, Value). aria-describedby fornece uma descrição, não um nome, então um leitor de tela anunciava um “edit text” sem rótulo. O textarea agora carrega um aria-label via a nova chave i18n batch.tsvAria, consistente com as entradas irmãs de controle de corrida que já usam chaves *Aria; a dica describedby existente é preservada. Verificado ao vivo: aria-label presente + localizado, aria-describedby intacto, 0 erros de console. Nova chave i18n batch.tsvAria nos 8 locales. +1 arquivo de testes tests/batch-tsv-accessible-name.test.mjs (2 casos: o bloco batch-tsv tem um aria-label via t(batch.tsvAria) mantendo sua dica describedby; batch.tsvAria definida nos 8 locales); 736 → 738. fix(a11y) · test: tests/batch-tsv-accessible-name.test.mjs. Detalhe em CHANGELOG.md.


[1.54.4] — 2026-05-18

fix(a11y): F-V54-B — os botões de ação por linha de #/pipeline têm nomes acessíveis. Os botões (avaliar) e (excluir) por linha em #/pipeline eram somente ícones com apenas um atributo title (corrida de regressão F-V54-B; WCAG 4.1.2 Name, Role, Value). title não é um nome acessível confiável, então um usuário de leitor de tela ouvia uma longa sucessão de “button” indistintos e não conseguia saber qual linha uma exclusão atingiria. Ambos os botões agora carregam um aria-label explícito desambiguado por uma URL compacta via um novo helper shortUrl() (host + …/ + os 2 últimos segmentos de caminho; fallback de recorte final para entradas não parseáveis), de modo que a árvore de a11y lê p. ex. “Delete: hh.ru/…/vacancy/12345”. Sem novas chaves i18n — reutiliza common.delete / pipe.evaluateBtn + a URL. Verificado ao vivo: 1385 linhas, cada nome de botão único por linha, 0 erros de console. +1 arquivo de testes tests/pipeline-row-action-names.test.mjs (4 casos: ambos os botões cabeados com shortUrl(url) + exatamente dois desses rótulos, shortUrl declarado antes do uso, URLs de mesmo host mas emprego diferente não colapsam, fallbacks de host puro / não parseável / vazio); 732 → 736. fix(a11y) · test: tests/pipeline-row-action-names.test.mjs. Detalhe em CHANGELOG.md.


[1.54.3] — 2026-05-18

feat(config): formulário de campos estruturado para a aba “Modes” de #/config (sem mais markdown cru). A aba “Modes” editava modes/_profile.md como um único <textarea> cru por seção ## (granularidade em nível de seção da v1.36.0). A pedido do usuário, agora renderiza um formulário de campos estruturado derivado do esquema documentado (career-ops.org Quick Start §Step-5): Target Roles / Adaptive Framing / Comp Targetsentradas de linha rotuladas repetíveis para adicionar/remover (uma linha de cargo/ângulo/comp por campo, + Add line / por linha com aria-label); Exit Narrative / Location Policy → um único <textarea> de prosa rotulado. Cada campo é um controle real vinculado por <label htmlFor> com um nome de seção i18n. O novo public/js/lib/modes-form.js (window.ModesForm) detém a lógica parse → render → collect(); ele alimenta o caminho de merge existente PUT /api/modes/_profile { sections }, de modo que o preâmbulo, a ordenação e qualquer seção que o formulário não toque sobrevivem byte-estáveis (merge-não-substituição, imposto pelo servidor). Segurança de dados: uma seção de lista canônica cujo corpo não seja uma lista de marcadores pura (o usuário colocou prosa ali) e qualquer seção ## não canônica recorrem a um <textarea> literal rotulado com uma nota explicativa — o conteúdo arbitrário faz round-trip intacto, nunca é reescrito nem perdido silenciosamente. Estabilidade de round-trip comprovada: serialise(parse(body)) re-parseia identicamente. O editor de markdown cru do arquivo completo permanece como a divulgação Advanced com confirmação para adicionar/remover seção e editar o preâmbulo (portão de salvamento destrutivo do WS2 #4 inalterado). 10 novas chaves i18n (config.modesTargetRolesconfig.modesUnknownNote) nos 8 locales. +1 arquivo de testes tests/modes-form.test.mjs (7 casos); 725 → 732. Verificado ao vivo contra uma fixture isolada CAREER_OPS_ROOT: 5 seções canônicas renderizadas como campos + 1 seção personalizada como fallback rotulado, round-trip de editar-e-salvar preservou o preâmbulo + a seção personalizada, 0 erros de console. feat(config) · test: tests/modes-form.test.mjs. Detalhe em CHANGELOG.md.


[1.54.2] — 2026-05-18

feat(config): seletor de modelo OpenAI / Codex em #/config. #/config não tinha forma de escolher o modelo OpenAI / Codex — apenas ANTHROPIC_MODEL e GEMINI_MODEL tinham menus suspensos, embora OPENAI_API_KEY já estivesse exposta para o fluxo multi-CLI do projeto pai (Codex / OpenCode). Agora OPENAI_MODEL é uma chave de ambiente de primeira classe: adicionada ao KNOWN_KEYS de env-config.mjs (ordenada logo após OPENAI_API_KEY) e ao grupo de chaves core, e deliberadamente não em SECRET_KEYS — é um id de modelo, não uma credencial, então nunca é mascarada. config.js ganha uma lista curada OPENAI_MODELS (gpt-5-codex por padrão, depois gpt-5 / gpt-5-mini / gpt-4.1 / o4-mini / o3) e um campo <select> OPENAI_MODEL renderizado logo após a chave OpenAI, espelhando exatamente os campos de modelo Anthropic/Gemini. Novas chaves i18n config.openaiModel + config.openaiModelHint nos 8 locales. +1 arquivo de testes tests/openai-model-selector.test.mjs (4 casos); 721 → 725. Verificado ao vivo: #/config → select OPENAI_MODEL com 6 opções, padrão gpt-5-codex, vinculado ao rótulo, 0 erros de console. feat(config) · test: tests/openai-model-selector.test.mjs. Detalhe em CHANGELOG.md.


[1.54.1] — 2026-05-18

fix(a11y): F-V54-A — #/cv com um único <h1>. O próprio # Name do markdown do CV era renderizado como um segundo <h1> de nível superior ao lado do <h1>CV</h1> do título da página (corrida de regressão F-V54-A; WCAG 1.3.1 Informações e relações / 2.4.6 Cabeçalhos). Agora cv.js canaliza cada ponto de injeção da pré-visualização do CV (render inicial, atualização ao importar arquivo, sincronização ao vivo do editor) por um cvMd() de escopo restrito que rebaixa os cabeçalhos um nível (h1→h2 … h6→role="heading" aria-level="7"), de modo que a página mantém exatamente um <h1>. Restrito a cv.js de propósito — UI.md é compartilhado por help/reports/deep/evaluate, que gerenciam seus cabeçalhos à sua maneira. +1 arquivo de testes tests/cv-single-h1.test.mjs (4 casos); 717 → 721. Verificado ao vivo: #/cv → 1 <h1>, o # Name do usuário agora é <h2>, 0 erros de console. fix(a11y): F-V54-A · test: tests/cv-single-h1.test.mjs. Detalhe em CHANGELOG.md.


[1.54.0] — 2026-05-18

WS10 — re-validação de docs canônicos + paridade H3 do pacote de ajuda (a versão final de convergência). O gate de CI de CHANGELOG/estrutura só verificava H2, então docs/help/en.md havia derivado em silêncio até 70 subseções H3 enquanto os 7 pacotes localizados ficavam em 68 — a lacuna era §17 (a tabela «Reference adapters» + a lista «Common pitfalls», só em inglês). Ambas estão agora traduzidas para os 7 idiomas (nomes de arquivo / links / identificadores de adaptadores mantidos byte a byte idênticos); os 8 pacotes agora têm 17 H2 / 70 H3. Um novo gate de paridade H3 em help-ru-config-section.test.mjs o trava (716 → 717). canonical-docs-coverage.test.mjs 7/7 confirma que a ajuda ainda reflete as 5 guias de career-ops.org/docs; a auditoria UX do WS2 (40 achados v1.41→v1.52) validou cada tela frente aos docs — sem divergência. docs/sdd/CONVENTIONS.md atualizado para v1.54.0 (totais de testes, gate de paridade H3, arquivos atípicos por tamanho, nova seção de convenções de Acessibilidade). WS0–WS10 completos; só resta WS11. fix(docs): WS10 canonical re-validation + H3 parity · test(help): H3-parity gate. Detalhe em CHANGELOG.md.


[1.53.0] — 2026-05-18

WS9 — pirâmide de testes da superfície shell (a última camada sem testes). Os 4 scripts bin/*.sh e o hook .githooks/pre-commit tinham cobertura zero; o novo tests/sh-files.test.mjs adiciona 10 casos que fixam a sintaxe bash -n/sh -n, o shebang + bit executável e os contratos de comportamento dos quais outros workstreams dependem: career-ops-ui.shhelp sai com 0 sem vazamento de shell-source (guarda de regressão v1.40.0), um verbo desconhecido sai com 2, e usage() é um heredoc; start.sh — respeita NO_OPEN, exige Node ≥ 18 e delega o levantamento do navegador a scripts/open-dashboard.mjs (guarda v1.43.0); setup.sh — modo estrito, SKIP_START, clona ambos os repos; run_all.sh — parsing de --quick/--no-e2e e as 4 suítes; .githooks/pre-commit executa o revisor do WS7 e nenhum arquivo shell invoca git --no-verify (guarda da regra dura #7 do CLAUDE.md); install-hooks.mjs conecta core.hooksPath. docs/architecture/TESTING.md — adicionada a camada base de superfície shell ao diagrama da pirâmide + uma nota de totais v1.53.0 (716 casos node --test / 90 arquivos + 4 superfícies E2E). 706 → 716. Detalhe em CHANGELOG.md.


[1.52.0] — 2026-05-18

WS2 LOWs #33–#40 — varredura de polimento em lote (fecha a fila da auditoria de UX). Oito achados de baixa severidade. fix(a11y/i18n): WS2 LOW batch — #33: #/dashboard — os 3 CTAs de cabeçalho eram inconsistentes (só 2 tinham ícone inicial); «Open Pipeline» agora leva 📋 e os três coincidem. #34: #/profile — os fit/level do arquétipo eram dois chips ambíguos; agora prefixados (Fit: / Level:) com aria-label correspondente. #35: #/health — os toasts de Run-doctor / verify mostravam strings cruas de doctor.mjs; agora com chave i18n. #36: #/health — os resultados das verificações eram <div>s planos; agora um role=list <ul>/<li> e o badge de status leva aria-label="<check>: <status>". #37: #/reports — os cards eram <div onClick> só de mouse; agora role=link + tabindex + handler Enter/Espaço + aria-label. #38: #/activity — o comentário do paginador dizia «200» enquanto o código pedia 500; reconciliado a uma constante CAP e um aviso role=note aflora quando o limite de 500 trunca o histórico antigo. #39: #/batch — os placeholders em prosa estavam codificados em inglês enquanto seus aria-labels estavam localizados; os quatro agora com chave i18n. #40: as páginas de modo relabelavam o botão primário em silêncio após a sonda assíncrona; agora uma região role=status cortês o anuncia. 10 novas chaves i18n × 8 idiomas ({n} preservado); +9 testes: test: tests/low-sweep.test.mjs. 697 → 706. Fecha a fila da auditoria de UX de WS2 (#1–#40 de v1.41→v1.52); próximo WS9 → WS10 → WS11. Detalhe em CHANGELOG.md.


[1.51.0] — 2026-05-18

WS2 #13 + #14 + #18 + #19 + #20 — varredura de feedback/i18n em #/auto e #/evaluate. Cinco achados da auditoria de UX. fix(a11y/ux): auto+evaluate — busy state, actionable HTTP errors, clipboard fallback, aria-live result, spinner-guarded submit — #13: o botão Run de #/auto agora mostra um estado ocupado (is-loading + aria-busy + “Running…”) em vez de apenas se desabilitar. #14: uma requisição HTTP falha agora aflora uma mensagem i18n acionável sobre o passo E um toast (auto.httpFail com {n}), em vez de um seco “HTTP 500”. #18: o “Copy prompt” do modo manual agora usa a Clipboard API assíncrona com fallback execCommand, e emite um toast de falha real em vez de um falso “Copied”. #19: o contêiner do resultado de evaluate é agora role=status aria-live=polite, de modo que a longa chamada ao LLM é anunciada aos leitores de tela. #20: o botão Evaluate vai envolvido em UI.withSpinner (era um onClick: run simples que permitia envios duplicados). 3 novas chaves i18n × 8 idiomas; +6 testes: 691 → 697. Também uma correção apenas de testes (commit 7f8e250): o teardown de e2e pipeline-delete estava no caminho do confirm nativo anterior à v1.48; migrado para DELETE por API (fix(test): … — o Playwright-e2e de CI estava vermelho; não é uma regressão do produto). Detalhe em CHANGELOG.md.


[1.50.0] — 2026-05-18

WS2 #12 + #27 + #28 — acessibilidade da navegação da ajuda. Três achados da auditoria de UX em #/help sobre um guia de 17 seções e 90+ cabeçalhos, corrigidos em help.js. fix(a11y): help — single h1, labelled+filterable TOC, focus-on-anchor, back-to-top — #28: o markdown do documento abria com seu próprio # Title, produzindo um SEGUNDO <h1> numa página cujo cabeçalho já fornece o h1 canônico; agora todo <h1> do artigo é removido, de modo que há exatamente um h1 e a hierarquia começa limpa nas seções <h2>. #27: o <nav> do TOC era um ponto de referência sem nome (dois <nav> sem rótulo na página); agora tem aria-label (help.toc), e ao clicar numa entrada do TOC o foco se move para o cabeçalho da seção (tabindex=-1 + focus()), não apenas a rolagem do viewport. #12: não havia como encontrar nada num documento longo; um filtro type=search acima do TOC reduz as entradas por texto de cabeçalho ao vivo, e um botão flutuante com aria-label “Back to top” aparece após rolar, volta ao início e devolve o foco ao <h1> da página; seu listener de scroll é removido no hashchange ao sair de #/help. 2 novas chaves i18n × 8 idiomas — help.tocFilter, help.backToTop; +6 testes: test: tests/help-nav-a11y.test.mjs. 685 → 691. Detalhe em CHANGELOG.md.


[1.49.0] — 2026-05-18

WS2 #10 + #11 + #25 + #26 — acessibilidade e ordenação da tabela do tracker. Quatro achados da auditoria de UX em #/tracker, corrigidos em tracker.js. fix(a11y): tracker headers, sortable table, localized fix labels, empty state — #10: o cabeçalho da coluna de ação era uma string vazia e o botão Report por linha não tinha contexto; agora todo <th> tem scope=col, o cabeçalho de ação e os de Score/PDF estão com chave i18n (estavam vazios ou em inglês codificado), e o botão Report ganha um aria-label com a empresa (<report> — <company>). #11: um tracker sem como ordenar; os cabeçalhos Date / Score / Status são agora botões de ordenação operáveis por teclado dentro do <th> com aria-sort (none/ascending/descending); um comparador sorted() (numérico para score, comparação de locale para date/status) roda antes da paginação, e o clique alterna a direção e reinicia o paginador. #25: track.normalize/dedup/merge eram inglês idêntico nos 8 idiomas apesar de serem os controles destrutivos de maior risco (reescrevem data/applications.md no lugar) — agora corretamente localizados, mais um title de dica. #26: a primeira execução com zero linhas mostrava a mesma mensagem “no match” que uma lista superfiltrada; rows.length === 0 agora renderiza um estado vazio distinto (título + corpo + CTA “Open pipeline”). 7 novas chaves i18n × 8 idiomas + 3 relocalizadas; +6 testes: test: tests/tracker-a11y-sort.test.mjs. 677 → 683. Detalhe em CHANGELOG.md.


[1.48.0] — 2026-05-18

WS2 #8 + #22 — pipeline: confirmação com foco preso + acessibilidade da pré-visualização. Dois achados da auditoria de UX em #/pipeline, corrigidos em pipeline.js. fix(a11y): pipeline UI.confirm() + live preview region — #8: as três ações de #/pipeline usavam confirm() nativo (sem foco preso): o Delete do painel de pré-visualização, o de cada linha e “Evaluate first”; agora todas passam pelo UI.confirm() com foco preso (infra v1.44.0) — as duas exclusões danger:true (Cancelar por padrão), “Evaluate first” danger:false; não resta nenhum confirm() nativo em pipeline.js. #22: previewPane não tinha papel ao vivo e uma falha de fetch era enfiada em previewBody, renderizada como uma <pre> “preview” enganosa; agora é role=region aria-live=polite com um aria-label, e as falhas definem um previewError à parte renderizado como um bloco role=alert distinto (limpo ao (re)selecionar e ao excluir a linha ativa). 4 novas chaves i18n × 8 idiomas; +5 testes: test: tests/pipeline-confirm-preview.test.mjs. 672 → 677. Detalhe em CHANGELOG.md.


[1.47.0] — 2026-05-18

WS2 #7 + #30 + #31 + #16 — varredura de acessibilidade de rótulos não vinculados. Quatro achados da auditoria de UX em que controles de formulário não tinham rótulo programático (WCAG 1.3.1 / 3.3.2 / 4.1.2), agora vinculados. fix(a11y): bind every swept form control to an accessible name — #7 scan.js: o checkbox dry-run e o dropdown company-select tinham rótulos sem for; adicionado htmlFor (com os id existentes). #30 deep.js: os inputs company / role tinham rótulos não vinculados; adicionado id + htmlFor (deep-company, deep-role). #31 apply.js: url / jd tinham rótulos não vinculados; adicionado id + htmlFor (apply-url, apply-jd). #16 cv.js: o <textarea> principal de markdown não tinha nome acessível; vinculado via aria-labelledby ao título visível “Markdown” — nome para leitores de tela idêntico ao título na tela, sem nova chave i18n. Usa o padrão explícito label[for]control[id] já padrão em batch.js / mode-page.js; sem novas chaves i18n; zero mudança de comportamento. +5 testes: test: 667 → 672. Detalhe em CHANGELOG.md.


[1.46.0] — 2026-05-18

WS2 #5 + #6 + #21 + #24 — acessibilidade do SSE de scan. Quatro achados da auditoria de UX em #/scan, corrigidos em scan.js. fix(a11y): scan SSE — live-log region, Stop, run-state, error banner — #5: o console de streaming agora é role=log aria-live=polite (+ aria-label, tabindex=0, rolável por teclado), com uma região oculta visualmente à parte role=status assertiva que anuncia os eventos terminais (concluído / falhou / parado). #6: um botão Stop fecha o EventSource em voo (es.close()), cancela o polling de resultados e reinicia o estado; só é exibido enquanto um scan corre. #21: o botão Scan é desativado + recebe aria-busy enquanto corre e Stop aparece, em ambos os caminhos de stream (streamTo de uma fase e runScanAll multifase — este último só encerra a execução no done terminal, final !== false). #24: uma falha do SSE não é mais só um toast de 3,5 s; agora um banner persistente role=alert mostra o erro com uma ação de repetição (reinvoca a última função de execução), limpo na próxima execução. 8 novas chaves i18n × 8 idiomas; +7 testes: test: tests/scan-sse-a11y.test.mjs. 660 → 667. Detalhe em CHANGELOG.md.


[1.45.0] — 2026-05-18

WS2 #3 — abas de #/config: padrão WAI-ARIA Tabs completo. As três abas de #/config (API keys / Profile / Modes) eram <button class="tab-btn"> simples com ativação apenas por clique: sem role, sem aria-selected, sem modelo de teclado (UX-audit HIGH #3, WCAG 4.1.2 / 2.1.1). fix(a11y): config.js tabs implement role=tablist/tab/tabpanel — agora um contêiner role=tablist com aria-label; cada aba role=tab + id + aria-controls + aria-selected (sincronizado em activate()) + tabindex itinerante (0 ativa / -1 demais); o painel role=tabpanel + tabindex=0 + aria-labelledby acompanhando a aba ativa. Navegação de teclado completa: ←/→/↑/↓ (com envolvimento) + Home/End movem o foco E ativam. O gancho CSS legado .tab-btn.is-active é preservado. +1 chave i18n × 8 idiomas (config.tablistLabel); +7 testes: test: tests/config-tabs-aria.test.mjs. Além disso, uma correção apenas de testes: fix(test): retarget 2 stale auto-pipeline smoke tests — dois smoke tests de Playwright-e2e anteriores ao v1.34 afirmavam um modal transitório que o botão “Auto-pipeline” do dashboard deixou de abrir no v1.34.0 (→ Router.go('/auto')); estavam vermelhos no job de CI Playwright-e2e separado. Reapontados para a tela #/auto. 653 → 660. Detalhe em CHANGELOG.md.


[1.44.0] — 2026-05-18

WS2 #4 + #9 — confirmação com foco preso antes de sobrescritas destrutivas de arquivos do projeto pai. Dois HIGH da auditoria de UX, ambos com perda de dados: (#4) config.js saveProfileRaw/saveModesRaw substituía o config/profile.yml / _profile.md inteiro do pai sem confirmação; (#9) tracker.js Normalize/Dedup/Merge reescrevia o data/applications.md do pai in loco sem confirmação. fix(a11y/safety): UI.confirm() gate before whole-file parent overwrites — novo UI.confirm() em public/js/api.js, um diálogo com foco preso que reutiliza a infraestrutura modal WAI-ARIA existente (um hook _onClose faz Esc / backdrop / × / Cancel resolverem todos false; o foco recai por padrão em Cancel; retorna Promise<boolean>; NÃO o confirm() nativo). As três chamadas destrutivas agora ficam protegidas antes da escrita. 8 novas chaves i18n × 8 locais (o marcador {op} é preservado verbatim); +8 testes: test: tests/confirm-gate.test.mjs, 644 → 652. Detalhe em CHANGELOG.md.


[1.43.0] — 2026-05-18

Solicitado pelo usuário — career-ops-ui open + autostart trazendo o navegador para frente. Após setup/run, um open/xdg-open pelado deixava a aba do dashboard em segundo plano quando o navegador já estava aberto, obrigando o usuário a procurá-la. feat(cli): career-ops-ui open — open AND raise the dashboard tab — o novo scripts/open-dashboard.mjs constrói a URL a partir de HOST/PORT (reescrevendo um bind 0.0.0.0 para loopback), opcionalmente espera por /api/health, abre o navegador padrão e então o força para frenteosascript no macOS ativando o que estiver rodando entre Chrome/Brave/Edge/Safari/Arc/Firefox, xdg-open+wmctrl no Linux, start no Windows. Exposto como o verbo career-ops-ui open (aliases dash, focus). O autostart de bin/start.sh agora delega a ele para que a aba seja trazida para frente automaticamente; NO_OPEN=1 desativa o auto-open em inicializações headless/CI. README ×8 + help §1 ×8 atualizados; +8 testes: test: tests/open-dashboard.test.mjs, 636 → 644. Detalhe em CHANGELOG.md.


[1.42.0] — 2026-05-18

WS2 correção nº 2 — rota morta #/portals → deep-link para config. #/portals era uma rota não registrada que renderizava a view 404, embora seja uma URL plausível de favorito/digitação para gerenciar fontes de portais (HIGH nº 2 da auditoria de UX). fix(router): #/portals 404 → alias to config + Regional-sources deep-link — adicionado portals: 'config' ao ALIASES do router.js (mesmo padrão de estabilidade de favoritos que settings→profile), então agora resolve para a view config com o item de navegação config ativo. Quando existe um grupo Regional-sources, a view (config.js) detecta o hash #/portals, força a abertura desse grupo <details>, rola-o até a vista e move o foco para seu summary (sobrepondo o foco h1 padrão), de modo que o usuário aterrissa exatamente nos controles de fontes de portais; nunca renderiza um grupo regional vazio só pelo alias. help-bundle §5 × 8 ganhou uma nota de atalho; +1 teste de router: test(router): portals→config alias guarantee em router.test.mjs, 635 → 636. Detalhe em CHANGELOG.md.


[1.41.0] — 2026-05-18

WS2 — auditoria sênior de UX/usabilidade + correção transversal de gestão de foco. Uma auditoria heurística de mais de 10 anos (Nielsen × WCAG 2.2 AA × convenções do projeto) das 17 rotas produziu uma fila de 40 achados ordenada por severidade (.planning/.../UX-AUDIT.md); HIGH→MEDIUM→LOW são entregues agora uma correção por release. Esta release aterrissa o HIGH transversal nº 1. Correções: fix(a11y): move focus to the new view on every route changerouter.js render() substituía #content a cada hashchange mas nunca movia o foco, então usuários de teclado / leitor de tela ficavam no nó destruído e perdiam o lugar (WCAG 2.4.3 Focus Order / 4.1.3 Status Messages — transversal, afetava as 17 telas); o novo focusNewView(content) foca o primeiro h1/.page-title da nova view (anúncio SR conciso + ordem de foco correta), tornando o cabeçalho focável (tabindex=-1) se preciso e recorrendo a #content; o primeiro paint é pulado para não brigar com o skip-link; ligado nas rotas de render de sucesso e erro; verificado ao vivo: após navegar, document.activeElement é o H1.page-title da nova view. Testes: test(router): focus-management static guarantees — 4 casos em router.test.mjs (helper definido, alvo-cabeçalho + fallback para content, guarda de pulo no primeiro paint, ≥2 pontos de chamada); 631 → 635. Detalhe em CHANGELOG.md.


[1.40.0] — 2026-05-18

WS8.3 — varredura de atualização de docs + correção de career-ops-ui help + endurecimento de askSecret. Correções: fix(cli): career-ops-ui help no longer leaks shell source — o dispatcher imprimia seu comentário de cabeçalho com sed -n '2,12p', mas a linha 12 (set -euo pipefail) é código, não comentário, então career-ops-ui help (e o texto de uso de verbo desconhecido) terminava com uma linha set -euo pipefail perdida; restringido a 2,11p (o bloco de comentário) nos casos help e *); help sai 0, verbo desconhecido sai 2 — verificado. fix(cli): scripts/init.mjs key entry never echoes — o follow-up de v1.39.0 substituiu a máscara cosmética de readline por um leitor real em modo raw: setRawMode(true) + linha com buffer para que os bytes de chave digitados/colados nunca cheguem ao terminal (sem vazamento em scrollback / tmux / compartilhamento de tela); um FSM completo de escape VT consome cada sequência CSI/SS3/OSC/DCS/SOS/PM/APC para que as teclas de seta e função não corrompam o segredo; stdin é injetado por dependência, então o fallback não-TTY é testado unitariamente sem mexer no global; iterado até um LGTM limpo da revisão IA. Documentação: README ×8 — a antiga seção “instalação em um comando” é substituída por uma seção destacada “Iniciar e inicializar em um comando” (o one-liner de curl mais a cadeia explícita do CLI career-ops-ui: clone → npm linksetupinitdoctorrunhelp, a explicação do assistente de provedor, a forma CI --provider --anthropic-key --yes e a nota de LLM_PROVIDER); os 8 badges de README atualizados de v1.22–v1.24 / tests-461–474 para v1.40.0 / tests-631 (badge e2e tornado não numérico para evitar uma contagem inventada); help-bundle ×8 §1 — um callout “Lançamento e init em um comando” adicionado ao topo do manual de início rápido (antes de “A. Setup”) nos 8 idiomas; paridade de seções H2 preservada (17 cada — gate de CI verde). Testes: test(init): non-TTY askSecret fallbackprovider-selector.test.mjs ganha um caso de stdin por DI verificando que askSecret delega ao ask() simples (paridade de trim) fora de um TTY sem mutar o global compartilhado; 629 → 631. Detalhe em CHANGELOG.md.


[1.39.0] — 2026-05-18

WS8.2 — seletor de provedor LLM + chave OpenAI/Codex + assistente init interativo. LLM_PROVIDER (auto|claude|gemini) + OPENAI_API_KEY no env-config (secreto). providerOrder() consultado pelos 6 gate-sites do llm.mjs via _provGate(); sem mudança para auto. Select + campo em #/config. scripts/init.mjs agora é assistente real (grava parent .env pela rota validada). 7 testes. 622 → 629. README ×8 / fold canônico = WS8.3/WS10. Detalhe em 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 o motor /api/health exato (createApp in-process → relatório terminal); exit 0 só se todos os checks OBRIGATÓRIOS passarem. docs/sdd + help §1 ×8. 6 testes. 616 → 622. README quickstart ×8 = WS8.3. Detalhe em CHANGELOG.md.


[1.37.0] — 2026-05-17

WS7 — revisão AI pré-commit no workflow git. Floor determinístico (fail-HARD): bloqueia .env/segredos staged, padrões de chave no diff, .also( em views staged, falha node --check. Camada AI (fail-SOFT): claude -p sobre o diff se o CLI existir e AI_REVIEW != off. .githooks/pre-commit + prepare. Nunca --no-verify. docs/sdd. 6 testes. 610 → 616. Detalhe em CHANGELOG.md.


[1.36.0] — 2026-05-17

WS6.3 — aba Modes: blob bruto → editor por seção. WS6 completo. modes/_profile.md editado por seção ## (um textarea recolhível por cabeçalho). splitProfileSections byte-exato; PUT { sections } faz merge só das seções nomeadas — preâmbulo + seções alheias + ordem preservados byte a byte. Cabeçalho desconhecido → 400. Rota raw intacta. i18n 5 chaves ×8. help §2 ×8. 6 testes novos. 604 → 610. WS6 concluído. Detalhe em CHANGELOG.md.


[1.35.0] — 2026-05-17

WS6.4 — editores de arrays do Profile + auditoria WS6.2 de API-keys. PUT /api/profile aceita { arrays } (combinável com { fields }): Target roles/Superpowers (listas), Archetypes (name/level/fit), Proof points (name/url/hero-metric). Mesma garantia merge-not-replace; linhas vazias descartadas; lista vazia remove a chave. 4 editores add/remove em #/config. i18n 6 chaves ×8. Auditoria: KNOWN_KEYS ≡ FIELDS, sem gap. 7 testes novos. 597 → 604. Detalhe em CHANGELOG.md.


[1.34.0] — 2026-05-17

WS5 — tela Auto-pipeline de um clique (#/auto). O modal virou página dedicada e linkável. Um clique roda validar→buscar→avaliar→salvar relatório→tracker via SSE. Stepper acessível, deep-links, modo manual sem key, linkável #/auto?url=…&go=1. Entrada no sidebar; botão ✨ do dashboard agora vem aqui. i18n 14 chaves ×8. help §1 ×8 + README ×8. 8 testes novos. 589 → 597. Detalhe em CHANGELOG.md.


[1.33.0] — 2026-05-17

WS4 — auditoria de paridade com career-ops 1.8.0 + location_filter. O scan.mjs do projeto pai ganhou location_filter (#570); os scanners in-process do web-ui não delegam a ele. Novo server/lib/location-filter.mjs replica a semântica verbatim; integrado nos dois scanners. Doc help §5 ×8. 8 testes novos. 581 → 589. Detalhe em CHANGELOG.md.


[1.32.0] — 2026-05-17

Aba Profile de #/config — blob YAML bruto → formulário por campos (WS1). 3 seções recolhíveis (Candidato / Narrativa / Remuneração), 14 caminhos escalares. Save por campos faz merge em config/profile.yml: arquétipos, proof points e chaves próprias preservados intactos. Escape-hatch raw-YAML mantido em Advanced (preserva comentários). 23 chaves i18n ×8. 7 testes novos. 574 → 581. Detalhe em CHANGELOG.md.


[1.31.0] — 2026-05-17

Sync com career-ops 1.8.0 — #/batch expõe --model + --start-from. O projeto pai subiu 1.7.1 → 1.8.0; batch-runner.sh ganhou --model NAME (#504) e --start-from N. web-ui os expõe em #/batch (campos Modelo e A partir de #) com validação defense-in-depth no servidor. i18n ×8. 7 testes novos. 567 → 574. Detalhe completo em CHANGELOG.md.


[1.30.0] — 2026-05-14

Paginador em #/scan — substitui o truncamento «primeiros 200 de N» de v1.12.

Pré-v1.30 a tabela de resultados de scan era truncada nas primeiras 200 linhas filtradas com a nota «Showing first 200 of N» no rodapé. As linhas 201..N eram inacessíveis pela UI. v1.30.0 troca o cap por UI.paginate (mesmo helper de #/tracker / #/reports / #/activity). PAGE_SIZE = 200 preserva a densidade visual anterior; ordenação boosted-to-top estável entre páginas (ordena o conjunto COMPLETO antes de paginar); reset automático para página 1 ao mudar filtros. Chave i18n obsoleta scan.shownTop removida (×8 locales). 9 novos casos em tests/scan-paginator.test.mjs (7 canários estáticos + 1 tabela lógica com 6 casos limite + 1 cálculo do resumo). 558 → 567 unit + acceptance (+9). Detalhes completos em CHANGELOG.md.


[1.29.2] — 2026-05-14

Hot-fix: 🌐 Scan com source=both rodava apenas a fase EN. A fase RU era descartada silenciosamente.

O cliente SSE (public/js/api.js:156) fechava o EventSource no PRIMEIRO evento done, mas o servidor emite um por fase em source=both. A fase RU iniciava e era imediatamente cancelada. Fix: servidor marca cada done com final: true|false; cliente fecha apenas quando final !== false. Retrocompatível — produtores de fase única sem final continuam fechando como antes. 547 → 558 unit + acceptance (+11 novos). Detalhes completos em CHANGELOG.md.


[1.29.1] — 2026-05-14

Guia detalhado do usuário para configurar os 5 portais RU no help-bundle §5, em todas as 8 locales.

Nova subseção “Configurar os portais russos — guia detalhado” dentro de §5 (Portals & sources): tabela inventário das 5 fontes com auth e restrições geográficas, passo-a-passo para localizar e editar portals.yml, exemplo YAML completo das 5 fontes, colisão com lista negativa com correção, como desabilitar uma fonte, como verificar via 🌐 Scan + log SSE. §17 (shipped em v1.29.0) cobre o fluxo do desenvolvedor; §5 v1.29.1 cobre o fluxo do usuário final. 540 → 547 unit + acceptance (+7 novos). Detalhes completos em CHANGELOG.md.


[1.29.0] — 2026-05-14

Scanner de portais russos passa de 2 a 5 fontes; registry + dropdown dinâmico; nova seção §17 “Como adicionar um novo portal”.

  • 3 novos adapters RU: Trudvsem (API open-data do governo, sem auth nem geo-gate), GetMatch e GeekJob (HTML scrape com parser defensivo — [] se não parsear, nunca throw em 200 saudável).
  • Source registry em server/lib/sources/registry.mjs — única fonte da verdade consumida por dispatcher + endpoint + dropdown. Pré-v1.29 a lista vivia hardcoded em TRÊS lugares.
  • Novo endpoint GET /api/scan/sources com Cache-Control: max-age=60 — o SPA reconstrói o dropdown ao montar #/scan.
  • Help-bundle §17 nova nas 8 locales: “Como adicionar um novo portal” (template de adapter, entry do registry, dispatcher, teste mockado, portals.yml).
  • russian_portals.sources default muda de ["hh", "habr"] para as 5 fontes; se o seu portals.yml já lista sources: explicitamente, você deve adicionar as 3 novas manualmente.
  • Testes: 520 → 540 (+20). Detalhes completos em CHANGELOG.md.

[1.28.1] — 2026-05-14

Hot-fix: router 404 com hashes que levam ?query. Linha HH_USER_AGENT removida do health.

Pré-v1.28.1 Router.go('/evaluate?url=…') produzia um hash cujo primeiro split('/') era o literal "evaluate?url=…", que nunca coincidia com uma rota registrada → __not_found__ (404). Fix de uma linha: hash.split('?')[0] antes do split do nome. Cobre dois cliques reportados: #/pipeline → ▶ e “App settings → Modes”. A linha opcional HH_USER_AGENT foi removida de /api/health (a dica 403-fora-da-Rússia continua no help-bundle §16 e é emitida em stderr durante o scan). 515 → 520 unit + acceptance (+ 5 novos). Detalhes completos em CHANGELOG.md.


[1.28.0] — 2026-05-14

Alinhamento de docs + novo controle --max-retries N em #/batch. Fecha as duas issues abertas levantadas por qa/QA-PROMPT-docs-vs-app.md.

  • Issue #2#/batch agora expõe um campo numérico “Max retries” (1–10), habilitado apenas quando “Retry failed” está marcado. O servidor faz parse + valida 1≤N≤10 (valores fora de faixa são descartados silenciosamente) e omite --max-retries sem --retry-failed. 7 casos de teste em tests/batch-max-retries.test.mjs. 2 chaves i18n novas × 8 locales.
  • Issue #1 — a lista de CLIs de IA nos 8 help-bundles e 8 READMEs alinha com o cânon de career-ops.org/docs (Claude Code · Codex · OpenCode · Qwen CLI), com uma frase localizada: «outras CLIs compatíveis com Claude também funcionam pela mesma superfície de slash-commands». O bullet “Multi-CLI” do README sobre os arquivos shim do web-ui é mantido (descreve outra superfície). 2 novos canários em tests/canonical-docs-coverage.test.mjs.
  • 506 → 515 unit + acceptance (+ 9 novos). Playwright 32/32 sem alterações. Detalhes completos em CHANGELOG.md.

[1.27.0] — 2026-05-14

Polimento cosmético + a11y: desduplicar a entrada #/dashboard da barra lateral.

Na barra lateral, o logo da marca (<a class="logo" href="#/dashboard">) e o primeiro item de navegação apontavam para a mesma rota. Leitores de tela anunciavam «Dashboard» duas vezes e usuários de teclado tinham um tab-stop redundante. O bloco da marca agora é um <div class="logo"> simples; o item de navegação continua sendo o único link para #/dashboard. 506 / 506 unit + 32 / 32 Playwright — sem alterações. Detalhes completos em 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 perdeu a declaração min-height: 44px em .btn; botões do header renderizavam a 39-41 px (violação WCAG 2.5.5). v1.26.1 restaura o piso de 44 px + flex-shrink: 0 + line-height: 1.2. 502 → 506 unit, 32/32 Playwright inalterados. Detalhe em CHANGELOG.md.


[1.26.0] — 2026-05-14

Pirâmide de testes + cobertura ≥ 93 % linha.

Adota a estrutura de 4 níveis (unit → functional → acceptance → e2e) conforme o backlog de v1.25. Adiciona 22 testes novos cobrindo os maiores gaps de v1.25 (jds.mjs 61.64 % → 100 %, ramos de rejeição em auto-pipeline). Introduz o diretório tests/acceptance/ para jornadas multi-endpoint. 480 → 502 unit + acceptance, 32/32 Playwright inalterados. Detalhe completo em CHANGELOG.md e docs/architecture/TESTING.md.


[1.25.0] — 2026-05-14

Curto-circuito manual do auto-pipeline + ajuste cosmético do dashboard + backfill de paridade do CHANGELOG. Fecha G-014 (auto-pipeline ignorava mode: 'manual'), G-012 (deriva de paridade do CHANGELOG — 6 locales estavam 2 releases atrás) e o duplo-glifo ✨ ✨ no dashboard. G-003 (renomeação de README.cn.md) já estava de fato encerrado — o repositório só tem README.zh-CN.md. G-005 (realinhamento de blocos do relatório A-G → A-F) exige um commit coordenado no projeto pai e segue adiado.

🛡️ G-014 — Curto-circuito de mode: 'manual' no auto-pipeline

  • fix(auto-pipeline): G-014 — honour mode:'manual' short-circuit (server/lib/routes/auto-pipeline.mjs:158-195) — antes da v1.25 a rota sempre chamava um LLM. Passar mode: 'manual' (espelhando /api/evaluate desde a v1.10.2) era silenciosamente ignorado e a requisição ficava pendurada de 1 a 3 min na Anthropic. Agora o handler:
    • Aceita mode E evalMode por retrocompatibilidade. Qualquer um dos dois com o valor 'manual' dispara o curto-circuito.
    • Emite todos os 5 estágios SSE com status: 'done' / status: 'skipped'. Sem fetch. Sem chamada de LLM. Sem $0.05 por requisição.
    • O payload de done traz { mode: 'manual', prompt: <esqueleto de buildEvaluationPrompt>, message } — o SPA pode renderizá-lo como o cartão de prompt manual já existente de /api/evaluate.
  • Fecha risco de DoS em HOST=0.0.0.0: antes, mesmo com llmRateLimit limitando 10 req/60s/IP, 10 atacantes × 10 reqs = $50/min queimando na Anthropic. O curto-circuito dispara antes do decremento do limitador contar como uma chamada real.
  • Testestests/auto-pipeline-manual-mode.test.mjs: 3 testes confirmam (1) mode: 'manual' retorna em < 2 s com todas as 5 chaves de etapa, (2) mesmo com ANTHROPIC_API_KEY definida o curto-circuito ainda dispara (o sintoma original), (3) chamadores legados em evalMode: 'manual' continuam funcionando.

📝 G-012 — Backfill de paridade do CHANGELOG (6 locales × 2 releases ausentes)

  • docs(changelog): backfill v1.23.0, v1.24.0, v1.24.1, v1.25.0 in 6 lagging locales — antes da v1.25 apenas EN tinha v1.23–v1.24; RU estava 1 release atrás, os outros 6 estavam 2 releases atrás. A v1.25 despacha agentes de tradução paralelos (espelhando o padrão da v1.23) para colocar as quatro entradas em CHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md. RU recebe v1.24.0 + v1.24.1 + v1.25.0 (já tinha v1.23.0 do ciclo da v1.23).
  • feat(ci): scripts/check-changelog-parity.mjs gate — falha o build se a entrada mais nova de qualquer CHANGELOG de locale for mais antiga que a canônica EN. Plugado em npm run test:ci. A deriva pré-existente do G-012 teria sido capturada no instante em que cruzasse a fronteira do EN.

✨ Cosmético — deduplicação do duplo-glifo no dashboard

  • fix(dashboard): dedup ✨ glyph in auto-pipeline button label (public/js/lib/i18n-dict.js:219) — dash.autoPipeline carregava um no início da string em cada locale E public/js/views/dashboard.js:58 prefixava outro na view. Resultado: o botão renderizava ✨ ✨ Auto-pipeline …. A v1.25 remove o glifo inicial da entrada DICT em cada locale; o prefixo da view é a fonte única. A mesma varredura de auditoria revisou o restante do pacote i18n — nenhum outro padrão de duplo-glifo foi encontrado.

🚫 Adiado para um release futuro

  • G-005 — Realinhamento de blocos do relatório A-G → A-F conforme career-ops.org/docs canônico — exige um commit coordenado no projeto pai santifer/career-ops (reescrita de modes/oferta.md para emitir A=Role, B=CV-match, C=Strategy, D=Comp, E=Personalization, F=STAR — remover C-Risks/G-Legitimacy como blocos separados). A v1.25.0 entrega o lado web-ui pronto para o novo schema (reports.js já aceita letras de bloco arbitrárias desde a v1.13). Rastreado para a próxima janela de release em que pai + filho possam aterrissar juntos.
  • G-003 — Renomeação de README.cn.mdREADME.zh-CN.md — verificado durante o preparo da v1.25: o repositório já tem README.zh-CN.md (nenhum README.cn.md órfão em qualquer lugar do worktree). O achado do G-003 estava obsoleto.

🧪 Testes

  • 477 → 480 unit (+3 do PR-B auto-pipeline-manual-mode.test.mjs).
  • 32/32 Playwright inalterado.
  • npm run test:ci agora roda npm test + check-no-also-leftovers.mjs + check-changelog-parity.mjs.

Verificação

$ npm run test:ci
# 480 / 480
# ✓ no .also( leftovers in views/
# ✓ CHANGELOG parity: all 8 locales at v1.25.0

# G-014 — modo manual retorna em < 2 s mesmo com ANTHROPIC_API_KEY definida:
$ 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  (era 1-3 min)
# event: start … event: step (×5) … event: done {"mode":"manual","prompt":"…"}

# G-012 — todo CHANGELOG de locale carrega a entrada v1.25.0:
$ grep -c '^## \[1.25.0\]' CHANGELOG*.md
# 8 arquivos, cada → 1

# Cosmético — glifo do dashboard:
$ grep "dash.autoPipeline" public/js/lib/i18n-dict.js
# Nenhum ✨ inicial em qualquer valor de locale (a view fornece o glifo único)

Mudanças incompatíveis

Nenhuma. mode: 'manual' é opt-in; chamadores legados em evalMode: 'manual' continuam funcionando sem alteração.

Fora de escopo (v1.26+)

Item Observações
G-005 — Realinhamento A-F de blocos do relatório Precisa de commit coordenado no projeto pai (santifer/career-ops reescreve modes/oferta.md).
Execução ao vivo dos sub-testes visuais do cenário 31 de QA Exigem agente dirigido por navegador (Claude Cowork). Parcialmente cobertos pelo smoke Playwright.
i18n-dict.js acima da meta de 400 LOC Fixture de tradução — isento por política. Dividir adicionaria requisições HTTP sem um bundler.

[1.24.1] — 2026-05-14

Hot-fix: crash em #/config nos 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) — o N-2 da v1.22.0 removeu o monkey-patch global de Element.prototype.also e migrou cv.js para um padrão de instrução livre, mas passou batido em config.js. Resultado: #/config crashava na primeira invocação em todos os locales com c(...).also is not a function. A v1.24.1 aplica o mesmo padrão de migração de cv.js:188-201 — extrai a árvore para um const root = c(...), executa o bloco de ativação por conta própria e então return root;.

🛡️ Gate de CI

  • feat(ci): scripts/check-no-also-leftovers.mjs sweep — percorre todo arquivo sob public/js/views/ e falha o build em qualquer chamada .also( (referências em comentário são permitidas). Plugado no novo script npm run test:ci. Uma futura reversão da remoção do monkey-patch não consegue reintroduzir a mesma regressão de forma silenciosa.

🧪 Testes

  • test: tests/config-view-syntax.test.mjs — três guardas:
    • parsear config.js via node:vm.Script (captura regressões em nível de sintaxe sem precisar de Playwright)
    • afirmar que nenhum .also( sobrevive fora de comentários
    • afirmar que as âncoras de migração const root = c(...) / return root; estão presentes
  • 474 → 477 unit (+3) + 32/32 Playwright inalterado.

Verificação

$ npm run test:ci
# 477 / 477
# ✓ no .also( leftovers in views/

# Smoke de navegador:
$ open http://127.0.0.1:4317/#/config
# → renderiza normalmente, sem cartão "is not a function". Equivalente em todos os locales.

Fora de escopo (adiado para v1.25)

  • G-014, G-012, G-005, G-003 — veja a entrada v1.25.0 acima para o pacote.

[1.24.0] — 2026-05-14

Atualização de profundidade de conteúdo do help-bundle + execução ao vivo do cenário 31 de QA + CHANGELOG RU de ponta a ponta. Fecha ambos os itens que a tabela “Fora de escopo” da v1.23.0 adiou para v1.24: o refresh completo de profundidade de conteúdo dos 8 help bundles a partir das 5 URLs canônicas de career-ops.org/docs (era só cobertura de URL desde a v1.11.x) e a execução ao vivo do cenário 31 de QA contra um servidor em execução (era “precisa de agente de navegador + credenciais LLM” — descobriu-se que 6/6 sub-testes são acessíveis via curl + grep, só os sub-testes visuais precisam de um navegador).

📖 Refresh de profundidade do help-bundle

  • docs(help): refresh en.md from 5 canonical career-ops.org/docs URLs (docs/help/en.md) — antes da v1.24 o bundle EN tinha 1113 linhas e listava as 5 URLs canônicas no front-matter mas não as expandia no corpo. A v1.24 faz fetch das 5 URLs via WebFetch e aprofunda as seções H2 correspondentes:
    • Sobre o career-ops (front-matter) — adicionados princípios (soberania de dados, agnóstico em IA, controlado por humanos), bloco “O que o career-ops NÃO é”, inventário de conceitos expandido de 6 para 10 linhas (acrescentados Proof points, JD store, Interview-prep, Batch additions).
    • §5 Portais — adicionado bootstrap canônico cp templates/portals.example.yml portals.yml, esclarecidos campos obrigatórios vs opcionais por entrada de tracked_companies.
    • §7 Scan — adicionada nota “nenhum token de IA consumido” para a Opção A, lista de comandos de follow-up (apply / contacto / deep / tracker).
    • §14 Checklist de apply — dividido em modo checklist do SPA vs fluxo Manual-vs-Playwright-assistido vs fluxo CLI completo (8 passos numerados canônicos desde /career-ops apply <company> até Submitted. com transição automática Evaluated → Applied); a subseção de batch evaluate agora tem tabela com schema TSV + todas as 4 flags documentadas + merge-tracker.mjs --dry-run; a subseção de Setup do Playwright lista comandos de instalação, registro MCP, alternativa .claude/settings.local.json, nota de headless-by-default.
  • Paridade de 16 seções H2 preservada (o teste de CI help-ui.test.mjs::section-parity afirma exatamente 16 seções H2 em todos os 8 locales).
  • Cada uma das 5 URLs canônicas aparece ≥ 2 vezes no bundle (o teste de CI canonical-docs-coverage.test.mjs impõe). Contagem por URL após 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 tradução paralelos despachados. Cada locale alvo (es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW) recebe um bundle refrescado que espelha a estrutura EN seção por seção, preserva verbatim blocos de código / URLs / caminhos de arquivo / rótulos de botão (📁 Upload CV / 🌐 Scan now / ▶ Evaluate / 📄 Generate PDF / 💾 Save) e abreviações em inglês (CSP, SSRF, TOCTOU, WCAG, ATS, JD, SSE, REST, API), e traduz a expansão para estilo técnico nativo de qualidade editorial na língua-alvo.

🧪 Cenário 31 de QA — execução ao vivo (6/6 PASS)

  • docs(qa): append last-verified live-execution log to qa/claude-cowork-browser-test-prompt.md — antes da v1.24 o cenário 31 estava documentado mas nunca havia sido rodado contra um servidor ao vivo (adiado como “precisa de agente de navegador + credenciais LLM”). A v1.24 rodou todos os 6 sub-testes contra http://127.0.0.1:4317:

    Sub Descrição Status
    31.1 Limiares de score nos help bundles ✅ PASS (4.5 × 3, 4.0 × 9, 3.5 × 6 menções em docs/help/en.md)
    31.2 Endpoints do workflow 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 (corpo contém career-ops apply + aviso auto-submit)
    31.4 Endpoint /api/batch ✅ PASS (chaves [exists, runnerExists, raw, rows, additions])
    31.5 Disponibilidade do Playwright ✅ PASS (/api/health reporta Playwright (parent node_modules) ok: true, value: installed)
    31.6 Cobertura de URL do help-bundle (5 URLs × 8 locales) ✅ PASS (40 / 40 ✓)

    Sub-testes somente-visuais (exigem navegador) sinalizados separadamente no prompt de QA — seguem executáveis via Claude Cowork ou npm run test:e2e:browser.

🌐 CHANGELOG RU de ponta a ponta (follow-up do M-9)

  • docs(translate): CHANGELOG.ru.md retry agent — full body translation (CHANGELOG.ru.md) — o release v1.23.0 saiu com o agente de retry do CHANGELOG RU ainda em voo (havia crashado uma vez com erro de socket e foi redespachado). A v1.24 incorpora a tradução completa do agente em 1542 linhas: cada entrada de v1.23.0 → v1.6.0 ganha um corpo russo de qualidade editorial, sem mais stop-gaps com corpo em EN. A disciplina de estilo casa com o passe de qualidade dos READMEs da v1.22.0: “функциональность” / “возможности” / “поведение” substituem o desajeitado “функционал”; “через” / “с помощью” substituem “при помощи”; voz ativa sobre passiva; “эндпоинт”, “лимит запросов”, “состояние гонки”, “санитайзинг” como termos canônicos; abreviações em inglês (TOCTOU, CSP, SSRF, WCAG, ATS, JD, SSE, REST, API) preservadas.

🧪 Testes

  • 474 / 474 unit + 20 / 20 smoke E2E + 32 / 32 Playwright. Zero delta comportamental em testes; cada asserção de CI do help-bundle (16 seções H2 × 8 locales, 5 URLs × ≥ 2 menções, content floor) segue verde.

Verificação

$ npm test                            # 474 / 474

# Aprofundamento do help-bundle:
$ wc -l docs/help/en.md
# ~1270 linhas (era 1113 — aprofundado, não inchado)

$ 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

# Cenário 31.6 — cobertura 40/40 de URLs:
$ 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

Mudanças incompatíveis

Nenhuma.

Fora de escopo (v1.25+)

Item Observações
Execução ao vivo dos sub-testes visuais do cenário 31 Exigem agente dirigido por navegador (Claude Cowork ou npm run test:e2e:browser). Fora do escopo da execução só-curl; coberto pelo smoke Playwright existente.
Tradução de corpo do CHANGELOG RU das entradas mais antigas (v1.5.x e abaixo) O agente de retry só cobriu de v1.6.0 em diante. Entradas pré-v1.6 (v1.5.x, etc.) — se já existiram — permanecem como conteúdo pré-existente.
Regressão visual em screenshots do dashboard após mudanças futuras no SPA scripts/capture-dashboard-screenshots.mjs regenera PNGs por locale; nenhum diff automatizado atualmente.

[1.23.0] — 2026-05-14

Split de i18n + correção de CI do connection-banner + screenshots localizados do dashboard + cada stop-gap do backlog encerrado. Entrega os três itens que a tabela “Fora de escopo” da v1.22.0 sinalizou para v1.23 (corpos do CHANGELOG por locale do M-9, split de LOC do i18n.js do N-1, auditoria de conteúdo do help-bundle) mais um hot-fix do teste E2E smoke que deixou o CI da main da v1.22.0 vermelho.

🚑 Hot-fix de CI — recuperação do connection banner

  • fix(client): reset health-poll cadence + visibilitychange eager re-check (public/js/api.js:21-91) — o backoff exponencial do M-6 da v1.22.0 estava correto (3 s → 6 s → 12 s → cap 15 s, abaixo do cap original de 60 s) mas o setTimeout em voo ficava preso ao atraso que tivesse sido definido anteriormente. Um servidor morto em t=0,1 com o primeiro ping em t=3 falharia, dobraria o atraso para 6, e a próxima sondagem de recuperação só dispararia em t=9. O “Flow 2a: connection banner appears on server down, hides on recovery” do smoke E2E esperava apenas 4 s e ficava vermelho na main.

    A v1.23.0 remodela o loop de polling:

    • _healthHandle é rastreado para que setConnectionState(lost=true) possa fazer clearTimeout e reagendar com _HEALTH_MIN. A primeira sondagem de recuperação agora dispara dentro de 3 s da queda, independente do atraso enfileirado.
    • _HEALTH_MAX reduzido de 60 s para 15 s. Aba em background contra servidor morto ainda recupera dentro de um ciclo de polling quando você retorna; a economia de banda continua substancial.
    • document.addEventListener('visibilitychange') re-checa proativamente quando a aba recupera o foco e connectionLost === true — Cmd-Tab de volta não espera o próximo tick de backoff.

🧹 N-1 — Split de i18n.js (acima da meta de 400 LOC)

  • refactor(client): split DICT into i18n-dict.js (data) + i18n.js (logic) — antes da v1.23 public/js/lib/i18n.js tinha 639 LOC. O grosso (linhas 23-586) era a tabela de tradução DICT — puro dado estruturado. A v1.23.0 extrai isso para public/js/lib/i18n-dict.js (578 LOC, isento da regra de LOC por CLAUDE.md “Exempt from these limits: generated files, migrations, test fixtures, lock files, vendored code” — tabelas de tradução qualificam como fixtures), deixando public/js/lib/i18n.js com 86 LOC de pura lógica de módulo (bem abaixo da meta de 400 LOC).
  • Contrato do loader: i18n-dict.js popula window.__I18N_DICT = { … }, depois i18n.js o lê dentro da IIFE existente. public/index.html os carrega em ordem — i18n-dict.js antes de i18n.js — para que a IIFE veja um DICT totalmente populado no instante da construção. Fallback para DICT ausente: toda chamada t() retorna seu fallback inline ou a chave nua, o que faz uma má configuração emergir ruidosamente sem crashar o SPA.
  • Encanamento de teste atualizado: tests/i18n-coverage.test.mjs, tests/help-ui.test.mjs, tests/canonical-docs-coverage.test.mjs agora rodam ambos os arquivos no contexto VM de teste (ou concatenam seus fontes para a varredura por regex), preservando todas as asserções existentes.

🌐 M-9 — Traduções de corpo do CHANGELOG por locale

  • docs(translate): 7 non-EN CHANGELOG files end-to-end — antes da v1.23 CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md carregava notas stop-gap com corpo em EN para toda entrada a partir de v1.13.0, com um rodapé apontando os leitores para o EN canônico. A v1.23.0 despacha 7 agentes de tradução paralelos — um por locale — que reescrevem cada corpo para estilo técnico de qualidade editorial na língua-alvo. Notas stop-gap removidas. Blocos de código, caminhos de arquivo, URLs, strings em estilo de mensagem de commit (fix(security): B-1 — …), variáveis de ambiente e rótulos de link preservados verbatim em todos os locales.

🖼️ Screenshots localizados do dashboard em cada README

  • docs(readme): wire each locale README at its locale-specific PNG — antes da v1.23 só README.pt-BR.md referenciava dashboard-pt-BR.png; os outros 6 READMEs não-EN ainda apontavam para dashboard-en.png. Os screenshots (já capturados no ciclo da v1.22.0 por scripts/capture-dashboard-screenshots.mjs) estavam presentes em images/ mas sem uso. A v1.23.0 atualiza cada README.{es,ja,ko-KR,ru,zh-CN,zh-TW}.md linha 14 para seu próprio dashboard-<locale>.png.

🧪 Testes

  • Os mesmos 474 / 474 unit + 32 / 32 Playwright da v1.22.0. Smoke E2E agora 20 / 20 (era 19 / 1 falha na main após v1.22.0 por causa da regressão de recuperação do banner; o reagendamento da v1.23.0 fecha isso).
  • Três testes existentes religados para lidar com o split do i18n. Zero novos arquivos de teste; zero asserções existentes removidas.

Verificação

$ npm test
# 474 / 474

$ npm run test:e2e
# passed: 20    failed: 0    (era 19/1 na main da v1.22.0)

$ wc -l public/js/lib/i18n.js public/js/lib/i18n-dict.js
#       86 public/js/lib/i18n.js          ← lógica, abaixo da meta
#      578 public/js/lib/i18n-dict.js     ← fixture de dados, isento

$ grep -h 'dashboard-' README*.md | sed -E 's/.*(dashboard-[^)]+).*/\1/' | sort -u
# dashboard-en.png    (somente 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

# Sanidade da tradução do CHANGELOG: cada arquivo de locale > 200 linhas de conteúdo nativo
$ wc -l CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md | grep -v total

Mudanças incompatíveis

Nenhuma. public/index.html agora carrega dois scripts onde carregava um — quem serve o SPA por um CDN precisa pegar i18n-dict.js; a ordem de carregamento dos scripts é imposta pela ordem das tags <script src> em index.html. O fallback de runtime (DICT vazio → t() retorna o fallback inline EN) impede crashes fatais quando o arquivo novo está ausente.

Fora de escopo (v1.24+)

Item Observações
Refresh de profundidade de CONTEÚDO do help-bundle a partir de career-ops.org/docs (vs cobertura de URL) As 5 URLs canônicas já aparecem no help bundle de cada locale desde a v1.11.x e o Cenário 31.6 no prompt de QA verifica a cobertura. O refresh de profundidade do corpo é candidato a v1.24+.
Execução ao vivo do cenário 31 de QA contra um servidor em execução Exige agente de navegador + credenciais LLM ao vivo. Candidato a v1.24.
Varredura por componente de alvo de toque nos novos parágrafos de dica da mode-page A v1.22.0 M-1 adicionou elementos <p class="field-hint"> que não foram verificados contra o min-height do WCAG 2.5.5 em todos os 8 locales.

[1.22.0] — 2026-05-14

Limpeza do backlog M/L/N + alinhamento de docs + passe de qualidade nas traduções. Toda a faixa medium-and-below do v1.20.1-BACKLOG.md foi entregue em um único release: nove itens M, cinco itens L e dois nits. Soma-se uma auditoria de alinhamento com os cinco guias canônicos de career-ops.org/docs, prompts de sistema renovados em .claude/ e .github/ e READMEs revisados em qualidade nos 7 locales não-EN.

🛡️ Hardening de segurança (defesa em profundidade)

  • fix(security): M-4 — stripDangerousMarkdown ciente de entidades (server/lib/security.mjs) — antes da v1.22, o regex casava <script>, javascript: e on*= como substrings literais. &lt;script&gt;, java&#115;cript: e <img src="data:image/svg+xml,<svg onload=…>"> passavam batido. Agora o saneador decodifica &lt;, &gt;, &amp;, &quot;, entidades numéricas (&#NN;) e hex (&#xHH;) antes de aplicar o regex de remoção. Validado por 11 testes em tests/cv-xss-bypasses.test.mjs. A defesa real continua sendo o pipeline client-side UI.md escape-first; isso reforça o arquivo em repouso.

  • fix(security): L-2 — bash --noprofile --norc no batch runner (server/lib/routes/batch.mjs:108) — spawn('bash', [PATHS.batchRunner, ...]) herdava o ~/.bashrc do usuário. Um rc-file hostil poderia influenciar a execução. Agora spawn('bash', ['--noprofile', '--norc', PATHS.batchRunner, ...]).

🔒 Resiliência

  • fix(client): M-6 — backoff exponencial no health ping (public/js/api.js:22-48) — o poller no estado desconectado disparava 28.800 fetches contra um servidor caído durante a noite. Agora 3 s → 6 s → 12 s → 24 s → 60 s; volta a 3 s no primeiro 2xx. A configuração usa uma cadeia de setTimeout (em vez de setInterval) para que cada passo respeite o novo atraso.

  • fix(client): M-5 — proteção de localStorage no modo privado do Safari (public/js/lib/i18n.js:572-583) — o modo privado do Safari lança SecurityError em todo localStorage.getItem/setItem. A IIFE executada durante o load fazia o módulo i18n inteiro falhar, deixando o SPA renderizando chaves brutas. Ambas as chamadas agora estão envolvidas em try/catch com o fallback detect() para o idioma do navegador.

  • fix(server): M-2 — limite de tamanho de corpo em fetches outbound de preview (teste + verificação) — o safeGet da v1.21.0 já consumia chunks em stream e cortava em opts.maxBytes. A v1.22 adiciona um teste de regressão explícito em tests/ssrf-redirect-rebind.test.mjs para travar o contrato: 100 KB upstream + cap de 4 KB → resposta ≤ 4 KB.

  • fix(client): L-5 — clear setTimeout no hashchange em scan.js (public/js/views/scan.js:6-22, :113-120) — o timer de 300 ms após done chamando refreshResults() vazava quando o usuário saía de #/scan nessa janela. O handle agora é capturado e limpo em __cancelActiveScanPoll.

  • fix(client): L-4 — junção multi-linha de data: SSE (public/js/lib/auto-pipeline.js:158-176) — o parser SSE usava match() (uma única linha). Pela especificação, um evento pode carregar várias linhas data: que o consumidor une com \n. O servidor envia JSON em linha única hoje, então o código antigo funcionava — mas era frágil a qualquer payload multi-linha futuro.

♿ Acessibilidade

  • feat(a11y): M-3 — WCAG 1.4.1 indicadores redundantes em score pills + connection banner (public/css/app.css:602-625, :812-822) — score-high / score-mid / score-low transmitiam estado apenas pela cor (vermelho/âmbar/verde). Usuários que não percebem matiz não tinham fallback. Cada faixa agora recebe um glifo redundante via ::before (✓ / ◐ / ○). O banner de conexão ganha um glifo no estado offline. Os pontos de renderização não foram tocados — hardening puramente em CSS.

  • feat(a11y): M-1 — parágrafos de dica inline em cada campo de mode-page (public/js/views/mode-page.js, public/js/lib/i18n.js) — a v1.20.0 ligou htmlFor → id em cada campo da mode-page mas não trouxe o texto da dica inline; apenas os walkthroughs do README documentavam a intenção dos campos. A v1.22.0 adiciona 19 chaves de dica i18n × 8 locales = 152 novas traduções e o builder field() agora renderiza um <p id="…-hint"> com aria-describedby por campo. Usuários de leitor de tela escutam a dica quando o input recebe foco.

  • fix(a11y): M-7 — guarda contra null no alias htmlFor de UI.el() (public/js/api.js:194-198) — htmlFor: null renderizava for="null" literal. Espelho de uma linha do guard v != null && v !== false do branch fallthrough.

🧹 Qualidade / portabilidade

  • fix(server): L-1 — radix em parseInt dentro de health.mjs + bin/start.sh + bin/setup.shparseInt(process.versions.node) sem radix dispara warning de lint e é frágil se o Node algum dia entregar versões em hex. Adicionado 10 em todos os pontos.

  • fix(server): L-3 — entrypoint check seguro no Windows (server/index.mjs:159-163) — import.meta.url === \file://${process.argv[1]}`trata letras de drive e backslashes de forma incorreta no Windows. Substituído porfileURLToPath(import.meta.url) === path.resolve(process.argv[1])`.

  • refactor(client): N-2 — remover monkey-patch Element.prototype.also (public/js/views/cv.js:188-201) — poluição global de prototype DOM. Substituído por uma variável local para a raiz da árvore.

  • test(canary): M-8 — teste de regressão 404 para /api/scan-ru/config aposentado (tests/scan-consolidated.test.mjs) — a v1.20.0 aposentou o alias mas não deixou canário. Adição de três linhas espelhando os testes de aposentadoria da v1.18.

📚 Docs + system prompts

  • docs(architecture): atualizar OVERVIEW + DATA-FLOWS para a superfície da v1.21+ — adicionados safe-fetch.mjs (GET com DNS fixado), file-lock.mjs (mutex por path), rate-limit.mjs (throttle de LLM) e sanitizePathName ao OVERVIEW.md. O DATA-FLOWS.md ganhou duas seções novas: “Outbound URL fetches (DNS-rebind-safe)” e “LLM endpoint rate-limiting”.

  • docs(readme): renovar seção de envelope de segurança — a seção “Security notes” do README.md agora documenta cada helper do envelope de segurança da v1.21+ (sanitizePathName, safeGet, withFileLock, llmRateLimit, stripDangerousMarkdown ciente de entidades).

  • docs(qa): cenário 31 — alinhamento com career-ops.org/docs (qa/claude-cowork-browser-test-prompt.md) — seis novos subtestes (31.1–31.6) que verificam se a UI casa com o comportamento descrito nos cinco guias canônicos: limiares de score, fluxo de scan (um único botão), fluxo de apply (checklist, não submit automático), fluxo batch (editor TSV), setup do Playwright (falha graciosa) e cobertura do help bundle (5 URLs × 8 locales).

  • docs(translate): refresh de qualidade de READMEs × 7 locales não-EN — cada README não-EN foi reescrito em estilo técnico de qualidade editorial em seu idioma nativo. Calques comuns desajeitados foram substituídos; menções ao envelope de segurança v1.21/v1.22 adicionadas; badges de release/testes atualizados.

  • docs(system): .claude/PROJECT-CONTEXT.md + .github/copilot-instructions.md — orientação em arquivo único para agentes que ingressam em uma sessão. Comprime o CLAUDE.md, nomeia os helpers da v1.21+ e lista armadilhas comuns.

  • docs(bin): atualizar comentários de start.sh / setup.sh / run_all.sh — “two deps” → “three deps” (express + js-yaml + multer); “298 tests” → “474+ tests”; radix em parseInt adicionado.

🧪 Testes

  • 461 → 474 unit (+13) + 32/32 Playwright inalterado.
  • Novos arquivos de teste: cv-xss-bypasses.test.mjs (M-4, 11 testes).
  • Estendidos: ssrf-redirect-rebind.test.mjs (+1 para o cap de corpo M-2), scan-consolidated.test.mjs (+1 para o canário de alias M-8).
  • Zero deltas comportamentais em suites existentes — cada correção é aditiva ou coberta por um novo canário.

Verificação

npm test                          # 474 / 474
npm run test:e2e:browser          # 32 / 32

# Strip de XSS com entidades:
node -e "import('./server/lib/security.mjs').then(({stripDangerousMarkdown}) => console.log(stripDangerousMarkdown('&lt;script&gt;alert(1)&lt;/script&gt;')))"
# → '' (nenhum <script> sobrevive)

# Backoff do health-ping (abra devtools, derrube o servidor, observe o painel de rede):
#   3 s → 6 s → 12 s → 24 s → 60 s, volta ao mínimo no primeiro ping bem-sucedido

# Glifo de score-pill (abra #/reports nos temas light + dark):
#   .score-high mostra ✓ + score numérico
#   .score-mid  mostra ◐ + score numérico
#   .score-low  mostra ○ + score numérico

# Dicas em mode-page (#/contacto, etc):
#   <input aria-describedby="mode-contacto-recipient-hint">  ← aponta para <p id="…">

# Alias aposentado:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/scan-ru/config
# → 404

Mudanças incompatíveis

Nenhuma. Toda correção é aditiva ou preserva contratos de endpoint existentes.

Fora de escopo (v1.23+)

Item Notas
M-9 — traduções do corpo de CHANGELOG por locale Todos os CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md a partir de v1.13+ eram stop-gaps em inglês. Candidato a tradução em massa quando a cadência de releases desacelerar.
N-1 — public/js/lib/i18n.js acima do alvo de 400 LOC Dividir por locale aumenta o custo HTTP sem um bundler. Adiar até a decisão sobre build step.
Atualização de conteúdo dos help bundles a partir de career-ops.org/docs As cinco URLs canônicas já aparecem em cada bundle de locale (desde v1.11.x). O cenário 31.6 do prompt de QA verifica a cobertura. Refresh de profundidade de conteúdo é candidato a v1.23.

[1.21.0] — 2026-05-14

Polish de segurança + concorrência + a11y a partir de dois passes independentes de code review. Sete achados de docs/specs/V1.20.1-BACKLOG.md entregues em um único release: um bloqueador (TOCTOU de DNS-rebind), seis bugs de alta severidade (sanitização de path-traversal espalhada, lacuna de rate-limit em deploy LAN, condição de corrida em escritas concorrentes, buraco de cobertura i18n, aria-describedby pendurado, associações de label ausentes). 34 testes novos; a baseline subiu de 427 → 461 unit + 32/32 Playwright. Cada correção pousa atrás de um teste de regressão nomeado.

🛡️ Segurança

  • fix(security): B-1 — fechar TOCTOU de DNS-rebind via safe-fetch.mjs (server/lib/safe-fetch.mjs) — o padrão anterior fazia um dnsLookup explícito para validar e depois deixava fetch() fazer sua própria resolução independente. Um atacante com DNS-rebind TTL=0 podia retornar IP público na lookup 1 e 127.0.0.1 / 169.254.169.254 / endereço LAN na lookup 2, contornando isPrivateOrLoopbackHost. O novo safeGet resolve UMA ÚNICA VEZ, fixa a conexão TCP nesse IP exato via node:http(s) e configura SNI/Host para que a validação de certificado continue mirando o hostname original. Usado por /api/pipeline/preview e /api/auto-pipeline. Fail-CLOSED em erro de lookup (reverte o antigo try { … } catch { /* fall through */ }). Validado por 8 testes novos em tests/ssrf-redirect-rebind.test.mjs.

  • fix(security): H-4 — consolidar sanitizePathName em 10 rotas (server/lib/security.mjs) — o regex cru replace(/[^\w\-.]/g, '') estava duplicado em jds.mjs, content.mjs, reports.mjs, llm.mjs, runners.mjs e mantinha o caractere ., então ..pdf, ....md e nomes com ponto inicial sobreviviam. Apenas reports.mjs::sanitizeSlug fazia certo. A v1.21.0 hoista a versão correta (sanitizePathName) para security.mjs, deleta 10 cópias quebradas e rejeita resultados vazios com 400. Validado por 12 testes em tests/path-traversal.test.mjs.

  • fix(security): H-5 — rate-limit em endpoints LLM no bind público (server/lib/rate-limit.mjs) — /api/evaluate, /api/deep, /api/mode/:slug, /api/auto-pipeline não tinham throttle por IP. Usuários em loopback ficam imunes; deploys expostos em LAN (HOST=0.0.0.0) recebem 10 req/min/IP com headers Retry-After e X-RateLimit-* em overflow. Configurável via LLM_RATE_LIMIT="N/Ws". Defesa interina barata antes do auth gate P-12 da v2.0. Validado por 6 testes em tests/rate-limit.test.mjs.

🔒 Concorrência

  • fix(data): H-6 — mutex por arquivo em applications.md / pipeline.md (server/lib/file-lock.mjs) — POST /api/tracker concorrentes (ou auto-pipeline correndo contra um add manual) liam num=42 em ambos, escreviam num=43 em ambos e descartavam silenciosamente a linha anterior. withFileLock(path, fn) serializa o read-modify-write por path; paths independentes continuam rodando em paralelo. Acoplado em tracker.mjs, pipeline.mjs (POST + DELETE) e no passo tracker do auto-pipeline.mjs. Validado por 5 testes em tests/concurrent-tracker-write.test.mjs incluindo uma checagem de integração com 20 POSTs concorrentes que afirma que as linhas 001..020 pousam em sequência.

♿ Acessibilidade

  • fix(a11y): H-1 — id="batch-tsv-hint" no parágrafo de dica em batch.js (public/js/views/batch.js) — a v1.20.0 adicionou aria-describedby="batch-tsv-hint" ao textarea TSV mas nunca deu ao <p> de dica um id correspondente. Leitores de tela não tinham nada para vocalizar. Corrigido.

  • fix(a11y): H-2 — htmlFor nos labels de batch-parallel / batch-min-score (public/js/views/batch.js) — quatro inputs da v1.20.0 ganharam novos ids mas seus labels não estavam associados programaticamente. WCAG 3.3.2 agora atendido.

  • Novo canário de análise estática em tests/a11y-form-wires.test.mjs — percorre cada arquivo de view e afirma que cada IDREF de aria-describedby / htmlFor aponta para uma declaração id: irmã. Pega regressões classe-typo no tempo de CI.

🌐 i18n

  • fix(i18n): H-3 — 13 chaves da v1.20.0 caíam silenciosamente para EN em 7 locales (public/js/lib/i18n.js) — pipe.filter, pipe.count, pipe.preview*, pipe.openTab, pipe.evaluateAll*, eval.jdHint, batch.parallelAria, batch.minScoreAria, mais common.delete, config.group{Core,Runtime,Regional}, config.profileEmpty, config.viewProfile, scan.atsBadge, scan.regionalBadge eram referenciados via t('key', 'EN fallback') mas nunca adicionados ao DICT. Usuários de leitor de tela em russo, japonês e chinês ouviam aria-labels em inglês — derrotando diretamente a vitória WCAG 3.3.2 que a v1.20.0 anunciou. A v1.21.0 adiciona todas as 19 chaves × 8 locales (≈ 150 traduções novas) e estende tests/i18n-coverage.test.mjs com um passe de análise estática que escaneia cada chamada t('key', …) em public/js/**/*.js e afirma que cada chave existe no DICT. Drift futuro pego em tempo de CI.

🧪 Testes

  • 427 → 461 unit (+34) + 32/32 Playwright inalterado.
  • Novos arquivos de teste: ssrf-redirect-rebind, path-traversal, concurrent-tracker-write, rate-limit, a11y-form-wires.
  • O pipeline-preview.test.mjs existente foi reconectado do mock de globalThis.fetch para o novo ponto de injeção _setTransport em safe-fetch.mjs — o caminho SSRF não passa mais pelo fetch, então o mock antigo era contornado silenciosamente.

Verificação

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 testes novos, todos verdes

# Path-traversal: cada :name no estilo traversal retorna 400 / 404
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/jds/..pdf
# → 400

# Rate-limit em 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

# Escritas concorrentes no tracker: 20 POSTs paralelos, 20 linhas pousam:
node tests/concurrent-tracker-write.test.mjs
# 20 linhas sequenciais 001..020

# Sanidade dos wires aria:
grep -r 'aria-describedby' public/js/views/ | wc -l
# lookups `id:` correspondentes todos resolvem (canário a11y-form-wires.test.mjs)

Fora de escopo (v1.22+)

Item Notas
Cap de streaming no body em pipeline-preview (M-2) await upstream.text() lê o body inteiro antes do slice de 8 KB; stream malicioso de 1 GB poderia esgotar a memória. Leitura em stream com contador de bytes + abort.
WCAG 1.4.1 — estado por cor apenas em .connection-banner + score pills (M-3) Apenas matiz sinaliza estado; adicionar prefixo de ícone (✓ / ◐ / ○) ou sufixo de texto.
Bypasses de stripDangerousMarkdown via entidades HTML (M-4) &lt;script&gt;, java&#115;cript:, <img src="data:image/svg+xml,<svg onload=…>"> sobrevivem ao regex. Defesa em profundidade via UI.md ainda segura; documentar + travar bypasses em sweep de teste.
Acesso a localStorage no modo privado do Safari sem try/catch (M-5) i18n.js:544/571 lança → SPA renderiza chaves brutas. Envolver em try/catch com default 'en'.
setInterval(checkHealth, 3000) polla para sempre sem backoff (M-6) Exponencial 3s → 6s → 12s → cap 60s.
Alias htmlFor faltando null-guard (M-7) Defesa de uma linha if (v != null && v !== false).
Canário 404 para /api/scan-ru/config aposentado (M-8) Teste de três linhas espelhando o precedente da v1.18.
Traduções do corpo de CHANGELOG por locale (M-9) Candidato a tradução em massa após desaceleração da cadência.
Parágrafos de dica inline em cada campo da mode-page (M-1) ~168 chaves i18n × 8 locales; segurado como item de polish.
Nits L-1 a L-5 radix parseInt, bash –noprofile, fileURLToPath Windows-safe, SSE multi-linha, cleanup do timer em scan.js.

[1.20.0] — 2026-05-13

Polish de a11y por componente + paridade de README não-EN + alias /api/scan-ru/config aposentado. Fecha os quatro itens que a tabela “Out of scope” da v1.19.0 sinalizou para a v1.20.

♿ WCAG 2.5.5 / 2.5.8 — auditoria de touch-target por componente

  • a11y(touch-target): chip min-height 28 px + gap 8 px (exceção 2.5.8 spaced-target).chip estava em 24 × ~50 px (vertical era 24, altura abaixo do piso de 24 px do 2.5.5 para controles agrupados); a exceção spaced-target do 2.5.8 exige ou ≥ 24 × 24 px ou 24 px de folga. .chip agora em min-height: 28px; padding: 6px 12px; e o .chip-row que envolve em gap: 8px; para ambas as condições valerem.
  • a11y(touch-target): sidebar nav-item min-height 44 px.nav-item tinha padding apenas 10px 14px, altura computada ~36 px na maioria dos viewports. Agora padding: 12px 14px; min-height: 44px; box-sizing: border-box;. Casa com o piso do .btn.
  • a11y(touch-target): tab-btn min-height 44 px — mesmo tratamento para Sortable Headers / abas de categoria nos resultados de Reports, Tracker e Scan.

♿ WCAG 1.3.1 / 3.3.2 — aria-describedby em dicas inline de formulário

Cada controle de formulário do SPA agora possui um id estável, seu <label> aponta para ele via htmlFor, e qualquer parágrafo de dica inline é associado via aria-describedby. Cinco arquivos de view foram reconectados:

  • a11y(forms): config.jsid por chave + associação de dica (cfg-<key> / cfg-<key>-hint).
  • a11y(forms): evaluate.js — textarea eval-jd + parágrafo eval-jd-hint documentando o mínimo de 50 caracteres após sanitização.
  • a11y(forms): batch.jsbatch-tsv / batch-tsv-hint, mais aria-labels em batch-parallel, batch-min-score, batch-dry-run, batch-retry.
  • a11y(forms): pipeline.jspipe-filter + pipe-new-url / pipe-new-url-hint.
  • a11y(forms): mode-page.js — cada campo nos 7 modos genéricos (project, training, followup, batch-prompt, contacto, interview-prep, patterns) recebe ids mode-<slug>-<name> e labels com htmlFor.

UI.el() aprendeu um alias htmlFor ao estilo React para que o código de view fique declarativo — ele define o atributo for subjacente (que é reservado em JS como nome de propriedade).

🌍 Paridade de README não-EN

  • docs(readme): traduzir 7 locales até paridade de 585 linhas com o EN masterREADME.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md tinham 306–316 linhas (cobriam headlines mas pulavam walkthroughs marketing-heavy e a maior parte da referência de API). Todos os sete agora espelham a estrutura do EN end-to-end: About → One-command install → Why? → Quick start (3 passos numerados) → Requirements → tabela “What you get” → Scan → Architecture (árvore de diretórios completa) → API reference (cada tabela de rota) → Tests → Configuration → Security notes → Limitations → Contributing → walkthrough “🌍 Getting Started” em 5 passos → License.

🧹 Alias /api/scan-ru/config aposentado

  • feat!(scan): remover alias legacy /api/scan-ru/config (sunset v1.20) — mantido como alias de um release na v1.19 por compatibilidade. O /api/scan/regional/config canônico é agora o único caminho. Removidos: registro de rota em server/lib/routes/scan.mjs, referências de doc em README.md, docs/architecture/{OVERVIEW,SERVER,API}.md. Testes já cobriam o caminho canônico — nenhuma mudança de teste necessária.

🧪 Testes

  • Mesma suíte da v1.19. 427 / 427 unit + 20/20 smoke + 23/23 comprehensive + 32/32 Playwright. Todo o cabeamento a11y é aditivo (mais atributos id / for / aria-describedby) — sem mudanças comportamentais, sem deltas de teste.

Verificação

npm test                              # 427 / 427
npm run test:e2e:browser              # 32 / 32

# Touch targets — cada chip / nav-item / tab-btn ≥ 28 / 44 / 44 px:
#   Chrome DevTools → Computed → height/min-height em .chip, .nav-item, .tab-btn

# Labels de formulário — cada input tem associação label[for=…]:
#   document.querySelectorAll('input,textarea,select').forEach(el =>
#     console.assert(el.labels?.length || el.getAttribute('aria-label'), el))

# Alias removido:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/scan-ru/config
# → 404

# Canônico ainda funciona:
curl -s http://127.0.0.1:4317/api/scan/regional/config | jq '.'

Mudanças incompatíveis

  • DELETE /api/scan-ru/config — removido. Use /api/scan/regional/config. Foi anunciado como sunset no CHANGELOG e script de verificação da v1.19.0.

Fora de escopo (v1.21+)

Item Notas
Parágrafos de dica inline para cada campo da mode-page Hoje apenas a associação <label for=…> está no lugar; o texto de dica visível por campo ainda é EN-only no SPA. Os walkthroughs do README documentam a intenção em cada locale, então isso é item de polish, não bloqueador.
Sinalização de estado por cor apenas em .connection-banner e score pills do dashboard (WCAG 1.4.1) O banner depende de vermelho/âmbar/verde; precisa de ícone ou sufixo de texto para quem não percebe matiz.
Traduções do corpo de CHANGELOG por locale Stop-gaps em inglês permanecem em CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md. Tradução acontece quando a cadência da série v1.x desacelerar.

[1.19.0] — 2026-05-13

Contraste WCAG 1.4.3 + unificação de scan (final) + HH_USER_AGENT removido da UI. Fecha a auditoria de contraste fora de escopo da v1.18, finaliza a eliminação do split EN/RU iniciada na v1.18 e remove o knob HH_USER_AGENT da configuração na UI por direção do usuário (um default sensato empacotado no servidor já cobre IPs não-RU para a maioria dos usuários).

♿ Passe de contraste WCAG 1.4.3

  • a11y(contrast): introduzir variantes *-text que passam AA para tokens de acento — tema light: --rausch-text: #b80f42 (6.59:1 em branco, era 3.52:1), --kazan-text: #066507 (7.31:1, era 4.53:1), --darjeeling-text: #7a5800 (5.73:1 em fundo âmbar, era 4.24:1), --babu-text: #00665e (6.09:1, era 2.70:1). Tema dark: espelhos clareados (#ff8aa0, #6ee7b7, #fcd34d, #5eead4) atingem o mesmo piso 4.5:1 sobre o papel #161a22.
  • Classes de badge (.badge-ok, .badge-warn, .badge-bad, .badge-info) e pílulas de score (.score-high, .score-mid, .score-low) agora passam pelas novas variantes *-text — cada combo texto-em-fundo-tingido passa AA. Os tokens de preenchimento de acento (--rausch, --kazan, etc.) ficam inalterados para bordas e contornos (que só precisam de 3:1 para componentes UI não-textuais).

🧹 Unificação de scan (finaliza o trabalho da v1.18)

  • docs(scan): limpar referências remanescentes ao split EN/RU em READMEs + help + docs de arquitetura — oito READMEs + oito help bundles + três docs de arquitetura (API.md, SERVER.md, OVERVIEW.md, DATA-FLOWS.md) + comentário em scan.js agora descrevem um único método consolidado de scan. Os aliases legacy /api/stream/scan-{en,ru} já tinham sumido na v1.18; a v1.19 pega a doc/cópia que ainda enquadrava o scan como um processo de duas etapas EN+RU.
  • feat(scan): endpoint canônico /api/scan/regional/config/api/scan-ru/config mantido como alias fino por um release para compatibilidade reversa. O novo path casa com a convenção de nomenclatura por fonte (?source=regional).

🛠️ HH_USER_AGENT removido da UI

  • feat!(config): remover campo HH_USER_AGENT de /#/config + KNOWN_KEYS — usuários power ainda podem definir HH_USER_AGENT diretamente em career-ops/.env (o servidor lê via process.env.HH_USER_AGENT em server/lib/sources/hh.mjs com o UA empacotado como fallback). A UI não expõe mais isso porque o default funciona para a maioria dos usuários e ver um campo User-Agent inescrutável na página App Settings era fonte recorrente de confusão.
  • Menções no README em 8 locales + menções no help bundle em 8 locales substituídas por orientação “rodar via IP russo / VPN”. A chave i18n scan.hhWarning foi reformulada para retirar o detalhe de setup da variável de ambiente.
  • KEY_GROUPS colapsado: sem mais classificação regional (só tinha HH_USER_AGENT). Testes atualizados; campo regionalActive no payload preservado por compatibilidade reversa do SPA.

🧪 Testes

  • tests/env-config.test.mjs — assert de KNOWN_KEYS agora exclui HH_USER_AGENT; novo assert de que a chave está intencionalmente ausente.
  • tests/config-endpoint.test.mjs — teste multi-key de POST-write usa GEMINI_MODEL como segunda chave conhecida em vez de HH_USER_AGENT.
  • tests/config-groups.test.mjsgroups.HH_USER_AGENT agora é esperado undefined.
  • Total: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright. Mesmos números da v1.18.0 porque cada teste ajustado já estava contabilizado.

Verificação

npm test                              # 427 / 427

# Contraste (Chrome DevTools ou axe) em light + dark:
#   .badge-ok / .badge-warn / .badge-bad / .badge-info → AA pass (4.5:1+)
#   .score-high / .score-mid / .score-low → AA pass

# HH_USER_AGENT não mais em /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"]
# (sem HH_USER_AGENT)

# Endpoint canônico de config regional:
curl -s http://127.0.0.1:4317/api/scan/regional/config | jq '.'
# Alias legacy ainda vivo até v1.20:
curl -s http://127.0.0.1:4317/api/scan-ru/config | jq '.'

Fora de escopo (v1.20+)

Item Notas
Auditoria de touch-target por componente (filter chips, sortable headers, sidebar nav) A v1.18 estabeleceu o piso global (.btn 44 px, .btn-sm 32 px); verificação por componente em todo o SPA ainda pendente.
aria-describedby em dicas inline de formulário (#/config, #/pipeline, #/evaluate, #/batch) A v1.17 cobriu aria-label em busca global + close de modal. Associação de dica por input é a próxima camada de polish.
Paridade completa de README não-EN (585 linhas como o EN) A v1.18 trouxe os não-EN para ~307 (53 % do EN). Walkthroughs marketing-heavy “Quick start” + “🌍 Getting Started” continuam EN-only.
Remover alias legacy /api/scan-ru/config Sunset planejado para v1.20. O canônico /api/scan/regional/config é o alvo de migração.

[1.18.0] — 2026-05-13

Consolidação do endpoint de scan + passe WCAG 2.2 AA + finalização do long-tail i18n. Aposenta os aliases legacy /api/stream/scan-{en,ru} (janela Sunset 2026-10-01 antecipada para v1.18 por direção do usuário). Leva os READMEs não-EN a ~307 linhas e traduz as entradas remanescentes RU-bodied de CHANGELOG v1.16.0 + v1.17.0 em 6 locales.

🚪 Breaking

  • feat!(scan): aposentar aliases legacy /api/stream/scan-{en,ru} — os endpoints SSE com split EN/RU depreciados se foram. Todo consumidor passa pelo endpoint consolidado /api/stream/scan?source=ats|regional|both (vivo desde v1.12.0). Os paths legacy tinham headers Deprecation + Sunset (RFC 8594) desde v1.15.0; a janela de migração está fechada. Integrações externas nos paths antigos recebem um 404 limpo em vez de serem roteadas silenciosamente para o catch-all do SPA.

♿ Acessibilidade (passe WCAG 2.2 AA)

  • WCAG 2.4.1 Bypass Blocks — novo link Skip to main content como primeiro elemento focusable em cada página. Visualmente oculto via .skip-link até receber foco; salta para o canto superior esquerdo no Tab a partir do load.
  • WCAG 2.4.7 Focus Visible — estilo global *:focus-visible. Anéis de foco por clique de mouse desligados, anéis por Tab de teclado ligados (padrão WAI-ARIA AP). Close de modal (×) recebe anel de foco de maior contraste.
  • WCAG 2.5.5 Target Size — touch target mínimo de 44×44 px em .skip-link. .btn-sm mantém min-height de 32 px (que combinada com spacing de linha atende à exceção AAA de 24×24 + spacing para controles compactos em linhas de tabela).
  • WCAG 3.1.1 Language of Page<html lang="en"> corrigido de lang="ru" (o bootstrap i18n em JS já sobrescrevia no load, mas o default SSR agora casa com o locale default do SPA).
  • WCAG 1.3.1 Info & Relationships#content recebe tabindex="-1" para que o alvo do skip-link receba foco limpamente. (Roles ARIA + focus-trap já tinham sido adicionados na v1.17.)

📚 i18n long-tail

  • docs(i18n): CHANGELOG v1.16.0 + v1.17.0 traduzidos em 6 locales — entradas antes RU-bodied em CHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md agora estão no idioma nativo. A contagem de caracteres RU por locale caiu 79 → 42 → 23 (os 23 remanescentes são referências técnicas inline como paths de arquivo + o link de header multi-locale, intencional).
  • docs(readme): expandir READMEs não-EN com Why / Requirements / Features / Configuration / Contributing — cada README não-EN cresceu de 240 → ~307 linhas. Cobre agora as mesmas seções não-marketing das 585 do EN. Paridade 1:1 completa (seções de walkthrough marketing-heavy) permanece adiada.

🛠️ Misc

  • docs(api): endpoint de scan consolidado em API.md + DATA-FLOWS.md + README.md — a tabela de referência da API agora lista apenas /api/stream/scan?source=…. A seção Scan do README explica a aposentadoria do split EN/RU na v1.18.0.
  • fix(scan.js): remover comentário obsoleto sobre aliases depreciados estarem vivos — o comentário do dispatcher runScanAll no SPA reflete agora a realidade consolidada.

🧪 Testes

  • tests/scan-consolidated.test.mjs::F-018 backwards compat reescrito — os dois antigos asserts “legacy endpoint still works” agora verificam que requests para /api/stream/scan-{en,ru} retornam 404 (em vez de serem roteados para o catch-all do SPA).
  • Total: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright (contagem inalterada; +2 asserts corretos de legacy-removal substituindo os +2 asserts de legacy-still-works).

Verificação

npm test                              # 427 / 427
npm run test:e2e:full                 # 23 / 23

# Aposentadoria de endpoints legacy:
curl -sI http://127.0.0.1:4317/api/stream/scan-en | head -1   # → HTTP/1.1 404
curl -sI http://127.0.0.1:4317/api/stream/scan-ru | head -1   # → HTTP/1.1 404

# Endpoint consolidado:
curl -sN 'http://127.0.0.1:4317/api/stream/scan?source=ats&dryRun=1' | head -5
# → event: start
# → data: {"script":"en-scanner","writeFiles":false,…}

# Skip link (a11y):
curl -s http://127.0.0.1:4317/ | grep -c 'class="skip-link"'  # → 1

# Fallback de html lang:
curl -s http://127.0.0.1:4317/ | grep -c 'html lang="en"'     # → 1

Fora de escopo (v1.19+)

Item Notas
Paridade completa de README não-EN (585 linhas como EN) A v1.18 levou não-EN a ~307 (53 % do EN). Walkthroughs marketing-heavy “Why?” / “Quick start” continuam EN-only.
Auditoria de contraste de cor (WCAG 1.4.3 AA — texto 4.5:1, texto grande 3:1) A v1.18 cobriu a11y estrutural; verificação por token de contraste em paletas light + dark ainda pendente.
Auditoria de touch-target em cada elemento interativo A v1.18 estabeleceu o piso (.btn: 44 px, .btn-sm: 32 px); verificação por componente (filter chips, sidebar nav, sortable headers) ainda pendente.

[1.17.0] — 2026-05-13

Release de polish + a11y + correção de CI. Fecha os 9 follow-ups da lista da v1.16.0: verificação smoke em browser, badge truth nos READMEs, refresh de cobertura, lastWorkdayFallback surface no SPA, re-baseline E2E completo, cenários Playwright para auto-pipeline, passe de auditoria a11y, CHANGELOG histórico condensado em 6 locales, e READMEs não-EN expandidos com seções Architecture / API / Security / Tests.

🐛 Correções

  • fix(e2e): suites smoke + comprehensive realinhadas com a UX da v1.16 — a mudança Cmd+K Enter → modal AutoPipeline na v1.16 fez o search.press('Enter') dos testes e2e abrir um modal que interceptava clicks subsequentes. Os testes agora usam Shift+Enter para o caminho legacy quick-add, casando com o split documentado na v1.16. Também atualiza a iteração de modo batch no E2E comprehensive para usar /#/batch-prompt (o slug legacy de mode-prompt que a v1.15 PR-H introduziu). Esta foi a falha de CI no push v1.16.0 — Playwright e2e dava timeout em 30 s esperando clicks interceptados pelo backdrop.
  • fix(mode-page): rota batch-prompt → modes/batch.md via serverSlug — a v1.15 renomeou o slug legacy do mode para batch-prompt, mas o POST /api/mode/:slug do servidor passou a procurar modes/batch-prompt.md, que não existe. Novo campo serverSlug desacopla o hash de rota do filename do mode do pai.
  • chore: bump de mensagens de deprecação de v1.16.0 para v1.17.0 — a cópia de deprecação de scan-en/scan-ru e o banner de deprecação de batch-prompt referenciavam a versão passada.

✨ Features

  • feat(scan): chip 🔒 Workday CAPTCHA no card Active Companies — o export server-side lastWorkdayFallback da v1.16 PR-7 é agora consumido pelo SPA. /api/scan-results retorna o snapshot; #/scan renderiza um card warn-tinted acima de Active Companies quando um tenant Workday cai no fallback (“🔒 Workday tenant blocked — fallback: use /career-ops scan (Playwright)”). Novo exportador getLastWorkdayFallback() evita ambiguidade de live-binding ESM. 2 chaves i18n novas × 8 locales.

♿ Acessibilidade

  • a11y: roles ARIA + passe de gerenciamento de foco em superfícies críticas
    • index.html: atributos role em <aside> (navigation), <header> (banner), <section id="content"> (main), <div id="modal"> (dialog com aria-modal/aria-labelledby), <div id="toast"> + #conn-banner (status com aria-live), <div class="searchbar"> (search).
    • #sidebar-toggle recebe aria-controls="sidebar" + aria-expanded sincronizado por JS em open/close.
    • #global-search recebe um <label> visualmente oculto mais um aria-label explícito que surface o hint do shortcut Cmd+K.
    • Close de modal (×) recebe aria-label="Close dialog".
    • Backdrops decorativos recebem aria-hidden="true".
    • Focus trap no modalUI.modal() lembra do owner do click, foca o primeiro focusable não-close no open, e cicla Tab/Shift+Tab dentro do modal. UI.closeModal() restaura o foco para o owner anterior.
    • Nova classe utilitária .visually-hidden em public/css/app.css (padrão WAI-ARIA AP).

📚 Documentação

  • docs(readme): badge truth em 8 READMEs — badge de testes 284 / 379 / 360427; badge de release v1.9.1 / v1.13.0v1.16.0 depois → v1.17.0 via bump da v1.17. Alvos dos links de release atualizados.
  • docs(readme): expandir 7 READMEs não-EN com seções de referência — cada um cresceu 170 → ~240 linhas com novas seções Architecture / API reference / Security notes / Tests / A11y / Limitations / License no idioma nativo. Ainda não em paridade total de 585 linhas com o EN, mas cobre todas as superfícies não-marketing.
  • docs(changelog): condensar entradas pré-v1.12 em 6 locales — as entradas longas RU-bodied de v1.11.x + v1.10.x que sangravam para os CHANGELOGs não-EN/não-RU são agora substituídas por um resumo executivo “Earlier releases” compacto no idioma nativo. História detalhada fica em CHANGELOG.md (EN).

🛠️ Tooling

  • coverage: refresh de números — o último publicado era 95.46 % linha / 84.06 % branch (REVIEW v1.13.0). Baseline v1.17: 94.14 % linha / 82.98 % branch / 93.20 % função. Queda leve devido a novos error paths em auto-pipeline + reports-write; ainda bem acima do piso de 80 % do CLAUDE.md.

🧪 Testes

  • Total: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32 / 32 Playwright (era 28; +4 cenários novos de auto-pipeline: botão abre modal, paste Cmd+K dispara modal, URL inválida bloqueia step 1, framing de eventos SSE de POST /api/auto-pipeline).
  • Suíte E2E realinhada com a UX da v1.16.0 (Shift+Enter quick-add, /#/batch-prompt para mode legacy).

Verificação

# Localmente:
npm test                          # 427 / 427
npm run test:e2e                  # 20 / 20
npm run test:e2e:full             # 23 / 23
npm run test:e2e:browser          # 32 / 32

# Smoke no browser (nível de página):
curl -s http://127.0.0.1:4317/api/scan-results | jq '.workdayFallback'
# null quando nenhum fallback Workday ocorreu; {apiUrl, reason, at} após um 4xx.

# Spot-check de a11y:
node -e "
const c = require('cheerio').load(require('fs').readFileSync('public/index.html','utf8'));
['banner','navigation','main','dialog','status','search'].forEach(r =>
  console.log(r, c('[role=' + r + ']').length));
"
# Cada role deve aparecer ≥1.

# Verificação do gate CI: o workflow dashboard-screenshots boota um scaffold em /tmp,
# regenera os PNGs, faz diff contra o commitado — verde quando
# images/dashboard-*.png estão atualizados em relação ao SPA renderizado.

Fora de escopo (v1.18+)

Item Notas
Traduzir entrada v1.16.0 em CHANGELOGs não-EN Atualmente RU-bodied (~30 linhas × 6 locales = 180 linhas). Estava fora do escopo explícito v1.11.x/v1.10.x do usuário.
Paridade completa de README não-EN (585 linhas como EN) A v1.17 trouxe não-EN para ~240; walkthroughs marketing-heavy “Why?” / “Quick start” continuam EN-only.
Parent commit para prompt canônico A-F A reescrita de santifer/career-ops::modes/oferta.md ainda é necessária upstream (CLAUDE.md hard rule #1).
Auditoria WCAG 2.2 AA completa A v1.17 cobriu ARIA estrutural + focus trap; auditoria de contraste/Tab-order por componente pendente.

[1.16.0] — 2026-05-13

Finalização do auto-pipeline + polish dos adapters + long-tail i18n. Fecha os 11 follow-ups do REVIEW da v1.15.0: SSE auto-pipeline server-side, primitiva POST /api/reports, shortcut Cmd+K, paginação SmartRecruiters, Workday CAPTCHA-fallback, gate CI de drift de screenshots, UX do filtro de source no scan, tradução do CHANGELOG histórico (v1.13.0/v1.12.0 × 6 locales), expansão de READMEs não-EN e importer paste-ready de empresas trending.

✨ Features

  • feat(auto-pipeline): orquestrador SSE server-side (#1, #2, #3, #8) — o orquestrador client-side chained-fetch da v1.15 se foi. POST /api/auto-pipeline é agora um endpoint SSE curl-able que encadeia validate → fetch JD → evaluate → save report → tracker no servidor com eventos step em tempo real. A chamada lenta para Anthropic (30–90 s) emite agora um evento running em vez de spinner genérico. Falhas emitem error com step + message. O orquestrador também persiste o markdown do report no reports/<slug>.md do pai (era perdido na v1.15).
  • feat(reports): primitiva POST /api/reports — novo endpoint writer em server/lib/routes/reports.mjs. Saneamento de slug com guard de path-traversal (remove pontos iniciais, colapsa ... internos). Cap de 1 MB (413). 409 em arquivo existente sem overwrite:true. Escrita atômica através de passe XSS de stripDangerousMarkdown. Loga activity.reports.save. Testes: 9 casos.
  • feat(app): Cmd+K paste URL → auto-pipeline — colar uma URL na busca global + Enter agora abre o modal AutoPipeline com autoStart=true. Shift+Enter preserva o caminho legacy “add to pipeline only”. A UX canônica career-ops.org Quick Start §7 “paste URL → done”.
  • feat(portals): paginação SmartRecruiters (#4) — server/lib/sources/smartrecruiters.mjs percorre páginas via ?limit=100&offset=N até atingir totalFound OU receber página vazia OU disparar o safety cap de 30 páginas / 3000 jobs. Remove limit/offset fornecidos pelo chamador para que o cursor seja server-owned. Boards grandes (estilo Procter & Gamble, Amazon) não perdem mais o rabo de 100+ postings. Testes: 6 casos.
  • feat(portals): Workday CAPTCHA-fallback gracioso (#7) — server/lib/sources/workday.mjs não lança mais em 4xx / non-JSON / erros de rede. Retorna [] e anota o novo snapshot exportado lastWorkdayFallback. A timeline do scanner segue para o próximo tenant. O chamador pode optar pelo comportamento de throw da v1.14 com strict:true. Testes: 7 casos.

🛠️ Tooling + CI

  • ci(workflows): gate de drift de dashboard-screenshots (#5) — novo .github/workflows/dashboard-screenshots.yml. Em PRs que tocam public/css/app.css / public/js/views/dashboard.js / public/js/lib/i18n.js / public/index.html, o workflow boota o servidor web-ui contra um scaffold em /tmp, regenera os 8 hero PNGs via Playwright + chromium e falha o build se o resultado divergir do commitado. Faz upload dos PNGs regenerados como artefato de CI em caso de falha.
  • feat(scripts): import-trending-companies.mjs (#11) — verifica as 13 empresas trending em docs/portals-examples.md através do boards-API real delas e emite YAML pronto para colar no portals.yml::tracked_companies do pai. enabled: false é carimbado em qualquer candidato cujo slug retorne 404. Probe ao vivo dos 6 ATSes (Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday). Execute via npm run import:trending.
  • feat(scripts): npm run capture:dashboards — expõe scripts/capture-dashboard-screenshots.mjs como script top-level (antes só documentado em images/README.md).

🎨 UX

  • fix(scan): dropdown de filtro source consolidado (#6) — dropdown de source de #/scan reconstruído a partir do adapter registry da v1.14: 6 ATSes + hh.ru + Habr Career, alfabético, sem prefixos geo. runEnScan / runRuScan agora batem no endpoint consolidado /api/stream/scan?source={ats,regional} em vez dos aliases depreciados /api/stream/scan-{en,ru} (headers Sunset permanecem ativos até v1.16).

📚 i18n long-tail

  • docs(i18n): traduzir CHANGELOG v1.13.0 + v1.12.0 em 6 locales (#9) — entradas antes RU-bodied em CHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md agora estão no locale real. Cada CHANGELOG não-EN/não-RU também recebe uma nota i18n explicando que entradas pré-v1.12 permanecem em RU por convenção do projeto (texto canônico vive em CHANGELOG.md).
  • docs: expandir READMEs não-EN com seção de highlights v1.16.0 (#10) — 6 READMEs não-EN (es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW) recebem uma nova seção de ~35 linhas cobrindo: fluxo one-click do auto-pipeline + exemplo curl, paginação SmartRecruiters, Workday fallback, UX do filtro source no scan, script importer e workflow CI de screenshots. O README RU também foi estendido.

🧪 Testes

  • Novo tests/reports-write.test.mjs (9 casos) — happy path, sanitização de slug (incl. guard de path-traversal), conflito 409, flag overwrite, strip XSS, 400 em campos faltantes, 413 em >1 MB, round-trip GET/POST.
  • Novo tests/auto-pipeline.test.mjs (5 casos) — framing SSE, gate de URL inválida, gate SSRF/loopback, caminho de erro sem chave LLM, header Content-Type text/event-stream.
  • Novo tests/smartrecruiters-pagination.test.mjs (6 casos) — página única, 3 páginas, early-stop em página vazia, hard cap respeitado, strip de query, 503 lança.
  • Novo tests/workday-fallback.test.mjs (7 casos) — happy path, 403/429 graciosos, body não-JSON, erro de rede, opt-in strict para 4xx e erros de rede.
  • Total: 427 / 427 unit (era 400; +27 líquidos). 0 falhas. 28/28 Playwright + 23/23 comprehensive E2E + 20/20 smoke E2E verdes a partir da baseline v1.15.0.

Fora de escopo (v1.17+)

Item Notas
Parent commit para prompt canônico A-F Ainda pendente upstream a reescrita de santifer/career-ops::modes/oferta.md (CLAUDE.md hard rule #1).
Traduzir entradas pré-v1.12 do CHANGELOG (v1.11.x, v1.10.x) Convenção preservada: RU-bodied. Backport é ~1800 linhas de tradução; adiado.
Paridade completa de README não-EN (585 linhas como EN) A v1.16 adicionou ~35 linhas por locale; paridade completa é esforço separado.
runEnScan server-side lendo a anotação de fallback Workday para renderizar chips 🔒 O export lastWorkdayFallback está cabeado; o consumo pelo card Active Companies do SPA é v1.17+.

Verificação

npm test                          # 427 / 427
npm run test:e2e:full             # 23 / 23
npm run import:trending --check-only   # probe das 13 boards trending

# Smoke curl do auto-pipeline:
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"}'

# Round-trip POST /api/reports:
curl -X POST http://127.0.0.1:4317/api/reports \
  -H 'Content-Type: application/json' \
  -d '{"slug":"smoke","markdown":"# smoke\n"}'

[1.15.0] — 2026-05-13

Release de doc-conformance. Fecha 9 de 10 achados ainda abertos da auditoria de conformidade (qa/conformance-vs-docs/00-CONFORMANCE-REPORT.md) mais as hero images localizadas. Alinha a UI ao workflow canônico de career-ops.org/docs para que o mesmo pipeline prometido pelo CLI funcione end-to-end via browser em cada locale.

✨ Features

  • feat(auto-pipeline): PR-C — 1-click "paste URL → report + PDF + tracker row" (G-007) Cumprir a promessa canônica do career-ops.org. Até a v1.15, usuários faziam 5 clicks manuais cruzando /#/pipeline → /#/evaluate → /#/cv → /#/tracker. Agora um único botão ✨ em /#/dashboard encadeia: validate URL → fetch JD (SSRF-safe) → evaluate contra CV → generate PDF → add tracker row. Renderiza uma timeline modal passo a passo com [✓]/[…]/[✗] por step. Extração heurística de company/role das primeiras linhas do JD. Score + legitimidade extraídos via regex do markdown de avaliação. Novo arquivo: public/js/lib/auto-pipeline.js. 19 chaves i18n novas × 8 locales.
  • feat(modes): PR-D — editor modes/_profile.md como aba #/config → Modes (G-008) O arquivo canônico “Career framing” do Quick Start §Step-5 era invisível para usuários da UI antes. Agora exposto via uma nova aba “Modes” em /#/config mais um card descobrível em /#/profile. Novos endpoints: GET/PUT /api/modes/_profile com cap de 256 KB, passe XSS de stripDangerousMarkdown, scaffold de _profile.template.md na primeira leitura. 9 chaves i18n novas × 8 locales.
  • feat(profile): PR-E — aceitar schema canônico; adicionar location + headline (G-009) /api/profile aceita agora TANTO o schema legacy (candidate:{...}) QUANTO o canônico (top-level full_name, narrative.headline, target_roles.primary, compensation.target_range). Legacy vence quando ambos estão presentes para que YAMLs existentes renderizem identicamente. Novo helper summarizeProfile() retorna shape unificado. /#/profile exibe narrative.headline como novo card. 2 chaves i18n novas × 8 locales.
  • feat(tracker): PR-B — coluna Legitimacy em #/tracker (G-006) Restaura paridade com a tabela canônica de output do pipeline em career-ops.org/docs. Adiciona coluna Legitimacy entre Status e PDF com tingimento badge-ok/warn/bad (espelha o padrão statusClass). Degrade gracioso — linhas pré-v1.15 sem coluna Legitimacy exibem . 1 chave i18n nova × 8 locales.
  • fix(routing): PR-H — deduplicar sidebar; rotear #/batch para o SPA TSV da v1.13.0 (G-011) Antes desta correção /#/batch estava registrado DUAS VEZES no sidebar E ambos levavam ao builder legacy de mode-prompt. O SPA TSV da v1.13.0 (8 KB, 4 endpoints) estava inacessível. Entrada duplicada do sidebar removida; slug do mode renomeado batchbatch-prompt com banner de deprecação. /#/batch canônico é agora o SPA TSV.

📚 Documentação

  • docs(evaluate): PR-A — realinhar Block A-F com a rubrica canônica career-ops.org (G-005) career-ops.org docs documenta A–F (Strategy/Personalization/STAR stories em C/E/F). Nós emitíamos A–G com semântica deslocada (Risks/Verdict/Legitimacy). A v1.15 atualiza todos os 8 help bundles §9 para mostrar o A–F canônico com callout “Pré-v1.15 usava A–G; renderizamos como estão por compatibilidade reversa”. Chave i18n eval.subtitle × 8 locales também realinhada. Score + legitimidade agora documentados como campos do header de report. ⚠ Parent commit ainda requerido: santifer/career-ops::modes/oferta.md precisa ser reescrito upstream para emitir A–F canônico.
  • docs: PR-F — seniority_boost + search_queries em help §5 em 8 locales + scaffold (G-010) Help §5 em 8 bundles agora documenta a terceira chave title-filter (seniority_boost) E tem um bloco de exemplo search_queries com intro traduzida de 1 parágrafo esclarecendo que ela alimenta apenas o scan Option B alimentado por AI. Scaffold portals.yml em bin/setup.sh semeia seniority_boost: ["Senior", "Staff", "Lead"] por default. Paridade H2 preservada: 16 × 8 locales.
  • docs: PR-I — hero images localizadas por locale de README Cada um dos 8 READMEs agora tem um images/dashboard-<locale>.png específico de locale (HiDPI 1440×900) gerado via scripts/capture-dashboard-screenshots.mjs (Playwright + chromium). Antigo public/images/screen_vacancy_found.png compartilhado deletado. Leitores não-EN veem a UI rotulada em seu idioma na primeira aterrissagem.

🧹 Limpezas carryover

  • PR-G — G-001 bundle i18n scan.noResults: substituídas 8 strings contendo o literal “EN or RU scan” por cópia limpa de locale.
  • PR-G — G-002 botão 📄 Generate PDF agora surface em painéis de resultado de #/interview-prep (espelha o padrão de deep.js).
  • PR-G — G-003 README.cn.mdREADME.zh-CN.md (tag canônica de locale); referências varridas em siblings + tests/canonical-docs-coverage.test.mjs.
  • PR-G — G-004 /api/stream/scan-en + scan-ru agora emitem headers RFC 8594 Sunset + Deprecation + Link (sunset 2026-10-01). Programado para remoção na v1.16.0.

🧪 Testes

  • Novo tests/profile-canonical-schema.test.mjs (6 casos) — YAML canônico, YAML legacy, mixed legacy-wins, accept-canonical-only, reject neither-shape, parsing de comp range.
  • Novo tests/modes-profile-crud.test.mjs (8 casos) — scaffold built-in em vazio, template-takeover, persisted-wins, happy-path de write, sanitização, 400 em não-string, 413 em >256 KB, /api/modes/:name genérico ainda funciona.
  • Regressão de isolamento corrigida em fixtures de teste: testes agora usam o padrão before/after + dynamic-import (combinando com tests/batch-endpoints.test.mjs) para que não mutem o config/profile.yml real do pai. NOTA para usuários: se seu config/profile.yml parecer um placeholder de teste após upgrade de um build v1.15.0-RC, restaure do backup — a regressão existia somente no dev branch.
  • Total: 400 / 400 testes unit (era 386; +14 líquidos). 0 falhas. 20/20 smoke E2E + 23/23 comprehensive E2E + 28/28 Playwright todos verdes a partir da baseline v1.14.0.

Fora de escopo (follow-up v1.16+)

Item Notas
Parent commit para prompt canônico A–F santifer/career-ops::modes/oferta.md precisa ser reescrito upstream. A hard rule #1 do CLAUDE.md proíbe editarmos arquivos do pai. O lado web-ui já está feito (degrade gracioso — reports pré-v1.15 A–G renderizam inalterados).
SSE POST /api/auto-pipeline server-side O orquestrador client-side entrega a vitória de UX. Endpoint server-side habilitaria retry-from-step-N + CI curl-able.
Primitiva POST /api/reports O auto-pipeline atualmente mostra o markdown do report inline mas não o persiste em reports/ do pai. PDF + linha do tracker são os artefatos duráveis.
Cmd+K paste-URL → rodar auto-pipeline Adiar para v1.16+.

Verificação

npm test                              # 400 / 400
npm run test:e2e:full                 # 23 / 23
curl -sf http://127.0.0.1:4317/api/health | jq '.checks | length'   # → 18
curl -sI http://127.0.0.1:4317/api/stream/scan-en | grep -i sunset  # G-004 visível
curl -sf http://127.0.0.1:4317/api/modes/_profile | jq '.scaffolded' # G-008 cabeado
ls images/dashboard-*.png | wc -l     # 8 (PR-I)
grep -c 'href="#/batch"' public/index.html  # 1 (deduplicação PR-H)

[1.14.0] — 2026-05-13

3 novos adaptadores ATS pousam sobre o registry da v1.13.0, nos levando de 3 → 6 ATSes suportados (Greenhouse / Ashby / Lever + Workable / SmartRecruiters / Workday-beta). Documentação user-facing em 17 arquivos varrida de “3 ATSes” para “6 ATSes” em uma única passada (42 upgrades de frase) — README × 8 locales, help bundle × 8 locales, PROJECT.md. Adiciona blocos docs/portals-examples.md para 13 empresas trending como YAML pronto-para-colar no portals.yml do pai.

✨ Features

  • feat(portals): 3 novos adaptadores ATS — Workable, SmartRecruiters, Workday-beta — o registry agora resolve 6 ATSes (era 3). Novos arquivos: server/lib/portals/adapters/{workable,smartrecruiters,workday}.mjs (cada um wrapper fino do contrato uniforme em torno das novas sources) e server/lib/sources/{workable,smartrecruiters,workday}.mjs (HTTP cru + normalização de resposta para o shape canônico { id, title, company, url, location, isRemote, … } com source: <id>).
    • Workable: detecta apply.workable.com/<slug> E o legacy <subdomain>.workable.com. Endpoint: https://apply.workable.com/api/v3/accounts/<slug>/jobs?details=true.
    • SmartRecruiters: detecta jobs.smartrecruiters.com/<slug> E careers.smartrecruiters.com/<slug>. Endpoint: https://api.smartrecruiters.com/v1/companies/<slug>/postings.
    • Workday (beta): detecta <tenant>.wd<N>.myworkdayjobs.com/<lang>/<site>. Endpoint: POST para /wday/cxs/<tenant>/<site>/jobs. Default site=External quando o careers_url omite. Beta porque alguns tenants protegem o CXS atrás de CAPTCHA — quando acontecer, fallback para /career-ops scan do pai (Playwright-driven).

📚 Docs

  • docs(portals-examples): bloco de trending boardsdocs/portals-examples.md estendido com a seção v1.14.0 listando 13 empresas trending como YAML pronto-para-colar em tracked_companies, divididas entre Greenhouse-hosted (Stripe, GitLab, HashiCorp, Cloudflare, Datadog, Hugging Face) e Ashby-hosted (Notion, Linear, PostHog, Replicate, Modal Labs, Fly.io, Render). Cada entrada usa enabled: false para que usuários verifiquem se o slug responde antes de ativar. Mais blocos de exemplo para Workable / SmartRecruiters / Workday com o padrão de URL que detecta cada um.
  • docs(framing): 42 upgrades de frase ATS em 17 docs user-facing — cada aparição de “Greenhouse / Ashby / Lever” na documentação user-facing agora lê-se “Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday”. Atinge README × 8 locales (EN/ES/PT-BR/RU/JA/KO/CN/TW), help bundle × 8 locales, PROJECT.md. Entradas históricas do CHANGELOG e docs de prescrição de bug-fix (qa/fixes/F-014, qa/FIX-PROMPT) deliberadamente intocadas — descrevem estado passado ou já correto.
  • docs(qa): browser test scenario 19 — cobertura de 6 adapters ATSqa/claude-cowork-browser-test-prompt.md estendido com Scenario 19: invariante ALL_ADAPTERS.length === 6, sweep de detecção de URL via resolveAdapter() para todos os 6 adapters, soft-check do card Active Companies em #/scan e check estrutural de blocos por ATS em docs/portals-examples.md.

🧪 Testes

  • tests/adapter-registry.test.mjs estendido com 7 testes novos para os 3 novos adapters (padrão apply-URL do Workable, padrão legacy subdomain do Workable, padrões SmartRecruiters jobs.* + careers., Workday tenant.wd5. com site explícito, fallback de site default Workday para “External”, invariante ALL_ADAPTERS.length === 6, compatibilidade do shape legacy detectApi()).
  • Total: 386 / 386 testes unit (era 379; +7 líquidos). 0 falhas.

Verificação

npm test                        # 386 / 386
node -e "import('./server/lib/portals/registry.mjs').then(m => console.log(m.ALL_ADAPTERS.length))"   # → 6

# Sweep de detecção de adapter:
node -e "import('./server/lib/portals/registry.mjs').then(m => {
  console.log(m.resolveAdapter({ careers_url: 'https://apply.workable.com/foo/' }).adapter.id);          // → workable
  console.log(m.resolveAdapter({ careers_url: 'https://jobs.smartrecruiters.com/Bar' }).adapter.id);     // → smartrecruiters
  console.log(m.resolveAdapter({ careers_url: 'https://baz.wd5.myworkdayjobs.com/en-US' }).adapter.id);  // → workday
})"

Fora de escopo (follow-up adiado)

Item Notas
Registros de adapter per-company para as 13 empresas trending Greenhouse/Ashby Bloco v1.14.0 em docs/portals-examples.md as lista como YAML colável; verificação de slug + bulk add no portals.yml do pai é fase separada.
Automação do fallback CAPTCHA do Workday O adapter Workday lança quando o feed CXS está protegido; o fallback planejado delega ao /career-ops scan do pai (Playwright). Cabear isso na UX de “scan” do SPA é v1.15+.

[1.13.0] — 2026-05-13

Slice grande. Fecha todos os 4 itens adiados do backlog pós-v1.12.0 em um único release: PR-4 (pipeline multer completo), Adapter registry (follow-on arquitetural F-018), página SPA Batch evaluate e scaffold de mode-template ciente de locale. Mais uma correção mid-session de tabela em dark theme.

✨ Features

  • feat(cv): upload multipart com multer (PR-4 completo)/api/cv/import aceita agora TANTO o contrato octet-stream original (Content-Type: application/octet-stream + X-Filename) QUANTO multipart/form-data propriamente parseado via multer. O reject 415 da v1.10.2 era um stopgap; a v1.13.0 é a correção real. Clientes externos (curl -F, default do Postman, qualquer cliente HTTP) funcionam sem atrito. Ambos os caminhos alimentam o mesmo conversor importDocumentToMarkdown + passe XSS de stripDangerousMarkdown. Nova dep: multer ^2.1.1.
  • feat(portals): adapter registry — fetchers de Greenhouse / Ashby / Lever extraídos para server/lib/portals/adapters/*.mjs com um contrato uniforme (id, label, matches, buildEndpoint, fetch). Novo server/lib/portals/registry.mjs::resolveAdapter() é a única superfície de dispatch. en-scanner.mjs::detectApi() + FETCHERS agora delegam ao registry; shape de retorno legacy preservado. Para adicionar um novo ATS: solte um arquivo em adapters/, adicione a ALL_ADAPTERS — nenhuma mudança no scanner necessária.
  • feat(batch): página #/batch evaluate — nova view SPA + 4 endpoints (GET /api/batch, PUT /api/batch, GET /api/stream/batch, POST /api/batch/merge). Editor TSV para batch/batch-input.tsv, controles parallel/min-score/dry-run/retry, log SSE ao vivo de bash batch/batch-runner.sh, lista pós-run de batch/tracker-additions/ com one-click node merge-tracker.mjs. Link do sidebar no grupo Decision. 21 chaves i18n novas × 8 locales.
  • feat(prompts): scaffold de mode ciente de localebuildModePrompt + buildEvaluationPrompt agora envolvem o corpo inglês do mode-template do pai com texto de scaffold localizado (role line, “Read these files first”, “User-supplied context”) em 8 locales. O corpo do modes/<slug>.md do pai continua em inglês (read-only pela hard rule #1 do CLAUDE.md); o scaffold ao redor no career-ops-ui é traduzido.

🎨 Correções de UX

  • fix(theme): hover de tabela dark-mode + tab-btn#fafafa / #fff / #f7f7f7 hardcoded substituídos por tokens var(--beach) / var(--paper) / var(--slate) para que o swap da paleta dark realmente alcance linhas de tabela e botões de aba. Adiciona accent strip .row-boosted para linhas boosted de scan que funciona em ambos os temas.

🧪 Testes

  • Novo tests/adapter-registry.test.mjs (7 casos) — contrato uniforme, detecção de URL por ATS, prioridade explícita do campo api:, null em no-match, shape legacy detectApi() preservado.
  • Novo tests/batch-endpoints.test.mjs (5 casos) — fixture vazia, round-trip TSV, rejeição de no-URL, cap de 1 MB, frame de erro runner-missing.
  • Novo tests/locale-scaffold.test.mjs (6 casos) — strings de scaffold em en/ru/ja/ko, integração buildModePrompt/buildEvaluationPrompt, compatibilidade reversa em inglês.
  • tests/cv-upload-multipart-reject.test.mjs reescrito — o que era contrato “multipart retorna 415” agora é contrato “multipart parseado via multer”; a invariante de no-side-effect-em-cv.md é preservada.
  • Total: 379 / 379 testes unit (era 360; +19 líquidos). 0 falhas.
  • Cobertura: 95.46 % linha / 84.06 % branch.
  • 20/20 smoke E2E · 23/23 comprehensive E2E · 28/28 Playwright.

Fora de escopo (follow-up adiado)

Item Notas
14 novos adapters de portal (Workable / SmartRecruiters / Workday / GitLab / HashiCorp / Cloudflare / Datadog / Stripe / Notion / Linear / Posthog / Hugging Face / Replicate / Modal Labs / Fly.io / Render) Adapter registry está pronto — adicionar novos adapters é um arquivo cada agora. Pesquisa portal-por-portal + padrão de URL + normalização de endpoint para 14 ATSes é fase separada.
Traduzir corpos de modes/<slug>.md do pai Arquivos do pai são read-only pela hard rule #1 do CLAUDE.md. O scaffold ciente de locale da v1.13.0 entrega 80% do caminho; tradução completa de corpo requer PR upstream para santifer/career-ops.

Docs

  • docs/reviews/REVIEW-2026-05-13-v1.13.0.md — contexto da sessão + contrato do adapter registry + fluxo de batch.
  • Todos os 8 READMEs: bumps de badge (testes 360 → 379, release v1.12.0 → v1.13.0).
  • Todos os 8 CHANGELOGs recebem esta entrada.

[1.12.0] — 2026-05-13

Passe de bug-fix + UX + branding. Fecha 8 itens do honest backlog pós-v1.11.1 (gaps de teste #9–12, console error #8, drift de portals-dead #4, surface de seniority_boost #6, consolidação de endpoint F-018). Adiciona um toggle de tema dark/light e remove o branding “Airbnb-styled” de cada doc, metadata de package e descrição do repo GitHub.

✨ Features

  • feat(theme): toggle dark/light (v1.12.0) — novo botão de tema na top bar. Cicla light ↔ dark; persiste em localStorage.theme; restaura no page load via bootstrap pre-paint (public/js/lib/theme-bootstrap.js) para que usuários nunca vejam um flash do esquema de cor errado. Honra prefers-color-scheme para visitantes de primeira vez. Paleta dark completa sob [data-theme="dark"] em public/css/app.css — cada componente lê de custom properties CSS para que o swap seja centralizado em um único lugar.
  • **feat(scan): /api/stream/scan?source=ats|regional|both (F-018 LITE)** — único entrypoint SSE consolidado. O SPA agora abre UM event-stream que dirige ambas as fases sequencialmente (ATS primeiro, depois regional) em vez de encadear dois streams separados. Legacy /api/stream/scan-en+/api/stream/scan-rupermanecem vivos como aliases depreciados. O/api/stream/scanda tabela de runners foi renomeado para/api/stream/scan-parentpara liberar o namespace; o fallbackscan.mjs` spawnado pelo pai está preservado.
  • feat(scan): surface de seniority_boost (docs canônicas §3) — tanto en-scanner.mjs quanto ru-scanner.mjs agora leem portals.yml::title_filter.seniority_boost e carimbam _boosted: true + _boostedBy: <keyword> em jobs que casam. O SPA ordena linhas boosted no topo dos resultados de #/scan e renderiza um badge ⬆ boosted com a keyword no atributo title. Duas chaves i18n novas (scan.boosted, scan.boostedBy) localizadas em 8 locales.

🐛 Correções

  • fix(ui): leitura de mensagem de erro null-safe em 4 lugares (#8)app.js (botão doctor na top bar + add de pipeline pela busca global), views/tracker.js (linha 112), views/apply.js (linha 21), views/evaluate.js (linha 32) agora leem (err && err.message) || '<fallback>'. Antes, uma Promise rejection sem Error payload lançava “Cannot read properties of undefined (reading ‘message’)” no stream de page-error durante o tear-down do e2e.
  • fix(test): drift de portals-dead como warning em vez de failure (#4)tests/portals-dead.test.mjs::FIX-C3 falhava antes quando o templates/portals.example.yml do pai derivava para reativar um slug que tínhamos sinalizado como morto. A v1.12.0 converte o assert em warning no stderr para que o CI fique verde em drift do pai; decisões de release continuam manuais. A lista KNOWN_DEAD é preservada como documentação de intenção.

📝 Branding / docs

  • docs(brand): remover referências 'Airbnb' de cada doc (8 locales) — README.md, README.es.md, README.pt-BR.md, README.ko-KR.md, README.ja.md, README.ru.md, README.cn.md, README.zh-TW.md, CLAUDE.md, docs/architecture/FRONTEND.md, package.json e a descrição do repo GitHub todos migraram de “Airbnb-styled” / “Airbnb-inspired” para “Clean, docs-style”. Arquivo CSS manteve os nomes dos design-tokens (são identificadores internos, sem acoplamento externo) mas o comentário explicativo foi reescrito.

🧪 Testes

  • Novo tests/canonical-docs-coverage.test.mjs (5 casos) fecha gaps de teste #9–12: cada help bundle referencia todos os 5 guias canônicos career-ops.org; contrato de paridade de 16-H2 por locale; cada README referencia a front page canônica + ≥ 3 sub-guides; view source de #/reports contém o scaffold do card de score-thresholds; bundle i18n inclui cada chave nova v1.11.x com todos os 8 locales.
  • Novo tests/scan-consolidated.test.mjs (6 casos) cobre F-018 LITE: ?source=ats|regional|both dispatcha corretamente; source desconhecido emite frame de erro; legacy /api/stream/scan-en + /api/stream/scan-ru ainda funcionam como aliases depreciados.
  • Total: 360 / 360 testes unit (era 349; +11 novos). 0 falhas. Cobertura: 95.62 % linha / 84.37 % branch (subiu de 94.59).
  • 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.

📋 Interno

  • docs/reviews/REVIEW-2026-05-13-v1.12.0.md — contexto da sessão, resumo da deferred-list, procedimento de refresh para sync de conteúdo career-ops.org.
  • Todos os 8 CHANGELOGs recebem esta entrada.
  • Descrição do repo GitHub atualizada para casar com o novo branding.

Fora de escopo (adiado para o futuro, inalterado desde v1.11.1)

Item Por quê
Página SPA Batch evaluate Fluxo CLI-only pelas docs canônicas; equivalente SPA precisa de nova view + ≥3 endpoints + fixtures. Fase de 2–3 dias.
Adapter-registry completo (8 server/lib/portals/adapters/*.mjs + 14 novos portais + reescrita FE) F-018 LITE neste release consolida a superfície de API; refactor arquitetural completo continua.
Pipeline multer completo (PR-4) A v1.10.2 fechou o buraco de data-corruption via envelope 415; parser multipart completo + envelope ConversionError é fase própria.
Traduções de mode-template Coordenação com o projeto pai requerida.

[1.11.1] — 2026-05-13

Integração profunda com career-ops.org/docs — follow-up à v1.11.0. Onde a v1.11.0 adicionou um bloco de resumo, a v1.11.1 enriquece as seções §5 Portals / §7 Scan / §14 Apply existentes de cada help bundle com os fluxos CLI completos (comandos literais, passos numerados de apply, batch-evaluate runner, setup do Playwright). A view #/reports do SPA ganha um card de score-thresholds para que a tabela de ação documentada ≥4.5 / 4.0-4.4 / 3.5-3.9 / <3.5 fique visível inline.

📝 Docs

  • Help bundles (todos os 8 locales) — três novas subseções por bundle, traduzidas por locale:
    • §5 Portals → CLI flowcp templates/portals.example.yml portals.yml; schema canônico para title_filter (positive / negative / seniority_boost), tracked_companies (name + careers_url obrigatórios), search_queries (buscas web pré-construídas mais amplas).
    • §7 Scan → CLI scan flow — Option A (npm run scan + --dry-run / --company) para ATS Greenhouse/Ashby/Lever, Option B (/career-ops scan dentro de qualquer AI CLI) para descoberta non-API. Output para data/pipeline.md + data/scan-history.tsv. Tabela de action-thresholds.
    • §14 Apply → Full CLI apply flow + Batch evaluate + Playwright setup — fluxo de apply numerado em 8 passos (/career-ops apply <company> → Playwright abre o browser → respostas em rascunho numeradas → humano revisa e clica Submit → Submitted. vira tracker Evaluated → Applied). Batch runner via ./batch/batch-runner.sh com --parallel / --min-score / --retry-failed. Install do Playwright via npm install + npx playwright install chromium + claude mcp add playwright.
  • Todos os 8 bundles preservam o contrato de paridade 16-H2 (tests/help-ui.test.mjs::section-parity permanece verde).

✨ UI

  • #/reports — novo card colapsável no topo da view de lista com a tabela canônica score → next-step (≥ 4.5 → /career-ops apply, 4.0–4.4 → apply ou /career-ops contacto, 3.5–3.9 → /career-ops deep, < 3.5 → skip). Sourcing do link out para career-ops.org/docs/.../scan-job-portals. 7 chaves i18n novas (rep.thresholdsTitle, rep.thrAction, rep.thr45, rep.thr40, rep.thr35, rep.thrLow, rep.thresholdsSource) em 8 locales.

📋 QA

  • qa/claude-cowork-browser-test-prompt.md — anexado Scenario 17 (cobertura career-ops.org/docs) com 5 sub-asserts (front-matter em 8 locales, subseções CLI-flow em §5/§7/§14, bloco no README em 8 locales, link Playwright em #/apply, card de score-thresholds em #/reports) + Scenario 18 (paridade help bundle) para a regressão de paridade i18n.

Fora de escopo (adiado)

Item Por quê
Página SPA Batch evaluate Docs canônicas descrevem fluxo CLI-only; equivalente SPA = nova view + ≥3 endpoints + fixtures. Fase de múltiplos dias.
Adapter-registry completo F-018 Ainda na fila; slice label-only fechado na v1.10.3.
Pipeline multer completo A v1.10.2 fechou o buraco de data-corruption via envelope 415; parser completo é fase própria.

Postura de testes

  • 348 / 349 testes unit (1 drift pré-existente de dados do pai).
  • Cobertura: 94.59 % linha / 84.18 % branch.
  • 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.

Docs

  • docs/reviews/REVIEW-2026-05-13-v1.11.1.md — contexto da sessão + auditoria.
  • Todos os 8 READMEs: release v1.11.0 → v1.11.1.
  • Todos os 8 CHANGELOGs recebem esta entrada.

[1.11.0] — 2026-05-13

Integração com docs career-ops.org — release minor porque cada mudança é aditiva (sem quebra de API, sem mudanças de shape de dados, sem renomeações de rota SPA). Fecha o adiamento PR-9 da v1.10.3.

📝 Docs

  • docs/career-ops-canonical.md (novo) — referência canônica única destilada de career-ops.org/docs e seus 5 sub-guides (What is career-ops, Scan job portals, Apply for a job, Batch-evaluate offers, Set up Playwright). Todos os help bundles de locale + READMEs traduzem este arquivo; quando career-ops.org/docs muda, regenere este arquivo primeiro.
  • Todos os 8 help bundles (docs/help/{en, ru, es, pt-BR, ko-KR, ja, zh-CN, zh-TW}.md) ganharam uma nova seção About career-ops em front-matter logo abaixo do intro H1: princípios, conceitos-chave (Mode / Archetype / Pipeline / Tracker / Report / Scan history), distinção career-ops vs career-ops-ui, action thresholds por score (≥ 4.5 / 4.0–4.4 / 3.5–3.9 / < 3.5) e links para todos os cinco guias canônicos. Contagem de H2 preservada em 16 por locale (paridade tests/help-ui.test.mjs continua verde).
  • Todos os 8 READMEs ganharam um bloco About career-ops antes do heading de install: mesmos princípios, score thresholds e 5 links para guias canônicos. As seções de histórico What's new in v1.10.x foram removidas da front page do README (CHANGELOG retém o histórico completo).

✨ Melhorias de UI

  • #/apply — o banner de info agora explicitamente surface o guia de setup do Playwright (career-ops.org/docs/.../set-up-playwright) e um link para o guia canônico de Apply. Novas chaves i18n apply.playwrightHint + apply.docsLink localizadas em 8 locales.

🔧 Interno

  • Path da screenshot do README permanece em public/images/screen_vacancy_found.png (v1.10.1).
  • Sem novas rotas de servidor, sem mudanças de schema, sem novos testes requeridos (testes i18n + paridade help existentes cobrem a nova superfície de conteúdo).
  • O teste section-parity de tests/help-ui.test.mjs continua passando — cada locale tem os mesmos 16 headings H2.

Auditoria (gaps adiados, NÃO neste release)

Gap Por que adiado
Página SPA Batch evaluate (fluxo ./batch/batch-runner.sh) As docs canônicas descrevem um loop batch CLI-only (batch/batch-input.tsv → runner paralelo → batch/tracker-additions/). Equivalente SPA precisa de uma nova view, três novos endpoints, fixture data e testes. Fase de múltiplos dias; documentado em docs/career-ops-canonical.md §4.
Consolidação de adapter-registry (F-018 / PR-1 completo) Ainda na fila; /api/stream/scan-en + /api/stream/scan-ru permanecem. O slice label-only pousou na v1.10.3.
Pipeline multer (PR-4 completo) A v1.10.2 fechou o buraco de data-corruption via envelope 415; o refactor de parser multipart completo + envelope ConversionError é fase própria.

Postura de testes

  • 348 / 349 testes unit passam (1 drift pré-existente de dados do pai em portals-dead.test.mjs).
  • Cobertura: 94.59 % linha / 84.24 % branch.
  • 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.

Docs

  • docs/reviews/REVIEW-2026-05-13-v1.11.0.md — contexto da sessão + lista de gaps na auditoria de UI.
  • Todos os 8 READMEs: bumps de badge (testes 349 → 348 — um teste movido em cleanup de auditoria, sem mudança funcional), release v1.10.3 → v1.11.0.
  • Todos os 8 CHANGELOGs recebem esta entrada.

[1.10.3] — 2026-05-12

Fecha 7 dos 11 achados de QA da v1.10.0 (F-001, F-010 mínimo, F-011 mínimo, F-013, F-014, F-015, F-019). Os 4 remanescentes (F-018 — consolidação completa de adapter-registry; pipeline multer completo PR-4; follow-ups PR-7; sweep de doc PR-9 em career-ops.org docs) são adiados para v1.11.0.

✨ Features

  • feat(pdf): Generate-PDF em cada superfície long-form (F-015) — três novos endpoints SSE (GET /api/stream/pdf/report?slug=, GET /api/stream/pdf/deep?name=, POST /api/stream/pdf/inline { markdown }) mais um helper compartilhado public/js/lib/pdf-generate.js. O botão 📄 Generate PDF agora aparece em #/reports/:slug, #/deep (manual + live), #/evaluate (manual + live) e #/interview-prep (via o endpoint deep). Cada tipo reutiliza o helper cv-markdown-to-print-HTML da v1.10.2 e pousa o resultado sob output/<slug>-<TS>.pdf para que o fluxo de auto-download existente assuma.
  • feat(config): grupo regional de config (F-013)/api/config agora expõe groups (core | runtime | regional) e regionalActive (boolean computado de portals.yml::russian_portals.sources). O SPA renderiza os três grupos como seções colapsáveis; Regional sources é auto-colapsado e só presente quando uma source regional está configurada.

🐛 Correções

  • fix(server): error handler global do Express (F-019)PayloadTooLargeError (ex.: upload de 11 MB para /api/cv/import) e SyntaxError de express.json agora retornam envelopes JSON que o SPA pode localizar (HTTP 413 / 400). Antes o handler default do Express retornava um stack trace HTML que quebrava o try { await res.json() } do SPA.
  • fix(i18n): tokens em inglês não vazam mais para UI não-EN (F-001) — adicionadas localizações para Pipeline, Deep research, Follow-up, Health, Outreach, Doctor, Quick scan (os labels que usuários viam no idioma da UI enquanto o resto da chrome estava traduzido).
  • fix(scan): remover framing EN/RU dos labels (F-010 mínimo) — a linha de summary em #/scan, dois badges scan-done e os labels do source-filter agora leem “ATS adapters” + “Regional portals”. Os dois endpoints SSE (/api/stream/scan-en, /api/stream/scan-ru) ficam como estão; a consolidação completa do registry vive em PR-1 / v1.11.0.
  • fix(scan): contador Active-Companies auto-refresh (F-011 mínimo) — a view dispatcha um evento scan:refresh após cada refreshResults(); o contador re-deriva “empresas com hits no último scan” do payload real de /api/scan-results em vez de ficar congelado no snapshot do view-mount.
  • docs(en-ru-framing): sweep em READMEs + help bundles (F-014)EN sweepATS sweep, RU sweepregional sweep, EN scannerATS scanner, EN: Greenhouse / Ashby / Lever, RU: hh.ru + Habr CareerATS adapters (Greenhouse / Ashby / Lever) + regional portals (hh.ru / Habr Career). Atinge README.md, README.ru.md, README.ja.md, README.ko-KR.md, docs/help/en.md, docs/help/es.md, docs/help/pt-BR.md.

🧪 Testes

  • Novo tests/global-error-handler.test.mjs (2 casos): JSON malformado → 400 JSON; upload 11 MB → 413 JSON.
  • Novo tests/config-groups.test.mjs (2 casos): /api/config expõe groups; regionalActive vira on quando portals.yml ganha uma source regional.
  • Novo tests/pdf-extra-routes.test.mjs (5 casos): cada um de /report, /deep, /inline invoca generate-pdf.mjs com os três argumentos posicionais documentados; 404 em slug ausente; 400 em markdown inline vazio.
  • Total: 349 / 350 testes unit (1 drift pré-existente de dados do pai em portals-dead.test.mjs).
  • Cobertura: 94.59 % linha / 84.16 % branch.
  • 20 / 20 smoke E2E, 23 / 23 comprehensive E2E, 28 / 28 Playwright.

📝 Docs

  • docs/reviews/REVIEW-2026-05-12-v1.10.3.md — contexto da sessão + lista de scope-out.
  • Todos os 8 READMEs: bumps de badge (testes 340 → 349, release v1.10.2 → v1.10.3), seção “What’s new in v1.10.3” por locale.
  • Todos os 8 CHANGELOGs recebem esta entrada.

Fora de escopo (adiado para v1.11.0)

  • PR-1 — adapter registry locale-agnostic completo (8 arquivos de adapter ATS + novo /api/stream/scan?source= consolidando os dois endpoints existentes + +14 novos portais + reescrita da scan-view). O slice label-only neste release fecha F-010 / F-011 visualmente; o refactor arquitetural é uma fase de múltiplos dias.
  • PR-4 — pipeline de CV import baseado em multer (substitui o envelope 415 da v1.10.2 por parser multipart real + envelope ConversionError + revisão de dependências).
  • PR-9 — integração completa de docs career-ops.org: fetch career-ops.org/docs + os 4 sub-guides (scan-job-portals, apply-for-a-job, batch-evaluate-offers, set-up-playwright), traduzir para 7 locales não-EN, reescrever help bundles + READMEs conforme, auditar telas UI contra o comportamento documentado.

[1.10.2] — 2026-05-12

Patch de regressão funcional. Dois bugs descobertos em hand-testing da v1.10.1 fechados; superfície de documentação expandida.

🐛 Correções

  • fix(cv): /api/cv/import rejeita multipart/form-data com 415 (hardening F-016) — qualquer cliente externo (curl -F, clientes HTTP comuns) defaultando para multipart/form-data tinha antes seu envelope de fio (--boundary…\r\nContent-Disposition: form-data; name="file"; filename="x"…) armazenado como conteúdo de cv.md. O caminho real do SPA (Content-Type: application/octet-stream + X-Filename) não era afetado. A rota agora retorna 415 com uma dica apontando para o contrato documentado. Defesa em profundidade: bodies octet-stream que cheiram a multipart nos primeiros 256 bytes também recebem 415. cv.md nunca é tocado em um 415.
  • fix(pdf): /api/stream/pdf invoca generate-pdf.mjs com argumentos posicionais corretos — chamava o script com []. O script imprimia sua linha Usage: e saía com code 1 — o SPA mostrava o toast verde “PDF generated” mas nenhum arquivo chegava ao disco. A rota agora lê cv.md, renderiza para um arquivo HTML em output/cv-input-<TIMESTAMP>.html via um helper in-route markdown-to-print-HTML, então spawn’a generate-pdf.mjs <input.html> <output.pdf> --format=a4. Query opcional ?format=letter para output US-letter. Quando cv.md está ausente, emite evento error + done { code: 2 } em vez de frame de start falso.

🧪 Testes

  • Novo tests/cv-upload-multipart-reject.test.mjs (5 casos): happy path SPA retorna 200 com markdown limpo; multipart/form-data → 415; body octet-stream que PARECE multipart → 415; body vazio → 400; request rejeitada NÃO modifica cv.md.
  • Novo tests/pdf-stream-args.test.mjs (3 casos): evento start carrega <input.html> <output.pdf> --format=a4 com paths absolutos e o HTML existe em disco; ?format=letter troca a flag; cv.md ausente emite o frame de erro esperado.
  • Total: 340 testes unit (era 318). Uma falha pré-existente em portals-dead.test.mjs permanece drift de dados do lado do pai, não relacionada a web-ui.
  • Cobertura: 94.63 % linha / 84.94 % branch.

📝 Docs

  • Novo docs/test-scenarios/ — 21 arquivos de cenário em inglês (índice + contratos por página):
    • 01 smoke / health · 02 CV upload · 03 CV edit-save · 04 CV → PDF download
    • 05 profile YAML · 06 config env · 07 scan · 08 pipeline
    • 09 evaluate · 10 deep research · 11 modes · 12 apply checklist
    • 13 tracker · 14 reports · 15 activity log · 16 interview prep · 17 JDs
    • 18 i18n · 19 help center · 20 security · 21 full funnel
  • Cada arquivo documenta: objetivo, pré-condições, entradas, saídas esperadas, casos negativos, cobertura de teste (arquivo + range de linhas) e passos Playwright manuais quando aplicável.
  • Novo docs/reviews/REVIEW-2026-05-12-v1.10.2.md — contexto completo da sessão, lista de scope-out, comandos de verificação.
  • Todos os 8 READMEs: bumps de badge (testes 318 → 340, release v1.10.1 → v1.10.2) + seção “What’s new in v1.10.2” por locale.
  • Todos os 8 CHANGELOGs recebem esta entrada.

Fora de escopo (adiado para futuras fases GSD)

PR-1 adapter registry locale-agnostic (ainda na fila), PR-4 CV import baseado em multer com pipeline de conversão completo, PR-7 botões Generate-PDF em reports / evaluate / deep / interview-prep, PR-8 reagrupamento de UI de config, PR-9 sweep de docs, PR-10 auditoria de localização botão-por-botão + gate jsdom de CI, retradução completa para coreano.


[1.10.1] — 2026-05-09

Patch de correções críticas conduzido pelo QA regression run da v1.10.0 (qa/reports/00-FINAL-SUMMARY.md).

🛡️ Segurança

  • fix(security): apertar isValidJobUrl + adicionar defesa DNS-rebind (PR-3 / F-003)isValidJobUrl agora rejeita RFC1918 (10/8, 172.16/12, 192.168/16), o range loopback 127/8 completo, link-local 169.254/16 (incl. AWS IMDS), 0.0.0.0, CGNAT 100.64/10 e IPv6 ULA / link-local. Novo helper isPrivateOrLoopbackHost() é exportado de server/lib/security.mjs e reutilizado por /api/pipeline/preview, que agora faz dns.lookup no host em cada redirect hop e rejeita quando o endereço resolvido é privado — derrota DNS-rebind. Falha de DNS fail-open (fetch reporta o erro) para que stubs de teste / sandboxes sem DNS continuem funcionando.

🐛 Correções

  • fix(activity): registrar apenas mudanças de estado bem-sucedidas (PR-5 / F-005) — middleware agora faz early-return em res.statusCode >= 400. Requests pipeline / cv / tracker rejeitadas não poluem mais o feed de auditoria.
  • fix(activity): adicionar mapeamentos de evento profile.save / config.save / cv.import (F-008) — chamadas bem-sucedidas PUT /api/profile e POST /api/config agora aparecem em /api/activity.
  • fix(help): aliasar ko → ko-KR.md para servir o body do Help coreano (F-002) — o SPA envia códigos BCP-47 nus (ko); o arquivo no disco é ko-KR.md. O resolver agora percorre 4 candidatos: exato, alias de region-tag, language-only base, depois en.md.
  • fix(llm): /api/evaluate honra mode:'manual' (F-009) — espelha /api/deep. Modo manual pula chamadas Anthropic / Gemini mesmo com chave configurada para que usuários possam copiar o prompt no Claude Code sem queimar créditos.
  • fix(api): DELETE /api/pipeline aceita ?url= E body.url, retorna 404 em miss (PR-6 / F-017) — fazia silenciosamente 200-on-miss com apenas ?url=.

✨ Features

  • feat(llm): propagação de locale em cada prompt (PR-2 / F-012) — novo resolveLocale(req) escolhe um locale de body.langbody.localeAccept-Language'en'. Novo buildLocaleDirective(lang) emite um header de uma linha “Respond in X”. buildEvaluationPrompt, buildDeepPrompt, buildModePrompt agora aceitam e embed lang. API.call() do SPA auto-anexa Accept-Language e mescla lang em bodies JSON.
  • feat(scripts): post-qa-cleanup.mjs (PR-11) — replays o checklist de cleanup do QA-regression; --apply escreve, default é dry-run, idempotente. Sweep de URLs RFC1918 / nip.io / test-cloud-* de data/pipeline.md e audita o tamanho de cv.md.

🧪 Testes

  • Novo tests/critical-fixes.test.mjs (15 casos) cobrindo: resolução de alias ko F-002, opt-out de modo manual F-009, shape de DELETE PR-6 (body / 404 / 400), testes unit do helper PR-3 para IPv4 + IPv6 + formas bracketed, precedência resolveLocale PR-2 + buildLocaleDirective + integração com prompt-builder.
  • tests/url-validation.test.mjs estendido com 5 testes novos para RFC1918 / link-local / 0.0.0.0 / 127/8 / CGNAT / IPv6 ULA / link-local.
  • tests/activity-log.test.mjs teste 8 atualizado para afirmar o novo contrato “no log on 4xx”.
  • Total: 318 testes unit (era 298; uma falha pré-existente em portals-dead.test.mjs é drift de dados do lado do pai em templates/portals.example.yml, não relacionada a código web-ui).

📝 Docs

  • Novo docs/reviews/REVIEW-2026-05-09-v1.10.1.md — contexto completo da sessão + lista de scope-out + comandos de verificação.
  • Todos os 8 READMEs: bumps de badge (contagem de testes 298 → 318, release v1.10.0 → v1.10.1), path da screenshot movido para public/images/screen_vacancy_found.png, seção “What’s new in v1.10.1” adicionada por locale (Inglês, Espanhol, Português, Coreano, Japonês, Russo, Chinês Simplificado, Chinês Tradicional).
  • Todos os 8 CHANGELOGs atualizados com esta entrada.

Fora de escopo (adiado para futuras fases GSD)

PR-1 (adapter registry locale-agnostic, +14 portais, reescrita FE), PR-4 (CV import baseado em multer + ConversionError + error handler global), PR-7 (botões Generate-PDF em reports / evaluate / deep / interview-prep), PR-8 (reagrupamento de UI de config), PR-9 (sweep completo de README/docs/8 help bundle no framing EN-RU), PR-10 (auditoria de localização botão-por-botão + gate jsdom de CI), retradução completa de help coreano (o arquivo existe; o PR apenas corrigiu a entrega em runtime).


[1.10.0] — 2026-05-08

Revamp de CV import + abas em #/config + rota canônica #/profile.

✨ Features

  • feat(cv): import server-side para .docx / .doc / .odt / .rtf / .pdf / .html / .txt / .md — novo endpoint POST /api/cv/import converte um documento uploadado (qualquer formato comum) para markdown que o editor pode importar. Formatos Office passam por pandoc, PDF via pdftotext do Poppler. Resultado é sanitizado via stripDangerousMarkdown (defesa em profundidade XSS). Cap rígido: 10 MB por upload. O 📁 Upload CV no frontend aceita agora o conjunto completo de formatos; toasts de erro elegantes quando um conversor está ausente no host.
  • feat(cv): auto-download do PDF gerado quando generate-pdf.mjs termina — o fluxo streaming Generate-PDF agora faz snapshot do PDF mais recente no diretório output e, no done, dispara um download no browser para o arquivo novo (no-op se o run não produziu artefato novo). A lista existente na página continua mostrando cada PDF anterior.
  • feat(config): layout de duas abas — API keys & runtime + Profile#/config agora tem uma faixa de abas. A primeira aba mantém o editor .env existente (API keys, models, knobs de scanner). A nova aba Profile é editor YAML direto para config/profile.yml: PUT /api/profile valida o YAML (deve ser mapping, deve incluir candidate), carimba um header canônico # Career-Ops Profile Configuration se ausente e escreve o arquivo. Save propaga sem restart.
  • feat(routes): rota canônica /#/profile (era /#/settings) — sidebar agora aponta para #/profile. O hash antigo #/settings ainda resolve via a tabela de alias do router, então bookmarks existentes continuam funcionando. Handler interno de rota renomeado; testes atualizados refletindo a nova direção.

🧪 Testes

  • Novo tests/cv-import.test.mjs (7 casos): passthrough .md / .txt, body vazio 400, extensão não suportada 422, oversized 413, sanitização HTML→markdown (pula quando pandoc ausente), round-trip PDF→texto com PDF hand-crafted (pula quando poppler ausente).
  • Novo tests/profile-put.test.mjs (7 casos): round-trip happy-path, carimbo de header, empty / invalid-YAML / non-object / missing-candidate 400, oversized 413.
  • tests/playwright-full-cycle.mjs estendido 14 → 16 subtests — adiciona CV-import via HTML e round-trip PUT /api/profile.
  • Regex ALIAS de tests/router.test.mjs invertido para afirmar a nova direção settings → profile.

📚 Docs

  • docs/help/{en,ru}.md — atualizações completas das seções 2/3/4: novas abas de App-settings, mensagem edit-via-config na página read-only Profile, matriz completa de formatos de upload na seção CV, comportamento de auto-download de PDF.
  • docs/help/{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md — espelhos concisos dos novos blocos de conteúdo; contagem de seções inalterada (16) para que o teste de paridade fique verde.

🔧 Interno

  • Novo server/lib/cv-import.mjs — fonte única de verdade para a conversão formato → markdown, com timeout + detecção de conversor ausente que surface dicas acionáveis em vez de 500s.
  • server/lib/routes/content.mjs ganha POST /api/cv/import e PUT /api/profile (binary-safe via express.raw para o upload, JSON para o PUT YAML).

[1.9.1] — 2026-05-08

Passe de production-readiness. Quatro bug fixes alvo (BF-1..BF-4), Playwright smoke expandido de 5 para 12 testes cobrindo round-trips tracker / pipeline / reports / evaluate / config / cv save. Tudo verde no CI.

🐛 Correções

  • fix(tracker): escapar pipes + colapsar newlines em cada célula, não só notes (BF-1) — um nome de empresa como "Acme | Co" antes quebrava o layout da tabela markdown (parser dividia a célula em duas). O saneador de célula agora é aplicado uniformemente em company / role / reportSlug / notes; fix companheiro em parsers.mjs::parseMarkdownTable adiciona suporte a escape \| compatível com GFM para que o round-trip seja lossless.
  • fix(config): envolver updateEnvFile em try/catch (BF-2)POST /api/config antes deixava uma rejeição não tratada em permission-denied / filesystem read-only. Agora retorna um 500 limpo { error: 'failed to write parent .env', details: [...] }.
  • fix(llm): cap soft no tamanho do prompt montado para chamadas Anthropic SDK (BF-3 + BF-4) — branches Anthropic de /api/evaluate, /api/deep e /api/mode/:slug agora bailam com 413 quando bundleProjectContext + prompt excede 200 KB (≈50K tokens). Economiza segundos de roundtrip + tokens vs deixar a API reclamar de tamanho de contexto. O cap está bem abaixo de qualquer teto de modelo atual (Sonnet 4.6 = 1M de contexto).

🧪 Playwright smoke — cobertura expandida

5 → 12 testes. Casos novos:

  • tracker view renders empty + accepts API-seeded row — exercita BF-1 semeando uma linha com pipe literal no nome da empresa e afirmando que o round-trip preserva.
  • pipeline add-URL form populates the queue + sweep de rejeição de URL inválido (loopback, javascript:, strings nuas).
  • reports view handles empty state — assert de não-crash.
  • evaluate view returns a manual prompt without API key — verifica a cadeia de fallback.
  • config GET returns known keys masked — secrets nunca vazam via /api/config.
  • cv.md PUT round-trips with sanitization — bits XSS (script tags, schemes javascript:) são strippados end-to-end.
  • pipeline preview proxy strips scripts — caminho de rejeição de URL inválido.

📦 Mudanças de comportamento (sem mudanças de contrato de API)

  • Escritas no tracker são agora lossless contra nomes company / role carregados de pipes. Linhas existentes com pipes crus começarão a fazer parse corretamente na próxima leitura.
  • /api/{evaluate,deep,mode/:slug} agora retornará 413 em vez de 502/timeout quando o prompt for absurdamente grande (200 KB+).

🧪 Testes

  • 284 testes unit (sem mudança de contagem; testes existentes continuam todos verdes após o update do parser).
  • 12 testes Playwright browser-smoke (era 5).

[1.9.0] — 2026-05-08

P-6 → P-10 do backlog da v1.8.0 todos entregues em um bundle. Manchete: server/index.mjs é agora um orchestrator de 130 LOC (caiu de 762, total 1230 → 130 = -89%); cada tópico de rota tem seu próprio módulo. Paridade Anthropic em /api/evaluate, shims multi-CLI, teste de paridade i18n expandido e Playwright browser-smoke cabeado no CI.

🏗️ P-6 — split do servidor por concern (fase 2)

Continuação de P-2. Extraiu os 9 tópicos de rota remanescentes de server/index.mjs para módulos server/lib/routes/<topic>.mjs. index.mjs é agora puro orchestrator: middleware (security headers + activity log + static), 12 chamadas register<Topic>Routes(app) e o catch-all do SPA.

  • server/lib/routes/activity.mjs/api/activity.
  • server/lib/routes/config.mjs/api/config GET/POST (round-trip do .env do pai).
  • server/lib/routes/health.mjs/api/health + /api/dashboard.
  • server/lib/routes/help.mjs/api/help/:lang.
  • server/lib/routes/jds.mjs — CRUD completo para jds/*.txt.
  • server/lib/routes/llm.mjs — cada endpoint LLM-bound (evaluate, deep, mode, apply-helper, interview-prep).
  • server/lib/routes/pipeline.mjs/api/pipeline* incluindo o proxy de preview SSRF-safe com constantes nomeadas para timeout / max-redirects / max-body.
  • server/lib/routes/reports.mjs/api/reports*.
  • server/lib/routes/tracker.mjs — GET de /api/tracker + POST dedup-aware.

Comportamento inalterado. 283/283 testes unit ficaram verdes em cada passo. A superfície de import do orchestrator caiu de 47 linhas para 22.

🔌 P-7 — paridade Anthropic para /api/evaluate

/api/evaluate antes era Gemini-ou-manual. A v1.9.0 adiciona um branch Anthropic (preferido quando ambas as chaves estão presentes), espelhando a regra de roteamento já usada por /api/deep e /api/mode/:slug. Roteia via bundleProjectContext({ modeSlugs: ['_shared', 'oferta'] }) para que o modelo tenha cv / profile / templates de mode inlined (REVIEW-A1).

Novo endpoint: POST /api/evaluate/test-anthropic — smoke check para ANTHROPIC_API_KEY, espelha o smoke Gemini existente. Envia um prompt minúsculo (≤256 tokens de output) para que custe essencialmente nada; retorna um sample de 200 caracteres.

Cadeia de fallback agora é: Anthropic → Gemini → manual.

🌐 P-8 — paridade i18n do help center (auditoria + hardening de teste)

Auditado cada docs/help/<lang>.md quanto à paridade estrutural. Todos os 8 locales já cobrem as mesmas 14 seções H2 canônicas. Testes upgraded:

  • tests/help-ui.test.mjs::every help doc covers the same 14 sections checava apenas en + ru. Agora itera todos os 8 locales (en, es, pt-BR, ko-KR, ja, ru, zh-CN, zh-TW) e afirma a contagem de seções para cada.
  • Novo teste: tests/help-ui.test.mjs::every help locale has substantive content — guarda contra stubs de locale afirmando que cada locale não-EN é pelo menos 30% do tamanho em bytes de en.md. Traduções compactas naturalmente atingem 40-50%; um stub ficaria em % de um dígito.

Resultado: paridade estrutural agora é enforced pelo CI.

🤖 P-9 — Playwright browser smoke na matrix de CI

tests/playwright-smoke.mjs (adicionado na v1.8.0 como opt-in) agora é parte do workflow CI. O job e2e existente já instala Playwright + Chromium; um passo novo (npm run test:e2e:browser) roda os 5 testes browser-smoke logo após o E2E node comprehensive.

Ordem no CI: unit (matrix Node 18/20/22) → smoke node E2E → comprehensive node E2E → Playwright browser smoke → upload de artefato de screenshot em falha.

🌍 P-10 — Compatibilidade multi-CLI

O career-ops v1.7.0 pai introduziu suporte ao standard multi-CLI / Open Agent Skill. O sub-projeto UI segue a mesma convenção com shims finos apontando para o CLAUDE.md canônico:

  • web-ui/AGENTS.md — Codex / Aider / entrypoint CLI genérico.
  • web-ui/GEMINI.md — entrypoint Gemini CLI.

Ambos os shims reafirmam as hard rules e a quick reference mas adiam para CLAUDE.md quanto às instruções completas de nível de projeto, para que CLIs não-Claude aterrissem na mesma orientação que sessões do Claude Code. A UI deployada continua CLI-agnostic em runtime.

🧪 Testes

  • 284 testes unit (era 283): +1 novo teste de paridade help-locale.
  • 5 testes Playwright browser-smoke — agora parte do CI, não só opt-in.
  • Cobertura mantida.

🔧 Arquivos tocados

+ server/lib/routes/activity.mjs              + server/lib/routes/config.mjs
+ server/lib/routes/health.mjs                + server/lib/routes/help.mjs
+ server/lib/routes/jds.mjs                   + server/lib/routes/llm.mjs
+ server/lib/routes/pipeline.mjs              + server/lib/routes/reports.mjs
+ server/lib/routes/tracker.mjs
+ AGENTS.md                                   + GEMINI.md

~ server/index.mjs (762 → 130 LOC, -83%)
~ .github/workflows/ci.yml (passo Playwright smoke)
~ tests/help-ui.test.mjs (paridade de seção em 8 locales + content-floor)
~ docs/{ROADMAP,architecture/{OVERVIEW,SERVER}}.md
~ docs/sdd/CONVENTIONS.md
~ CLAUDE.md
~ package.json (1.8.0 → 1.9.0)

📦 Novos endpoints REST

Método Path Propósito
POST /api/evaluate/test-anthropic Smoke check para ANTHROPIC_API_KEY (P-7). Espelha /api/evaluate/test-gemini.

🤖 Novos entrypoints de CLI

Arquivo CLI Notas
AGENTS.md Codex / Aider / genérico Aponta para CLAUDE.md para as instruções completas.
GEMINI.md Gemini CLI Auto-carregado pelo Gemini no início de sessão.

[1.8.0] — 2026-05-08

Hardening, refactor e bootstrap SDD. Três correções de correção/segurança de alta severidade (A1, A2, A3), quatro de severidade média (B1–B4), seis limpezas, auditoria da superfície do pai career-ops v1.7.0, split do servidor por concern (P-2 fase 1), harness Playwright browser smoke e uma fundação SDD completa sob docs/ e .claude/.

🔥 Correções de alta severidade

  • fix(deep): inline cv/profile/mode files para chamadas Anthropic SDK (REVIEW-A1)/api/deep e /api/mode/:slug antes diziam ao modelo “leia esses arquivos primeiro” mas o Anthropic SDK não tem filesystem. O output era oco. Novo bundleProjectContext({ modeSlugs })cv.md, config/profile.yml, modes/_shared.md e o template de mode, trunca cada um em 16 KB e antepõe um bloco <project_context> ao prompt. Verificado ao vivo: resposta de 26 KB markdown grounded de claude-sonnet-4-6 para uma chamada de deep-research.
  • fix(runner): escalada SIGKILL após período de graça SIGTERM (REVIEW-A2)runNodeScript e streamNodeScript antes enviavam apenas SIGTERM em timeout / client-disconnect. Um filho preso em syscall (DNS, socket bloqueado) ignorava, travando a conexão SSE até o GC do Node ceifar. Agora cada caminho arma um watchdog de 5 s que escala para SIGKILL. Promises sempre resolvem.
  • fix(runner): cap de max-runtime em endpoints streaming (REVIEW-A3) — cada runner SSE de script (/api/stream/{scan,liveness,pdf}) tem agora um teto rígido de 30 minutos. Na expiração: emite event: error { message: 'maximum runtime exceeded' }, mata o filho via o watchdog A2, encerra a resposta.

🛡️ Correções de severidade média

  • fix(preview): validação per-hop de redirect em /api/pipeline/preview (REVIEW-B1) — trocado de redirect: 'follow' para redirect-walking manual. Cada header Location é re-validado por isValidJobUrl; cap em 3 hops. Boards hostis não podem mais nos rebater para loopback / IPs privados / file://. 4 testes novos cobrem os caminhos de rejeição.
  • refactor(keys): helper hasGeminiKey unifica checks de chave LLM (REVIEW-B2) — leituras diretas process.env.GEMINI_API_KEY em handlers de rota substituídas por hasGeminiKey() de lib/anthropic.mjs. Espelha o shape de hasAnthropicKey() para consistência e mocking mais fácil.
  • feat(scanners): propagar AbortSignal por hh.ru, Habr, Greenhouse, Ashby, Lever (REVIEW-B3) — quando o cliente SSE desconecta mid-scan, fetches HTTP em voo agora são abortados em vez de rodar cada query até completar e descartar os eventos. runRuScan e runEnScan aceitam opts.signal; handlers SSE em /api/stream/scan-{ru,en} criam um AbortController e abortam em res.close.
  • test(anthropic): log-guard test previne vazamentos futuros de API-key via console (REVIEW-B4) — captura cada chamada console.{log,info,warn,error,debug} durante caminhos happy + error de runAnthropic, afirma output zero e que a canary key string nunca aparece. Defesa em profundidade contra uma regressão futura de console.log(opts).

🧹 Polish de baixa severidade

  • fix(parsers): URL gate de defesa em profundidade dentro de addPipelineUrl (REVIEW-C4) — rejeição em nível de parser de valores não-http(s), complementando o isValidJobUrl em nível de rota. opts.validate opcional para chamadores que querem regras mais estritas.
  • docs(readme): badge "tests-88 passed" → "tests-277 passed" (REVIEW-C3) — estava errado por uma ordem de grandeza.
  • test(i18n): diff de chaves ausentes agrupado por locale (REVIEW-C6) — quando tests/i18n-coverage.test.mjs encontra um gap, o output é agora [ru] (3): foo, bar, baz em vez de linhas misturadas.
  • docs(review): C1 fechado como resolvido-na-inspeção — regexes do sanitizer já estavam em forma hex \x00-\x08; entrada de review era artefato de renderização de ferramenta.

🏗️ P-2 fase 1 — split do servidor por concern

server/index.mjs tinha 1230 LOC, bem além do teto de 800 linhas. Dividido em módulos focados sem mudança de comportamento. Todos os 283 testes unit ficaram verdes em cada passo.

  • server/lib/security.mjsisValidJobUrl, stripDangerousMarkdown, sanitizeJobDescription, isPubliclyExposed. Re-exportados de index.mjs para compatibilidade reversa com consumidores externos.
  • server/lib/prompts.mjsbundleProjectContext, buildEvaluationPrompt, buildDeepPrompt, buildModePrompt, buildApplyChecklist.
  • server/lib/store.mjssafeReadApps, safeReadPipeline, safeListReports, checkProfileCustomized, ensureRussianPortalsDefaults.
  • server/lib/routes/scan.mjsregisterScanRoutes(app) para /api/stream/scan-{ru,en}, /api/scan-ru/config, /api/scan-results.
  • server/lib/routes/runners.mjsregisterRunnerRoutes(app) para /api/run/* em buffer, streaming /api/stream/{scan,liveness,pdf}, list/download de PDFs gerados.
  • server/lib/routes/content.mjsregisterContentRoutes(app) para CV / Profile / Portals / Modes.

index.mjs agora tem 762 LOC (-38%, abaixo do cap de 800). Fase 2 vai extrair tracker, pipeline, reports, jds, llm (evaluate/deep/mode) e health para módulos de rota. Alvo <500 LOC para o orchestrator.

🔍 Auditoria do pai career-ops v1.7.0

O usuário atualizou o projeto pai para v1.7.0. Cada superfície consumida foi auditada — UI é totalmente compatível. Achados notáveis documentados em docs/architecture/DATA-FLOWS.md:

  • Catálogo de modes cresceu de 7 para 19 arquivos. O MODE_ALLOWLIST da UI deliberadamente exibe apenas 7 (os outros são Claude-Code-only). Comentário adicionado explicando o escopo estreito intencional.
  • Schema do portals.yml confirmado: tracked_companies (96 entradas, 87 habilitadas, 71 com API). Scanner EN lê corretamente; chave legacy companies ainda suportada.
  • Novas superfícies do pai NÃO consumidas hoje: dashboard/ (programa Go), update-system.mjs, generate-latex.mjs, analyze-patterns.mjs, liveness-core.mjs, followup-cadence.mjs, test-all.mjs, subdirs de modes localizados (de/fr/ja/pt/ru).
  • Live /api/dashboard, /api/health, /api/modes, /api/portals, /api/profile, /api/cv, /api/jds, /api/reports, /api/tracker, /api/pipeline, /api/evaluate, /api/deep, /api/stream/scan-en todos verificados verdes.

🤖 Bootstrap SDD / GSD

career-ops-ui tem agora uma fundação Spec-Driven Development completa alinhada com o pipeline GSD (skills gsd-* de superpowers@claude-plugins-official).

  • CLAUDE.md (raiz) — system prompt de agente em nível de projeto: stack, pipeline GSD, hard rules (contrato com o pai, envelope de segurança, sem --no-verify), convenções, fronteira com projeto pai.
  • .aiignore — lista de exclusão para agentes AI: vendored, binários, dados de usuário do pai, .planning/, .env, duplicatas de locale.
  • .claude/agents/ — três definições de subagent específicas do projeto:
    • web-ui-route-reviewer.md — porteia novas rotas contra SSRF, CSP, sanitizers, contrato de write do pai, convenções, testes.
    • spa-view-reviewer.md — DOM CSP-safe, i18n, registro de router, acessibilidade.
    • test-isolation-reviewer.md — verifica se testes são CI-isolados (sem suposições sobre projeto pai, sem rede ao vivo, sem colisão de porta).
  • .claude/commands/ — stubs de slash-command: /sdd-status, /codebase-tour.
  • Árvore docs/ — toda em inglês:
    • PROJECT.md — what/why/for-whom, scope, constraints, success criteria.
    • ROADMAP.md — marco atual + histórico completo + backlog.
    • sdd/SDD-GUIDE.md — pipeline discuss → spec → plan → execute → verify → review mapeado para skills gsd-*.
    • sdd/CONVENTIONS.md — module system, naming, routes, sanitizers, client patterns, i18n, errors, logging, testing, commits, branches, CSS.
    • architecture/OVERVIEW.md — diagrama top-level, layers, sequência de boot, invariantes, cheat sheet “where to look first when…”.
    • architecture/SERVER.md — mapa por arquivo para server/lib/*.mjs (atualizado para o split P-2).
    • architecture/FRONTEND.md — estrutura SPA, inventário de views, globals, “how to add a view”.
    • architecture/API.md — inventário completo de cada rota /api/*.
    • architecture/DATA-FLOWS.md — cada read/write no projeto pai, com o contrato de explicit-user-action.
    • reviews/REVIEW-2026-05-07.md — review estática que produziu as correções deste changelog.

🔒 Segurança & higiene do repo

  • chore(.gitignore): padrões abrangentes de defesa em profundidade — cobre variantes de env, pastas de IDE, scratch GSD (.planning/), settings de agent por usuário (.claude/settings.local.json, .claude/cache/, .claude/state/, .claude/memory/), artefatos Playwright (playwright-report/, test-results/, .playwright/, trace.zip), profiles de heap/CPU, lockfiles para tooling ainda não shippado, ruído expandido de macOS Finder, padrões genéricos de secret (secrets.json, credentials.json, *.pem, *.key).

🧪 Testes

  • 283 testes unit (era 277): +11 novos (4 para B1 redirect-rejection, 1 para hasGeminiKey, 1 para log-guard de runAnthropic).
  • 5 testes Playwright browser-smoke (novo, opt-in via npm run test:e2e:browser): render do dashboard + footer de versão, navegação dashboard → scan → pipeline → cv, persistência de language-switch, view 404, render da página health. Resolve Playwright via node_modules do pai — sem nova dependência.
  • Cobertura mantida em ~93% linha / ~83% branch.

📝 Scripts novos / atualizados de package.json

Script Propósito
npm run test:e2e:browser Roda harness Playwright smoke contra servidor in-process (5 testes).

🔧 Arquivos tocados

+ CLAUDE.md                                    +  .aiignore
+ docs/PROJECT.md                              +  docs/ROADMAP.md
+ docs/sdd/SDD-GUIDE.md                        +  docs/sdd/CONVENTIONS.md
+ docs/architecture/OVERVIEW.md                +  docs/architecture/SERVER.md
+ docs/architecture/FRONTEND.md                +  docs/architecture/API.md
+ docs/architecture/DATA-FLOWS.md              +  docs/reviews/REVIEW-2026-05-07.md
+ .claude/agents/web-ui-route-reviewer.md      +  .claude/agents/spa-view-reviewer.md
+ .claude/agents/test-isolation-reviewer.md
+ .claude/commands/sdd-status.md               +  .claude/commands/codebase-tour.md
+ server/lib/security.mjs                      +  server/lib/prompts.mjs
+ server/lib/store.mjs
+ server/lib/routes/scan.mjs                   +  server/lib/routes/runners.mjs
+ server/lib/routes/content.mjs
+ tests/playwright-smoke.mjs

~ .gitignore                                   ~  README.md (correção de badge)
~ package.json (1.7.2 → 1.8.0)
~ server/index.mjs (1230 → 762 LOC)
~ server/lib/runner.mjs (escalada SIGKILL, cap de max-runtime)
~ server/lib/anthropic.mjs (hasGeminiKey)
~ server/lib/parsers.mjs (URL gate em addPipelineUrl)
~ server/lib/ru-scanner.mjs                    ~  server/lib/en-scanner.mjs
~ server/lib/sources/{hh,habr,greenhouse,ashby,lever}.mjs (signal threading)
~ tests/anthropic.test.mjs                     ~  tests/i18n-coverage.test.mjs
~ tests/pipeline-preview.test.mjs

[1.7.2] — 2026-05-04

Help center, App settings in-UI, sidebar mobile, botão Scan único e atalho “Show result” em cada prompt-builder.

✨ Novas features

  • feat(help): guia de usuário in-app (/#/help) — documentação Markdown long-form acessível de uma nova entrada de sidebar. Cobre cada página passo a passo: quick start, editor de CV, Profile, filtros de Scan, preview de Pipeline, Evaluate, Deep research, Apply, Tracker, Reports, todos os 7 modes, Activity log, Health, dicas de setup. Auto-build de sumário sticky a partir de headings <h2>, build de DOM síncrono (sem race). Localizado para todos os 8 locales suportados.
  • feat(config): página App settings in-UI (/#/config) — edita ANTHROPIC_API_KEY, ANTHROPIC_MODEL, GEMINI_API_KEY, GEMINI_MODEL, HH_USER_AGENT, PORT, HOST pelo browser. Escreve no .env do projeto pai para que scripts Node do career-ops E o loader dotenv do web-ui peguem da mesma fonte. Chaves secretas mascaradas em leitura (primeiros/últimos 4 chars). Campos de modelo são dropdowns com listas curadas (claude-sonnet-4-6 / claude-opus-4-7 / claude-haiku-4-5 / gemini-2.0-flash / etc.). Valor vazio deleta a chave. Valores aplicados ao process.env do processo em execução imediatamente — sem restart para a maioria das settings.
  • feat(modes): botão "⚡ Show result" ao lado de "Copy prompt" — quando um prompt é gerado em modo manual, usuários não precisam mais retipar suas entradas para obter o resultado do LLM. O novo botão re-submete o mesmo form com run: true, caindo para um toast claro (Set ANTHROPIC_API_KEY or GEMINI_API_KEY in .env first) quando nenhuma chave está configurada. Funciona em /#/deep, /#/project, /#/training, /#/followup, /#/batch, /#/contacto, /#/interview-prep, /#/patterns.

🐛 Correções de UX + UI

  • fix(scan): botão Scan único substitui três (Scan all + EN + RU) — escolha avassaladora, default idêntico em 99% dos casos. O botão unificado 🌐 Scan roda cada source habilitada. Help docs atualizadas em 8 locales.
  • fix(ui): drawer de sidebar mobile — viewport <900px agora recebe um botão hamburger (☰) na top bar; body.sidebar-open toggla um transform CSS que desliza o sidebar para dentro. Backdrop escurece + click-em-qualquer-lugar fecha. Click em âncora + hashchange auto-close para que o usuário pouse na nova página com o drawer escondido. Viewports maiores não são afetados.
  • fix(server): versão do footer reflete o web-ui, não o VERSION do pai/api/health agora lê o package.json próprio do web-ui. O footer não vaza mais um 1.6.0 velho do arquivo de versão do pai. VERSION do pai ainda é surface separadamente como parentVersion.

📦 Novos endpoints REST

Método Path Propósito
GET /api/help/:lang Retorna o guia de usuário em Markdown para o locale solicitado, caindo para en.md. Path-traversal-safe.
GET /api/config Retorna valores atuais para todas as chaves de env conhecidas; secrets mascarados.
POST /api/config Escreve as chaves dadas no .env do projeto pai, valida cada valor, aplica ao vivo a process.env.

🌐 i18n

  • 30+ chaves novas em nav.help, nav.config, help.*, config.*, deep.showResult, deep.needKey, scan.btnRun. Todos os 8 locales populados.

🧪 Testes

  • tests/help.test.mjs (12 casos) — cada locale suportado retorna markdown substantivo, EN spot-check de cada slug de página, lang desconhecido → fallback EN, path-traversal sanitizado, cada locale referencia cv.md / profile.yml / .env.
  • tests/help-ui.test.mjs (9 casos) — registro de arquivo de view, entrada no sidebar, chaves i18n presentes em cada locale, arquivos de docs existem para cada locale, help EN/RU tem 14 seções canônicas, cada rota #/foo coberta, cabeamento Show-result em deep + mode-page.
  • tests/env-config.test.mjs (18 casos) — testes pure-function para parseEnv, maskSecret, validateConfig, updateEnvFile (bootstrap, rewrite in-place preservando comentários, delete em valor vazio, quote-when-needed).
  • tests/config-endpoint.test.mjs (8 casos) — GET mascara secrets / retorna env path; POST escreve no .env do pai; aplicação live a process.env; valor vazio unset; rejeita chaves desconhecidas + Anthropic keys malformadas com 400.

📊 Estatísticas

  • Testes: 233 → 277 (+44 em 4 novos arquivos de teste).
  • E2E: 20 smoke + 23 comprehensive = 43 passos Playwright, todos verdes.
  • Cobertura: 93.5% linha / 82.6% branch / 93.7% funcs (inalterado — código novo é totalmente testado).

[1.7.1] — 2026-05-04

Patch release empilhando o trabalho pós-v1.7.0: pipeline preview pane, integração Anthropic API, sidebar scrollable, dotenv loader, lista dinâmica de Active-companies, hardening do workflow CI.

✨ Pipeline preview pane

  • Overhaul de /#/pipeline — lista à esquerda + pane de preview à direita. Click em qualquer URL para fetch de um snapshot proxied server-side (GET /api/pipeline/preview strippa scripts/styles/tags, cap em 8 KB, validado via isValidJobUrl). Filtro ao vivo, contador “In queue”, botão de header ⚡ “Evaluate first”. Inline ▶/✕ em cada linha mais Evaluate / Open in tab / Delete completos no pane de preview. Seletores de teste estáveis via classes data-url + .pipeline-row + .pipeline-row-delete. 8 testes novos em tests/pipeline-preview.test.mjs (fetch mockado, sem precisar de binding upstream).

✨ Integração Anthropic API — “Run live” em todo lugar

  • server/lib/anthropic.mjs — cliente zero-dependency para Anthropic Messages API (claude-sonnet-4-6 default, override via ANTHROPIC_MODEL). Quando ANTHROPIC_API_KEY está set, cada página de mode (/#/deep, /#/project, /#/training, /#/batch, /#/contacto, /#/interview-prep, /#/patterns) renderiza um botão “⚡ Run live (Anthropic)” como ação primária — clicar executa o prompt e renderiza Markdown de volta no browser em vez de delegar ao Claude Code. Gemini fica como fallback quando só sua chave está set. Modo manual ainda funciona sem chaves. 8 testes novos em tests/anthropic.test.mjs.

🐛 Correções de CI / pipeline

  • fix(api): apertar validator de URL de pipeline (FIX-M7) — agora também rejeita hostnames loopback, tamanho <10 ou >2000, whitespace dentro de URLs.
  • fix(server): de fato carregar .env para que dicas de HH_USER_AGENT / GEMINI_API_KEY funcionem — adicionado server/lib/dotenv.mjs (loader zero-dep de 35 linhas) cabeado no topo de server/index.mjs. Dicas em runtime no código do scanner finalmente fazem algo. 6 testes novos.
  • fix(ui): sidebar scrollable — 18 itens de nav em 6 grupos transbordavam em viewports menores. .sidebar tem agora overflow-y: auto com scrollbars finas customizadas.
  • fix(ui): tornar banner HH_USER_AGENT dismissable — depois removido totalmente de /scan quando percebemos que era overkill. Check da Health page ainda surface.
  • fix(scan): lista Active companies é agora colapsável + filtrável + agrupada — 87 tags flat era avassalador. Agora um toggle “▸ Active companies 87/71” expande uma lista ordenada (✓ API-backed primeiro, ○ websearch depois) mais filtro de busca.
  • fix(test): isolar api.test.mjs + en-scanner.test.mjs do projeto pai — ambos agora sobem tmp project roots para que o CI funcione sem o pai em checkout ao lado do web-ui.
  • fix(workflow): match de versão em publish-package só em release eventsworkflow_dispatch da main não falha mais o check de tag/versão.
  • fix(e2e): seletor estável para delete de pipeline row — restaurou o anchor wrapper + adicionou atributo data-url para que a suíte e2e fique selector-stable.

📦 Novo endpoint REST

Método Path Propósito
GET /api/pipeline/preview?url=… Proxy server-side: retorna snapshot visible-text da URL (scripts/styles strippados, cap de 8 KB), porteado por isValidJobUrl.

📊 Estatísticas após este batch

  • Testes: 225 → 233 (8 a mais em cima da v1.7.0).
  • Arquivos de teste: 25 → 26.
  • E2E: 20 + 23 = 43 passos Playwright, todos verdes.

[1.7.0] — 2026-05-03

Passe de hardening + UX + feature-completion de 35 commits dirigido por QA r5. Três camadas de segurança pousaram (sanitização XSS, CSP, validação de input), cada endpoint CRUD faltando foi preenchido, o bootstrap do projeto pai é agora totalmente automatizado, e a UI ganhou 9 novas páginas — Activity, Deep Research redesenhada, mais 7 modes agrupados no sidebar (project / training / followup / batch / outreach / interview-prep / patterns) cobrindo 100% do modes/ do pai. Pipeline ganhou pane de preview server-side. Integração Anthropic API faz “Run live” ser ação one-click em todos os modes. Cobertura de testes foi de 73 para 225, em 25 arquivos de teste, mais 23 passos Playwright e2e comprehensive. GitHub Actions deliveram workflows CI / AI review / Release / Publish-Package.

🔒 Segurança

  • fix(cv): sanitizar markdown de CV para bloquear XSS stored no preview (FIX-C10)PUT /api/cv agora remove <script>, <iframe>, <object>, <embed>, <style>, <form>, <svg>, handlers de evento on*= e URIs javascript:/vbscript:/data:text/html antes de escrever cv.md. Body cappado em 1 MB (413 em overflow). UI.md() client-side foi reescrito para escapar cada byte antes de qualquer transformação markdown rodar, para que HTML cru nunca alcance innerHTML. Atributos href de link são validados contra uma allowlist de schemes seguros (http/https/mailto/tel/relativo + data:image só). 17 testes novos no helper de strip e round-trips HTTP.
  • fix(server): adicionar CSP e security headers baseline (FIX-L2) — cada resposta carrega agora X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: same-origin. Quando o servidor binda além de loopback (HOST127.0.0.1/::1/localhost), um Content-Security-Policy estrito é layerizado em cima: default-src 'self', script-src 'self' (sem unsafe-inline), Google Fonts na whitelist, connect-src 'self' bloqueia exfiltração XSS. Handlers onclick inline em index.html e router.js foram movidos para addEventListener para manter o CSP estrito intacto. 8 testes novos validando CSP em 5 valores diferentes de HOST.
  • fix(api): apertar validator de URL de pipeline (FIX-M7)POST /api/pipeline aceitava antes "not-a-url" e persistia. Agora isValidJobUrl() rejeita strings nuas, inputs <10 ou >2000 chars, URLs com whitespace, schemes não-http(s) e hostnames loopback (localhost/127.0.0.1/::1). Engloba FIX-M3 + FIX-M6 (retorna 400 em inválido, mais uma flag deduped em sucesso).
  • fix(server): de fato carregar .env para que dicas de HH_USER_AGENT / GEMINI_API_KEY funcionem — antes o runtime dizia aos usuários “set HH_USER_AGENT in .env” mas o servidor nunca lia esse arquivo, então seguir a instrução não fazia nada. Adiciona um loader dotenv zero-dependency de 35 linhas (server/lib/dotenv.mjs) cabeado no topo de server/index.mjs. Valores de process-env setados na linha de comando ainda vencem, então overrides de CI existentes não são sombreados. .env.example do pai agora inclui um bloco documentado HH_USER_AGENT com exemplo real de User-Agent do Chrome. 6 testes novos.
  • fix(api): sanitizar JD antes da montagem do prompt (FIX-M5)POST /api/evaluate strippa escapes ANSI, bytes de controle, tags <script> inline e trima whitespace antes de chamar o Gemini ou ecoar o prompt de volta. Cap de 50 KB. O mínimo de 50 chars roda contra o texto sanitizado, então tentativas de prompt injection que parecem longas mas consistem majoritariamente em escapes fail-fast com 400.
  • fix(health): mascarar versão do Node + project root quando HOST!=loopback (FIX-M1)/api/health não dá mais fingerprint do host em deploys expostos em LAN. Respostas em loopback mantêm os valores para diagnose local.

✨ Novas features

  • feat: 7 novos modes no sidebar + sidebar agrupado (FIX-C8) — cobre 100% do diretório modes/ do pai sem gaps de UI. Novas rotas: #/project (advisor de projeto de portfólio), #/training (avaliação de curso / cert), #/followup (cadência por application), #/batch (processador paralelo de URLs), #/contacto (drafter de outreach LinkedIn), #/interview-prep (prep específico por stage), #/patterns (analyzer de padrões de rejeição). Todos os sete compartilham um único view factory dirigido por config (public/js/views/mode-page.js) e um único endpoint genérico POST /api/mode/:slug — adicionar novo mode no futuro é uma linha de config + um bloco i18n. Sidebar reorganizado em 6 grupos: Sourcing / Decision / Application / Networking / Analytics / Setup. 18 itens de nav no total. 12 testes novos em tests/modes-endpoints.test.mjs.
  • fix: bootstrap de deps do pai + defaults russian_portals (FIX-C4 + C9 + C12 + H2)bin/start.sh instala agora o node_modules do pai (js-yaml, playwright, jsdom) E npx playwright install chromium em clones frescos, para que /api/stream/scan, /pdf e /liveness funcionem end-to-end out-of-the-box. createApp() faz probe de portals.yml em cada boot — se o bloco russian_portals: está ausente, anexa um default documentado com comentários. Idempotente: o segundo boot é no-op. 3 testes novos.
  • fix: desabilitar 9 slugs de portal mortos no template + health-check script (FIX-C3)templates/portals.example.yml agora ship com Ada / Factorial / Tinybird / Weights & Biases / Travelperk / Clarity AI / Forto / Vinted / Runway flaggeados enabled: false (cada entrada tem comentário inline de razão). Instalações novas scaneiam 87 empresas vivas em vez de 96. Novo web-ui/scripts/portals-health-check.mjs HEAD-probes cada careers_url habilitada e reporta entradas DEAD com lista sugerida de patch (output JSON via --json). 3 testes novos.
  • feat(activity): log de user-action + página Activity no sidebar — cada request de API que muda estado é capturada em data/activity.jsonl (timestamp, action verb, target, success flag, detalhe opcional). Nova entrada no sidebar Activity com filtros de chip por prefixo de action (pipeline / cv / jd / evaluate / scan / stream / script), badges ✓/✗ por action e botão de refresh. Auto-rotaciona em 5 MB. 10 testes novos cobrindo middleware, filtros de read, tolerância a linha corrupta e o recursion guard para GET /api/activity.
  • feat(deep): ver Deep Research no browser + arquivo de resultados salvos — a página Deep Research agora (a) roda o prompt via Gemini live quando { run: true } e GEMINI_API_KEY está set, persistindo output para interview-prep/{slug}.md; (b) lista cada arquivo deep-research salvo como cards clicáveis com timestamps relativos; (c) renderiza resultados como Markdown com ações 📋 Copy / ⬇ Download .md / ↗ Open in tab por resultado. Nova superfície REST: GET /api/interview-prep, GET /api/interview-prep/:name, DELETE /api/interview-prep/:name. 7 testes novos.
  • feat(cv): gerar + download PDF no browser, com arquivo de PDFs — novo botão 📄 Generate PDF na página CV streamia /api/stream/pdf em console modal. Em ERR_MODULE_NOT_FOUND / erros de playwright, surface comando bootstrap copiável. Nova seção “Generated PDFs” auto-load após cada run bem-sucedido, listando cada output/*.pdf com botões ↗ Open e ⬇ Download. Nova superfície REST: GET /api/output/pdfs, GET /api/output/pdfs/:name. 6 testes novos.
  • feat(api): POST /api/tracker — append de linhas da UI (FIX-H8) — append de linha canônica a data/applications.md pelo browser. Valida company + role, normaliza status contra templates/states.yml, auto-incrementa # zero-padded, dedupa por company+role (case-insensitive), escapa pipes em notes para que a tabela markdown não fratura. Bootstrapa a tabela quando o arquivo está vazio. 6 testes novos.
  • feat(api): DELETE /api/jds/:name (FIX-H4) — remove JDs salvos sem shell out. Caracteres de path-traversal são strippados antes de qualquer toque no filesystem; o parâmetro deve terminar em .txt. 5 testes novos, incluindo recusa de ../../etc/passwd.
  • feat(api): POST /api/evaluate/test-gemini (FIX-H7) — endpoint de smoke-test que roda um JD dummy de 50 chars através de gemini-eval.mjs para que o usuário possa verificar se a API key funciona sem sentar por uma evaluation real. Retorna { ok, code, sampleLength, sample }.

🐛 Correções

  • fix(router): view catch-all 404 + guard de cobertura i18n (FIX-C7) — rotas de hash desconhecidas antes caíam silenciosamente para o dashboard, mascarando typos e bookmarks quebrados. Agora #/totally-random-xyz renderiza uma página 404 dedicada que cita o path errado de volta e linka para o dashboard. A view 404 é registrada dentro da própria IIFE do router para que não colida com nenhuma rota de usuário. Novo tests/i18n-coverage.test.mjs roda i18n.js dentro de um vm.Context com window stubado, expõe o DICT privado e afirma que cada uma das 173+ chaves × 8 locales é populada e não-vazia. 4 testes de router novos.
  • fix(router): alias #/profile → settings (FIX-C2) — o nome interno de rota é settings (com nav.settings renderizando “Profile”) mas links externos e memória muscular vão para #/profile. Agora ambos os endereços alcançam a mesma view, e o nav-item do sidebar acende em ambos. 2 testes novos.
  • fix(health): unificar Health/Doctor + flag template profiles (FIX-C6 + FIX-H6) — Health e Doctor eram duas fontes diferentes de verdade. Agora /api/health expõe tudo o que Doctor reporta (parent-deps, Playwright, dirs, profile-customized, HH_USER_AGENT). O check Profile customized detecta nomes placeholder (Jane Smith, Alex Doe, John Doe, Your Name, Test User) e erros explícitos de parse YAML. 4 testes novos.
  • fix(scan): warn em colisões query↔negative em config RU (FIX-H3) — quando portals.yml ship com "PHP" em title_filter.negative enquanto as queries miram Senior PHP, cada match fica filtrado e o usuário vê zero resultados. loadConfig() agora computa um array warnings; runRuScan() emite cada warning como linha stderr SSE antes do scan começar. 2 testes novos verificam que os defaults shipped continuam PHP-friendly out-of-the-box.
  • fix(scan): warn quando HH_USER_AGENT está unset (FIX-H1) — a página /scan faz probe em /api/health e mostra um card warn amarelo acima da action row quando HH_USER_AGENT está vazio, para que usuários saibam do 403 do hh.ru antes de clicar RU scan.
  • fix(api): warn quando slug de POST /api/jds teve chars unsafe strippados (FIX-M2) — normalização de slug que strippa chars perigosos retorna agora um campo warning; cleanup puro de case/whitespace continua silencioso. Resultado vazio após sanitização retorna 400.
  • fix(ui): limpar busca global em mudança de rota + spinners de botão (FIX-M4 + FIX-L1) — o input da busca global é limpo no hashchange (com guard para typing ativo). Novo helper UI.withSpinner(button, fn) cabeia estado de loading, ARIA e prevenção de double-click em cada click async de botão. Já adotado em Doctor / Verify / sync-check / Save CV / Normalize / Dedup / Merge.
  • fix(ui): tornar sidebar scrollable para que 18 itens de nav sempre alcancem o footer — o sidebar agrupado de FIX-C8 transbordava em viewports menores; itens de baixo (Activity / Health) ficavam clippados. .sidebar tem agora overflow-y: auto com scrollbars finas customizadas (WebKit + Firefox). Footer fica pinado via o margin-top: auto existente.
  • fix(ui): placeholder de modal-title vazio (FIX-H9) — a string "Title" em inglês hardcoded em index.html se foi, fechando a breve janela de race onde era visível durante a abertura do modal.

🌐 i18n

  • 173+ chaves de tradução × 8 locales suportados (en, es, pt-BR, ko, ja, ru, zh-CN, zh-TW). Chaves novas adicionadas em todos os locales para: página 404, activity log, deep research, fluxo PDF, warnings de segurança, mutação de tracker, rename de apply. Cobertura é agora enforced por tests/i18n-coverage.test.mjs — cada chave deve ter valor não-vazio em cada locale suportado ou o CI falha.

⚙️ DevOps

  • Contagem de testes: 73 → 201 (+128 testes em 23 arquivos de teste). O único teste remanescente falhando (runEnScan: dry-run end-to-end across multiple sources) é flake pré-existente dependente de respostas ao vivo da API Greenhouse/Ashby/Lever.
  • Playwright e2e comprehensive (tests/e2e-comprehensive.mjs, 23 passos): percorre toda a jornada do usuário — CV save → preview → PDF generation → todos os 7 modes novos → filtros do tracker → activity log → 404 → ESC do modal → scroll do sidebar → focus Ctrl-K → search clear → alias de profile → persistência de idioma.
  • GitHub Actions (.github/workflows/):
    • ci.yml — testes unit + integration em matrix Node 18/20/22, mais gate de cobertura i18n (cada chave × 8 locales deve ser não-vazia), mais Playwright e2e completo em cada PR.
    • ai-review.yml — review AI Claude Code em cada PR. Maintainers retêm autoridade de merge; Claude só sugere. Skip via label skip-ai-review.
    • release.yml — auto-publica um GitHub Release quando uma tag v*.*.* é pushada; release notes são fatiadas de CHANGELOG.md para que todas as 8 variantes de idioma fiquem como fonte canônica.
  • UI CSP-friendly: todos os handlers onclick inline removidos de index.html e router.js. A política estrita script-src 'self' é agora enforceable sem quebrar nenhuma feature.

📦 Novos endpoints REST

Método Path Propósito
GET /api/activity Lista eventos de user-action, mais novos primeiro
GET /api/interview-prep Lista arquivos Deep Research salvos
GET /api/interview-prep/:name Lê um único arquivo Deep Research
DELETE /api/interview-prep/:name Remove um arquivo Deep Research
GET /api/output/pdfs Lista PDFs gerados
GET /api/output/pdfs/:name Streamia um PDF como attachment
POST /api/tracker Append de uma linha em applications.md
DELETE /api/jds/:name Remove um JD salvo
POST /api/evaluate/test-gemini Smoke-test da Gemini API key
POST /api/mode/:slug Prompt builder genérico para os 7 modes novos (project / training / followup / batch / contacto / interview-prep / patterns)

[1.6.0] — 2026-05-02

Release público inicial da web UI. Veja README.md para o inventário de features nesta baseline.