Changelog

GitHub

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

[1.253.0] — 2026-10-24

Dez novas fontes de vagas entram no scanner — Atlassian, BWI, Computrabajo, Dover, Google, Kalibrr, Magneto365, Platsbanken, Tech in Asia e Zoho Recruit — o registro agora entrega 120 fontes (115 EN + 5 RU) com 115 adaptadores, e o role matcher mantém separados os títulos que diferem apenas por um código curto em maiúsculas.

Adicionado

  • 10 fontes portadas do pai 1.38.0, cada uma com seu adaptador de fetch-walk e uma suíte isolada de CI (fetchImpl falso, zero rede): Atlassian (feed JSON único sem auth, URLs iCIMS, allowlist https+icims.com), BWI (ItemList JSON-LD, JSON.parse-first), Computrabajo (LATAM, decodificação de entidades, «remoto» lê remote), Dover (paginação DRF por tenant, render Remote (International), quadros validados por UUID), Google Careers (primeiras páginas por frase, o empregador do cartão vence, dedup entre consultas), Kalibrr (agregador SEA, id do feed como id da linha), Magneto365 (LATAM, ritmo inter-página fixo), Platsbanken (API JobTech da Suécia, janelas por palavra-chave), Tech in Asia (chaves Algolia descobertas do build do site), Zoho Recruit (board de hidden-input, enriquecimento de detalhe limitado, moeda só ISO-4217).
  • Contagem do registro: 120 fontes = 115 EN + 5 RU · 115 adaptadores; os cinco gates de contagem regenerados do registro vivo; o fallback do dropdown #/scan, os links do cvstart.org e help §17 ×18 seguem.

Corregido

  • role-matcher (porto #5688): títulos que diferem apenas por um código curto em maiúsculas («HR BP» vs «HR GP», «AWS» vs «GCP», mesmo em escritas mistas) não são mais emparelhados por fuzz como a mesma vaga; os códigos são lidos do título cru com exclusões de palavras-ruído e o guard fica logo antes do ratio de Jaccard.
  • Os portos sobreviveram a uma revisão Opus 5-5 de rigor máximo (21 achados): throws em quadros silenciosamente vazios por deriva de marcação (google/bwi), varredura pré-abortida lança em vez de devolver [], ordem decode-then-strip nos scrapers LATAM (a forma XSS strip-then-decode), https+allowlist nos URLs de linha do atlassian, asserts de host que rejeitam portas, parse de páginas tardias dentro do try (parciais sobrevivem) e honestidade de testes (orçamentos contados exatamente, contratos de coerção fixados com verdade). 13 achados corrigidos; a cauda [spec] rastreada à parte.

Notas

  • Os cinco RU continuam web-ui-only (sem adaptadores upstream — de propósito); cada fonte EN tem seu adaptador.
  • Ratchet de cobertura: os seis maiores portos elevados a ≥95 % branch (27 casos novos); o código morto defensivo restante fica documentado por arquivo.
  • As fontes novas são selecionadas com provider: <slug> no portals.yml — nada muda para configurações existentes.

[1.252.1] — 2026-10-17

O banner de versão da wiki agora é verificado por um script em vez de uma varredura manual — a regressão v1.252.0 só o detectou manualmente — e a diretiva de locale sobrepõe explicitamente o language.output do perfil e o idioma da vaga.

Adicionado

  • scripts/wiki-banner-check.mjs — o gate que a regressão pedia: lê cada Home*.md num clone da wiki e compara o banner com package.json e com os contadores VIVOS do registro (scanner adapters do registro de portais — 105, não os 109 vencidos do banner; route modules de server/lib/routes/; UI locales do conjunto CHANGELOG). O regex do banner é agnóstico de idioma por FORMA: as páginas Home localizadas traduzem o rótulo e os contadores («Текущий релиз», «Aktuel udgivelse», «адаптеров сканера», «enhedstests») e seus números haviam derivado mais (5177) que a página EN (5245). --tests N recebe o número que o test:ci recém-executado imprimiu; --check-only proíbe o clone implícito para uso offline. Teste de fixture offline em tests/wiki-banner-check.test.mjs.

Corregido

  • O banner da wiki Home ×18 volta a ficar em sincronia (enviado com esta versão): a varredura da v1.252.0 mudava badges e links mas nunca a linha do banner, e nada mecânico o sinalizava. O gate acima o torna verificação bloqueante de release.
  • A diretiva de locale define a prioridade do idioma (buildLocaleDirective): «This UI locale overrides the profile’s language.output setting and the job posting’s own language». A própria regra 6 do pai manda escrever em language.output — sem a frase de prioridade o modelo resolvia as duas instruções contraditórias de forma inconsistente por locale, que era a dispersão da coluna de idioma na v1.252.0. Fixado por tests/locale-directive-language-output.test.mjs no texto da diretiva (17 locales não-EN + EN vazio).

Notas

  • Observação de FORMA no ta evaluate (§3, vigiar): a primeira execução ao vivo da v1.252.0 produziu um evaluate de 48 336 caracteres sem Block G e sem SCORE_SUMMARY — com forma de loop, não de idioma (CAR-70 é de idioma). Se se repetir no próximo release, terá tarefa própria no Linear, separada da CAR-70.
  • Pelo FIX-PROMPT: o pai fica somente-leitura; a sobreposição vive inteiramente na diretiva da UI.

[1.252.0] — 2026-10-17

O fork do pai está sincronizado com career-ops-hq/career-ops — 264 commits do upstream, pai 1.36.0 → 1.37.0 — com ambas as divergências do fork defendidas, e o deploy volta a fixar o pai em execução nessa base.

Adicionado

  • Modo de mercado vietnamita (vi) do upstream (modes/vi/, tuyen-dung / ung-tuyen): BHXH/BHYT/BHTN, thử việc, lương tháng 13, trợ cấp thôi việc — o 21º mercado na tabela de modos do pai.
  • Provedor do quadro Tech in Asia (upstream): lê o índice Algolia público job_postings que a própria UI de vagas de techinasia.com consulta — um quadro SEA de startups, ~300 vagas vivas na amostragem, 273 indonésias. As chaves são descobertas a cada execução a partir do build do site, o link próprio do empregador vence a página do quadro e o salário fica sem anexo (os campos de salário do índice são bandeiras, não dinheiro).
  • Oito novos documentos de sinais de legitimidade Block G (modes/oferta-signals/08–17): país na terminologia de benefícios, etiqueta de localização de plataforma de terceiros, licenciamento de agências, excesso de requisitos migratórios, conteúdo proibido por jurisdição, amplitude da faixa pay-transparency, pergunta ao advogado sobre salário mínimo, divulgação de AI-screening, divulgação de contrato fixo, poder de compra na realocação.

Alterado

  • Endurecimentos do pai que viajam com a base: a verificação story-provenance funciona por unidade e frase e mantém figuras user-cannot-confirm fora de negociação, ATS e saída de entrevista; o fact-gate do CV roda em cada renderizador e lê afirmações reformuladas e estruturadas; o LaTeX rejeita slots extraídos vencidos antes de escrever; a varredura launchd de follow-ups é pulada quando os follow-ups estão desligados; o update-system mantém sua varredura de edições locais dentro do limite de argv.
  • As divergências do fork sobreviveram ao merge: providers/telegram.mjs (parser exclusivo do fork, ADR-0001) e o LOCATIONISH_RE Unicode lookaround em telegram-channel.mjs (ADR-0002). O documento de quadros agora lista a linha telegram-channel aprimorada do upstream, a linha telegram exclusiva do fork, Tech in Asia e o max_pages padrão do Tencent elevado 20 → 100.

Notas

  • Sem mudança de comportamento do SPA nesta versão: é o trem de documentação mais a re-fixação do pai. O deploy agora roda o pai 1.37.0; a tabela de estados canônicos não é tocada pelo delta do pai — os status do tracker renderizam como antes.
  • No merge do upstream dois conflitos foram resolvidos: docs/SUPPORTED_JOB_BOARDS.md (ambas as linhas telegram mantidas) e test-all.mjs (o copyDirSync baseado em walkTree do upstream subsume o guard manual de checkouts aninhados por .git do fork; a lista de import é a união).

[1.251.1] — 2026-10-12

O filtro de especialidade agora coincide apenas com palavras DISTINTIVAS do papel — antiguidade e substantivos genéricos de trabalho não coincidem mais uma vaga por si mesmos — e o remote-qa separa forma de idioma para que uma deriva nunca marque A–G em vermelho.

Corrigido

  • O filtro de especialidade remove antiguidade e substantivos genéricos de seu comparador: «Senior PHP Developer» coincidia com «Senior Java Developer» pelo Senior/Developer compartilhado. O comparador mantém apenas tokens distintivos (PHP, Go, Platform, SRE, Backend…); um papel sem tokens distintivos («Software Engineer») retrocede à correspondência de frase completa. Os limites de palavra se mantêm: «Go» nunca coincide com «Golang» nem «Google».
  • O remote-qa separa forma de idioma: o indicador A–G é julgado apenas por códigos de blocos/SCORE_SUMMARY/looping; uma deriva report not in <locale> vai para seu próprio indicador e coluna de warnings (a tabela ganha a 4ª coluna que faltava; os códigos são armazenados por linha).
  • A barra do mapa é um verdadeiro 50/50 a 390 px (a divisão 58/42 deixava a busca com 124 px e truncava seu marcador), e a regra CSS morta .job-map-job-select foi removida.
  • CodeQL 552 — a variável sem uso OVERLAYS no teste design-qa removida.

Notas

  • As milas seguem adiadas pelo proprietário. A deriva de idioma ta fica como limitação DOCUMENTADA do backend hermes (2–26 % de prosa entre execuções; A–G se sustenta): o remote-qa a reporta como achado esperado. A lista de tokens genéricos é mínima de propósito — ampliá-la é uma linha por idioma.

[1.251.0] — 2026-10-12

O select do mapa agora filtra por ESPECIALIDADE das configurações (config/profile.yml → target_roles.primary): escolha «Product Manager» ou «Backend Engineer (Go / PHP)» e o mapa mantém apenas as vagas cujo título traz as palavras desse papel. O localizador de lista de vagas do v1.250.0 é removido — era a ferramenta errada (correção do proprietário).

Adicionado

  • Filtro de especialidade: o select lista as especialidades das configurações (target_roles.primary); escolher uma mantém apenas as vagas cujo título carrega um dos tokens distintivos do papel (por limites de palavra, sem maiúsculas — «Go» nunca coincide com «Google»); o Reset devolve todas.
  • O localizador de lista do v1.250.0 é removido — o substituto: filtro por especialidade, não lista de vagas (correção do proprietário).

Alterado

  • Two mobile-map height specs: the floor moves 600 -> 540 px (the toolbar takes ~90 px of the viewport).

Notas

  • As milas seguem adiadas pelo proprietário. A correspondência é deliberadamente SOMENTE pelo título com as palavras das configurações: empresas ou descrições trariam vagas alheias para a especialidade. Uma vaga fora de todos os papéis configurados continua visível em «Todas».

[1.250.0] — 2026-10-12

A página do mapa torna-se operacional: um localizador de vagas acima do mapa voa para qualquer vaga visível e abre seu cartão de prévia (inclusive no desktop), e uma busca de texto restringe o mapa por empresa ou cargo — ambos sobrevivem ao recarregamento.

Adicionado

  • Localizador de vagas: um select acima do mapa lista as vagas visíveis (pós-filtro) ordenadas por empresa+título; escolher uma voa ao círculo e abre a prévia — também no desktop.
  • Busca de texto: restringe o mapa por empresa+cargo (sem maiúsculas na comparação; o campo mantém o caso digitado), persiste em map-prefs e o Reset limpa.
  • O FAB não cobre mais a atribuição em #/map em todas as larguras.

Corrigido

  • Duas especificações de altura móvel: o piso vai a 540 px (a barra ocupa ~90 px do viewport).

Notas

  • As milhas quedam adiadas pelo proprietário ao próximo release. As 4 chaves i18n novas entram nos 18 dicionários e o snapshot montado é regenerado. O localizador sempre abre a prévia (navegar o derrotaria); o círculo é encontrado pela referência inversa __job — não _point, que o L.Circle já possui.

[1.249.0] — 2026-10-11

O mapa de vagas agora prioriza a relevância: uma escala calma de verde-petróleo a cinza destaca as melhores vagas, os filtros de nota e trajeto são persistentes, o celular ganha mapa em tela cheia com prévia ao toque e o tracker compartilha filtro e ordenação de trajeto.

[1.248.8] — 2026-10-11

Paridade com o pai: o fork e o web-ui sincronizam com o delta de 115 commits do upstream — uma nova fonte PageUp, as células URL do rastreador são analisadas como links markdown, o estado Assessment — além do trabalho de idioma ta/ja: relatórios em loop são cortados e nomeados, o esqueleto de cabeçalhos mantém A–G estável, e a cota de escrita lê apenas prosa.

Adicionado

Corrigido

  • Nova fonte PageUp (105 adaptadores EN / 110 fontes); células URL do rastreador analisadas como links markdown (o href é a chave de dedup); o estado Assessment chega via states.yml.
  • Relatório em loop cortado e nomeado (report looping (block B repeated)), esqueleto de cabeçalhos ## A) … ## G) para não-EN.
  • A cota de escrita lê apenas prosa (tabelas, SCORE_SUMMARY, código e URLs fora) — um relatório japonês com tabelas latinas não flaqueia mais.
  • /api/evaluate avisa report not in <locale> e o remote-qa imprime os códigos numa coluna.

Notas

  • Não portado (documentado): a infraestrutura scan-ats-full (fetchListing/listingIdentity/_connection-scope — o web-ui não tem varredura reverse-ATS), o gravador de histórico de expirados em liveness (o web-ui não escreve scan-history; o planejador puro é portado quando aparecer um escritor), coverage-check/discover/story-bank/pdf –ats/text mode/domain_filter (só CLI), o fix de req-id no dedup-tracker (o web-ui não tem dedup-tracker), README.vi.md (o conjunto de locales do web-ui é próprio). O fork do pai defende ADR-0001/0002: telegram.mjs sobrevive, LOCATIONISH_RE Unicode se mantém (o upstream reverteu para \b ASCII).

[1.248.7] — 2026-10-11

A função do cabeçalho do relatório agora lê a convenção real do pai — # Evaluation: {Company} — {Role} — em vez de assumir # <Função> — <Empresa>, então o rastreador não carrega mais «Evaluation: » como função; um relatório no idioma errado avisa em vez de salvar em silêncio; e o papo do modelo antes do H1 sumiu dos relatórios salvos.

Corrigido

  • O rótulo localizado é removido primeiro (Evaluation:, Оценка вакансии: e mais 16 variantes); após o rótulo Company — Role → a função é o lado direito; sem rótulo Role — Company → a esquerda; a parte que nomeia a empresa decide; a função nunca é a empresa nem uma palavra de rótulo — o fallback é a linha da postagem limpa.
  • cleanup-plan soma os N das linhas (— N dirty-URL line(s)), não conta arquivos — um arquivo com 5 URLs sujas dava 1, não 5.
  • Um relatório no idioma errado avisa: o prompt abre E fecha com a instrução obrigatória de idioma (todo o relatório, tabelas incluídas), e o pipeline valida o idioma do relatório salvo — a deriva aparece como aviso report not in <locale>.
  • Sem papo do modelo antes do H1 — o relatório começa na sua linha de cabeçalho.

Notas

  • Não portado e intocado (verificado no regression v1.248.6): o fetch embed, a estrutura de linhas do post («Компания : X»), o dedup «## Rejected», a rejeição da raiz de domínio antes do fetch, a honestidade de recon/link-check e os modos cleanup. A validação de idioma é um aviso — o relatório (já validado em conteúdo A–G + pontuação) é salvo; o operador vê a deriva e pode repetir a avaliação.

[1.248.6] — 2026-10-10

Polimento pós-regressão da cadeia do Telegram: uma postagem real mantém sua estrutura de linhas (o rótulo «Компания : X» e a linha da função são encontrados onde realmente estão), a função do rastreador vem do cabeçalho do relatório em vez de uma linha crua da postagem, e as superfícies operacionais (recon, cleanup-plan, link-check) relatam o que dizem relatar.

Corrigido

  • Uma postagem do t.me não cola mais em uma linha: <br> vira quebra de linha antes da remoção de tags e entidades HTML numéricas são decodificadas; o padrão do rótulo de empresa aceita «Компания : X» com espaço antes dos dois pontos.
  • A função do rastreador vem do cabeçalho do relatório (H1 # <Função> — <Empresa>); a linha de fallback é limpa — sem hashtags, sem emojis, ≤80 caracteres.
  • ## Rejected não se duplica mais: a seção é criada uma vez; cada rejeição adiciona sua linha dentro dela.
  • A raiz de domínio é rejeitada antes da requisição — uma URL de lixo não custa nada.
  • Recon calcula o sinal do contrato (honours rejected: verifica markPipelineRejected no código implantado); a contagem de marcas não imprime 0 duas vezes.
  • cleanup-plan relata números por categoria (t-role a mover, linhas dirty-URL, linhas test-row) em vez de um único grep -c que contava a própria linha de sucesso.
  • link-check trata 5xx do github.com como não verificados — atrás do muro de bots, não como links quebrados.

Notas

  • Não portado e intocado (verificado no regression v1.248.5): o próprio embed-fetch, o contrato de salto em pipeline, os testes offline _setLookup, a aceitação de ordinais uk, os limites de palavra no filtro de função e o modo cleanup-apply. A contagem por categorias do cleanup-plan lê os rótulos de saída do próprio script — renomeá-los quebraria os contadores em silêncio.

[1.248.5] — 2026-10-10

Endurecimento pós-regressão: entradas individuais do t.me agora buscam o próprio texto da postagem (forma embed), uma entrada rejeitada sai do pool do temporizador de avaliação do lado do servidor, e a numeração nativa de blocos num relatório ucraniano é aceita — além de um teste que usava a rede secretamente ficar offline para sempre.

Corrigido

  • As entradas do t.me finalmente são avaliadas: a página de uma única postagem serve apenas o cabeçalho do canal em HTML simples (~0,2 KB — «View in Telegram» e nada mais), o modelo recebia uma descrição vazia e cada entrada do Telegram morria na avaliação. A busca agora reescreve t.me/<canal>/<id> (e a forma de feed t.me/s/…) para a forma embed (?embed=1&mode=tme) — mesmo host, validação SSRF inalterada — e lê a postagem de .tgme_widget_message_text (fallback: og:description). Sem texto do widget → rejeição na etapa de busca (telegram post has no text, rejected: true) antes de gastar tokens do LLM.
  • O temporizador de avaliação para de pagar novamente pelas entradas rejeitadas (contrato do lado do servidor): uma entrada rejeitada agora é marcada em data/pipeline.md — sua linha sai da cerca de URLs e vai para uma seção ## Rejected com um motivo curto (idempotente, nunca duplicado). O temporizador pega URLs nuas da cerca, então uma entrada marcada é pulada naturalmente — sem depender das entranhas do script do temporizador. O recon imprime pipeline rejected marks: N + honours rejected: yes (server-side) em vez de buscar no script.
  • Relatórios ucranianos passam na validação A–G novamente: um modelo uk numera os blocos pela ordem do seu próprio alfabeto (А Б В Г Д Є Ж), e a passagem de lookalike contava apenas А/В/Е — exatamente os blocos C, D, F, G estavam faltando na produção (2/2 reproduções). O validador agora aceita a sequência ordinal ucraniana quando os cabeçalhos aparecem em ordem (a mesma disciplina que abjad para ar e devanagari para hi), e o prompt de avaliação para cada locale exige explicitamente A–G latinas.
  • O portão de palavras-chave de função dispara em palavras, não em substrings: as raízes linkedin.com/ e x.com/ passavam pelo portão porque lead/it viviam dentro do texto da interface da página. As palavras-chave agora correspondem em limites de palavras Unicode, e uma raiz de domínio (URL sem caminho) é rejeitada antes da chamada ao LLM.
  • Um teste unitário não precisa mais da rede ao vivo: o teste no-hints (tests/auto-pipeline-jd-gate.test.mjs) substituía o transporte HTTP, mas ainda fazia uma consulta DNS real — verde no CI só porque o runner tinha conectividade. O safe-fetch ganhou um hook _setLookup() (o próprio resolvedor é substituível em testes; o código de produção nunca o sobrescreve), e o teste resolve offline.

Adicionado

  • Modos de workflow cleanup-plan / cleanup-apply no deploy.yml: a limpeza de lixo de QA na produção (post-qa-cleanup) roda do lado do servidor sem SSH de um laptop. O log carrega apenas números — sem nomes de arquivos ou prévias de linhas, porque o log é público e os nomes de arquivos contêm nomes de empresas. A etapa Verify é pulada para os modos de limpeza.

Notas

  • Não portado: o compilador de localizações por limites de palavras e a tabela de estados USPS do pai (documentado desde v1.248.1). O contrato de salto de entradas rejeitadas vive no servidor (marcação em data/pipeline.md), não no script do temporizador — está fora do repositório e não é modificado de propósito.

[1.248.4] — 2026-10-10

A cadeia de resolução de função para entradas de Telegram: uma publicação cirílica com uma vaga real agora produz um relatório nomeado em vez de um arquivo unknown-role rejeitado — e a chamada ao LLM só dispara para entradas que valem a pena avaliar.

Corrigido

  • A cadeia de função funciona para publicações cirílicas: a lista de palavras-chave somou desenvolvedor/programador/engenheiro/analista/gerente/руководитель/timlid/lid/designer/tester/QA/arquiteto/especialista/diretor/DevOps/SRE (a antiga era só EN: 0 de 40 publicações reais recebiam uma função), o cabeçalho do relatório é consultado (parseReportHeader — o modelo escreve a função no H1) e a primeira linha com sentido da publicação é o último recurso.
  • A porta de identidade mudou para ANTES da chamada ao LLM: uma entrada sem empresa nem traço de função falha no passo fetch (rejected: true) — sem gastar tokens numa avaliação que seria rejeitada de qualquer forma.
  • t.me/s/<canal>/<id> não nomeia mais a empresa «S» — o segmento de pré-visualização é pulado; ambas as formas de link resolvem ao canal.
  • Autoetiquetas «Компания: X» em publicações do Telegram são honradas (e «…в компании X» no meio da frase para hosts de mensageria).

Adicionado

  • Falhas do auto-pipeline carregam rejected: true no erro SSE — o temporizador de avaliação do servidor usa a marca para pular a entrada do pipeline em vez de repeti-la a cada 2 horas.

Notas

  • Não portado desta vez: o compilador de localização com fronteira de palavra do pai e sua tabela USPS (documentado desde v1.248.1).
  • O bloco «Translations» do README é gerado de uma lista canônica, com um teste de paridade no CI (17 links por arquivo, o próprio nunca linkado) — ar/pl/uk ganharam o bloco; a deriva de separadores |/· eliminada.

[1.248.3] — 2026-10-10

Continuação da rodada de regressão: o temporizador de avaliação não pode mais arquivar lixo de Telegram sem nome em hipótese alguma, a limpeza QA não exclui mais vagas reais do Telegram, a atribuição do mapa volta a ficar visível e os chips do painel leem ≥ 4.5:1.

Corrigido

  • O auto-pipeline valida ANTES de salvar: um relatório sem pontuação 0–5 ou sem blocos A–G encerra a execução no passo evaluate (rejected: true no payload SSE) — sem arquivo de relatório, sem linha no tracker. As entradas de t.me que passavam na porta de comprimento da v1.248.2 produziram mais dois lixos depois daquela versão; agora entradas sem nome são rejeitadas com company/role not identifiable.
  • guessCompanyRole não inventa mais empresa de hosts de mensageria — t.me, telegram.me, vk.com, linkedin.com e afins estão em EMPTY_DOMAINS: uma publicação do Telegram retrocede ao nome do canal, e uma entrada sem nome é rejeitada em vez de arquivada como unknown-role.
  • post-qa-cleanup lê primeiro a pontuação do cabeçalho: o auto-pipeline remove o bloco SCORE_SUMMARY antes de salvar, então a velha checagem só-summary marcava toda vaga do Telegram salva como lixo — na produção 8 de 41 têm pontuação no cabeçalho e o --apply as teria excluído. A pontuação do cabeçalho (via parseReportHeader) é agora o sinal primário; o bloco fica como reserva. O dry-run imprime o motivo por arquivo.
  • A atribuição do Leaflet fica visível em #/map — o FAB de docs a cobria (violação da licença OSM); o canto sobe 96px.
  • Os chips do painel superam 4.5:1 — os rótulos dash-chip e a variante --manual migraram para --foggy-strong (antes 4.31:1).

Adicionado

  • Falhas do auto-pipeline carregam rejected: true no erro SSE — o temporizador de avaliação do servidor usa a marca para pular a entrada do pipeline em vez de repeti-la a cada 2 horas.

Notas

  • O desvio ar/ja não se repetiu na reexecução (ver v1.248.2).
  • A limpeza da produção ainda aguarda um sim explícito do usuário (CAR-61c); com esta correção a limpeza não pode mais excluir as vagas reais do Telegram.

[1.248.2] — 2026-10-10

Rodada de regressão: o temporizador de avaliação não transforma mais lixo do pipeline em «Last evaluation», a análise de SCORE sobrevive a formatos reais e o tracker finalmente fala o seu idioma.

Corrigido

  • Relatórios com JD vazio nunca mais são escritos. A porta de JD do auto-pipeline subiu de 50 para 200 caracteres antes da chamada ao LLM — páginas-placeholder (example.com/qa-v167-…) terminam em erro SSE + aviso no console em vez de um relatório …-t-role-<ts>.md que o painel mostrava como «Last evaluation» (42 arquivos na produção).
  • A análise de SCORE_SUMMARY sobrevive a formatos reais: «4,2/5», «4.2 / 5», «4.2», «SCORE :» validam; vírgula com 1–2 dígitos é decimal, com 3 é milhar (o código antigo lia «4,200» como 4.2 e passava a porta 0–5). Uma falha registra só a linha SCORE: — nunca o conteúdo do relatório (os logs de CI são públicos).
  • Os status do tracker são traduzidos: abas e badges mostram rótulos localizados nas 18 locales; o valor canônico em dados, filtros e URL não é tocado.
  • ETA honesta: «⏱ ~30s» virou «~2–4 min» em evaluate/advisor/relatório de mercado (a faixa real é 86–285 s), e o subtítulo do evaluate lê A–G.
  • Acessibilidade: o contador de API-keys do config e o do pipeline ultrapassam 4.5:1 (antes 4.31:1); chip de faceta com valor vazio não é mais renderizado sem nome acessível.
  • Polimento de UI: o Delete de buscas salvas parece destrutivo só com uma busca selecionada (senão desabilitado); o cartão EMAIL do perfil não corta mais em 1440 px; a pílula live-evals ganhou espaçamento ícone/texto inclusive RTL; o FAB de docs não cobre a atribuição do Leaflet; o «?» do help não quebra sob as abas de stats; usage ganhou respiro entre subtítulo e abas; health agora diz career-ops-ui.

Adicionado

  • scripts/post-qa-cleanup.mjs: dry-run por padrão, --apply com backups automáticos (*.bak-<ts>, relatórios movidos a qa/cleanup-backup-<ts>/): linhas ZZ-QA-TEST do tracker, relatórios *-t-role-* sem SCORE válido e entradas example.com do pipeline.

Notas

  • O desvio ar/ja da rodada de regressão não se repetiu na reexecução (ambos: lang ✓ + A–G ✓) — a entrada ta do language-check e o parser de SCORE seguem como defesa em profundidade.
  • A limpeza da produção aguarda um sim explícito do usuário: primeiro o plano em dry-run, depois --apply.

[1.248.1] — 2026-10-09

location_filter ganha os níveis always_allow e block_hard do pai (CAR-60) — uma string de localização multipaís sobrevive a uma lista de irmãos bloqueados, e um bloqueio explícito duro não pode mais ser resgatado.

Corrigido

  • always_allow vence block: com block: [romania, poland, croatia] e always_allow: [united kingdom], a célula mesclada do workable «London, United Kingdom · Romania · Poland · Croatia» passa — a região de origem é uma opção (o caso consumidor do workable #4806, agora na forma do pai).
  • block_hard vence always_allow: termos de nível de país que nunca devem ser resgatados por um acerto de always_allow em outro ponto da string.
  • strict: true agora conta block_hard como nível restritivo; a seção location_filter do docs/help documenta ambos os níveis ×18.

Notas

  • Não portado desta vez de propósito: o compilador de palavras-chave de localização com fronteira de palavra do pai e sua tabela de estados USPS (web-ui mantém correspondência por substring; a tabela serve a configurações centradas nos EUA — arquive sob CAR-60 se precisar). A forma de allow-list do teste consumidor do workable permanece como um segundo veredicto ao lado do novo, na forma do pai.

[1.248.0] — 2026-10-09

Paridade com o pai: cinco correções de provedores/liveness portadas — boards que mudaram de API continuam funcionando e um banner de encerramento na própria linha não passa mais despercebido.

Corrigido

  • Gupy segue a API até portal.gupy.io e envia os nomes de filtro que a API lê de fato (workplaceType/type) — os nomes plurais antigos eram ignorados em silêncio; country não é mais enviado (um país configurado gera um aviso com o nome da entrada). Empregadores confidenciais caem por correspondência exata de rótulo («Confidencial Seguros» sobrevive) e max_pages tem teto de 100 — a API responde 400 após o offset 10.000, e o aviso de truncamento agora sugere restringir palavras-chave.
  • Workable mantém todos os países de uma vaga multipaís: irmãos sob a mesma URL de vaga se dobram em uma linha («London, United Kingdom · Romania · Poland · Croatia») em vez de o primeiro país vencer.
  • Liveness: um banner na própria linha não faz parte da linha de cima — «Sign in if you want to apply» / «Get notified when this position is filled» abaixo da vaga é lido como a cláusula de fechamento que é, enquanto um genuíno «until filled» atravessando linhas ainda lê ativo.
  • Radancy lê um elemento irmão job-location na marcação legada de cartões (layouts da família Barclays põem a localização depois de </a>) — linhas sem localização própria ficam vazias em vez de emprestar a do vizinho.
  • Links de fallback do SmartRecruiters mantêm o slug configurado — uma vaga sem ref confiável sintetiza a URL pública a partir do slug do tenant, nunca do nome de exibição.

Adicionado

  • No-ports documentados: a correção _http de proxy fetchImpl do pai (web-ui não tem ramo de proxy — portá-la anularia a verificação de identidade da guarda de DNS-rebinding e adicionaria uma dependência undici que o repositório evita); verify-ats (+226 linhas) e cv-facts do pai (web-ui implementa seu próprio scoring ATS e parsing de CV); o dashboard Go TUI e scripts CLI do pai; um reordenamento de imports no Teamtailor (cosmético).

Notas

  • Lacuna conhecida levada como follow-up: o tier always_allow do filtro de localização do pai (o teste consumidor do workable sobrevive a um bloqueio UK por causa dele) não existe em web-ui; o port expressou o mesmo veredicto do consumidor por um teste de allow-list. Registrado para mudança separada.

[1.247.0] — 2026-10-09

A cauda da varredura de design CAR-49…58 está completa — todos os menores e polimentos restantes, mais o mapa escuro que a versão anterior adiou de propósito.

Corrigido

  • O mapa renderiza um tema escuro real. Zoom/camadas/atribuição escureceram na v1.246.0; agora os mosaicos também seguem: o modo escuro desenha a camada OSM com inversão CSS (sem chaves de fornecedor) e existe um preset Carto-dark com chave via MAP_TILE_DARK_URL (host permitido no CSP img-src). A camada é remontada ao vivo ao trocar o tema.
  • Callouts amarelos têm variante escura (config, batch, aviso do hero) com a mecânica de tokens da v1.246.0 — e uma regressão foi pega no caminho: .callout a sobrepunha .btn-primary; agora com escopo :not(.btn).
  • A URL do cartão de informações do Apply não quebra mais no meio da palavra.
  • Botões de cabeçalho do CV usam um único sistema de ícones (texto; fora o misturado 📁/📄/💾).
  • Abas de status do tracker em um único padrão de caixa — frase simples, com variações por locale (CJK/ar/hi/ta sem formas de gênero).
  • Ícones da barra superior são um conjunto único de SVG de traço — fora 🔔/🌙/🩺; o botão de tema desenha sol/lua via CSS.
  • O cartão Pipeline do painel perde o realce rosa (anel só em hover/focus).
  • Excluir buscas salvas parece destrutivo de verdade (.btn-danger, não um fantasma neutro).
  • A pré-visualização do pipeline tem estado vazio de verdade (título + dica + CTA, padrão do tracker).
  • O metadado de tempo/custo é um padrão único: UI.pageMeta desenha ⏱ + custo em uma linha sob o título (evaluate/deep/auto/mode-page/orientation).
  • Auto-pipeline perde o ✨ — o único h1 decorado volta à borda esquerda comum.
  • URL de docs do batch é link em linha.
  • interview-digest / orientation têm estados vazios desenhados (título + dica + CTA).
  • Export do career-plan fica desabilitado sem plano, ações em uma linha.
  • stats: fora o título duplicado «Market report» e o placeholder de região cabe (texto mais curto ×18, largura fixa).
  • Config não vaza mais caminhos absolutos — displayPath() do servidor mostra ~/… ou o nome do arquivo.
  • O helper do LLM_PROVIDER são três linhas curtas (×18 + pre-line), não um muro mono de 150 caracteres.
  • O cartão EMAIL do perfil mantém o endereço em uma linha (reticências; quebra só ≤480px).
  • O texto do Health não parte mais palavras («Run buttons» inteiro; overflow-wrap anywhere).
  • Código em linha do help sobrevive em RTL (isolado LTR, nowrap, rolável).
  • A pílula «Live evals» do hero não colide mais em ru/ar (flex-wrap, ícones não redutíveis).

Adicionado

  • Preset de mosaicos escuros (MAP_TILE_DARK_URL + attribution) documentado em código; a ampliação do CSP img-src cobre o host da Carto.

Notas

  • O padrão escuro é uma inversão CSS do OSM: a Carto sem chave fechou seus mapas atrás de API key (verificado), então o preset com chave é opcional, não o padrão.

[1.246.0] — 2026-10-09

Os dez principais achados da varredura sênior de design CAR-48 estão corrigidos — incluindo o bloqueador e os sete maiores.

Corrigido

  • Apply escuro: o link do cartão de informações volta a ser visível. O cartão mantinha a superfície clara no modo escuro; o link «Need Playwright? See …» ficava a ≈1.0:1. O .callout--info com tokens por tema (surface/border/text/link) eleva o link escuro a ≈7.3:1.
  • RTL não inverte mais composições numéricas. Pílulas de pontuação («1.5 / 5»), faixas de relatórios («≥ 4.5», «< 3.5») e o «/ 5.0» do painel ganham isolamento direcional (spans LTR); o dicionário ar reformula faixas numéricas em palavras onde a prosa pede.
  • O markdown do CV permanece LTR. Textarea e pré-visualização renderizam com dir="ltr" em árabe; o entorno segue RTL.
  • A coluna DATE dos relatórios nunca mais quebra uma data ISO (nowrap + numerais tabulares).
  • A barra lateral termina acima do HUD de uso (reserva de scrollport + fade) — o último item de navegação não rola mais para baixo do painel; espelhado para RTL.
  • Scan móvel @390: «Save search» totalmente visível — a linha de controles quebra e comprime em vez de cortar na borda.
  • Controles do mapa seguem o tema escuro (zoom, camadas, atribuição). Os mosaicos ficam como estão por design: a origem é fixada pelo CSP img-src do servidor.
  • O FAB de chat não cobre mais conteúdo interativo — o padding de rolagem o limpa (botões Disable do Portals, pílula de pontuação do Reports, entrada do two-pager, atribuição do Leaflet).
  • O registro de atividade fala o idioma da interface: 22 slugs de ação localizados ×18 locais (com fallback ao id cru) e chips de filtro traduzidos; a coluna TARGET permanece (o auto-pipeline a preenche).
  • Formulários de assessments ganham rótulos reais (frase simples, vínculo htmlFor/id) em vez de só placeholders.

Notas

  • Fora desta versão: mosaicos escuros do mapa (exige ampliar o CSP img-src — mudança de servidor, feita de propósito no cliente); variantes escuras dos callouts amarelos (config, batch) — a mecânica de tokens já fica pronta.

[1.245.0] — 2026-10-09

O tâmil (தமிழ்) entra como o 18.º idioma da interface — o primeiro novo desde a onda de 17, com paridade total de chaves.

Adicionado

  • Tâmil (ta): 1449/1449 chaves com paridade total com en, nenhum placeholder perdido, convenções índico-técnicas no estilo hi (provedores, modelos, caminhos e comandos permanecem em latino). detect() aceita ta, ta-IN, ta-LK; tâmil é LTR (garantido por teste).
  • docs/help/ta.md completo (33 H2 / 125 H3, contagens de fontes verificadas no registro vivo, âncoras canary do Hermes, YAML literal), além dos espelhos README.ta.md e CHANGELOG.ta.md.
  • Portões de enumeração 17 → 18: LANGS, sondas de detect, auditoria i18n, paridade de changelogs, guarda RTL, langs do CI — que também incorpora hi, antes ausente da lista.

Notas

  • A landing do cvstart.org segue com 17 locales por enquanto — traduzi-la é uma tarefa separada; a sincronização de conteúdo do site já publica docs/help/ta.md e o changelog ta.
  • REPORT_LABELS do report-header.js segue em 17 — o parsing de relatórios tâmeis chega com a geração de relatórios tâmeis no repo pai.

[1.244.2] — 2026-10-08

O polimento da página de escaneio: o lançador, a barra de status, o painel de reposts e os filtros são redesenhados como um chrome consistente — e a tabela de resultados não pode mais ser esticada por dados de boards.

Corrigido

  • As células de salário mostram apenas a faixa monetária. Alguns boards colocam toda a descrição de benefícios no campo do salário — a célula visível agora mostra só o trecho econômico e move o texto para o tooltip (antes: uma célula de 6 linhas esticando cada linha).
  • Linhas com título vazio mostram a empresa (e depois um travessão neutro) em vez de uma célula gigante em branco.
  • O badge de seniority nunca quebra em sua coluna de largura fixa.
  • Linhas sem pontuação mostram um «◎ —» esmaecido com tooltip explicando que a comparação com o two-pager não encontrou correspondências (antes: nada, o que parecia quebrado perto de linhas com pontuação).

Adicionado

  • Redesign do chrome: cartão lançador (linha de controles alinhada, botão primário dominante), barra de status do terminal com ponto de estado (ocioso/executando/concluído/erro) safe para reduced-motion, painel de reposts com teto de altura e cabeçalho fixo (um dataset de 1,560 clusters renderizava um painel de 155,000 px), e o bloco de filtros como grade responsiva uniforme com rodapé alinhado.

Notas

  • Todo o resto do #/scan é idêntico ao v1.244.1 — apenas o polimento do chrome e as quatro correções acima mudaram. Testes 5045 → 5068 unit.

[1.244.1] — 2026-10-08

Corrigido

  • Scan icons announce real values. The boost/fit/score icons’ accessible names leaked the raw dict templates (“Fit: {band}”, “Boosted by {by}”) — {band}/{by}/{score} are now substituted (band word, booster keyword, numeric score). [HIGH a11y, caught live in the v1.244.0 regression round]
  • Title pipes split correctly. A 4-segment title like «C++ | Rust | Go Developer | Onsite» no longer lifts “Go Developer” out as a “country” — the split demands the segment resolves to a real country (conservative Countries.detectCountry).
  • Meta dates render for every row. daysSince accepted date-only ISO, but the API serves full timestamps for ~83% of dated rows — those rows showed no date segment.

Notas

  • Todo o resto do #/scan é idêntico ao v1.244.0 — apenas as três correções acima mudaram.
  • Deliberadamente não feito: estado de filtros no hash, navegação de linhas por teclado e o parse de 5 lakhs INR (pendências anteriores).

[1.244.0] — 2026-10-08

Os resultados do scan se leem como uma lista, não como um muro: uma vaga = duas linhas, os sinais viram ícones, e um scan de 700 linhas é uma tabela paginada em vez de uma página de 37,000 px.

Corrigido

  • Anatomia da linha. Toda vaga é renderizada como uma linha delimitada de duas linhas: linha 1 o título, linha 2 company · location · source · date · work-type. O badge de boost, a faixa de title-fit e a pontuação de fit viraram ícones com nomes acessíveis localizados e tooltips (leitores de tela leem «Соответствие: сильное» / “Fit: strong”, não um muro de palavras); os badges de confiança e de realocação se recolheram no mesmo trilho. Senioridade e salário continuam como colunas auxiliares apenas no desktop.
  • Higiene do título. Um segmento final | Germany | Remote é separado do título para a linha de metadados — consciente de Unicode, apenas quando a cauda é um marcador real de work-type e o segmento do meio tem forma de lugar (pipes legítimos como “C++ | Rust | Go Developer” nunca são separados).
  • Fim do espalhamento horizontal. Layout de tabela fixo, min-width: 0, título/meta de uma linha com ellipsis e o texto completo no tooltip: zero overflow da página e zero scroll interno da tabela em 1440 px e 390 px (a página antiga estourava 675 px na largura de celular).
  • Paginação. Os resultados são renderizados 50 por página (25/50/100/200 selecionável) — um scan de 700 linhas agora tem ~4,600 px de altura em vez de ~37,000 px, e o DOM mantém ~1,000 nós em vez de 5,500+.

Adicionado

  • Testes de contrato de layout: a linha de duas linhas, os nomes acessíveis dos ícones (en + ru), o orçamento de nós da paginação e a guarda de contagem/ordem do title-fit rodam na suíte de navegador — o redesign não pode regredir em silêncio.

Notas

  • Testes 5036 → 5045 unitários (a suíte de navegador ganhou os contratos de layout), média de cobertura 98.14 % linhas / 89.34 % branches (piso 96/86).
  • O que o scanner encontra, as fórmulas de pontuação e o last-scan.json ficam intocados — o title-fit continua sendo apenas uma anotação.
  • Fora desta versão: o estado de filtros via hash de URL e a navegação de linhas por teclado (follow-up); a interpretação de 5 lakhs INR (pré-existente).

[1.243.2] — 2026-10-08

Corrigido

  • Versão implantável cortada como v1.243.2. A tag git v1.243.1 foi re-apontada após a publicação, e o git fetch --tags do servidor recusa corretamente sobrescrever uma tag existente — por isso v1.243.1 nunca foi implantável. Esta tag carrega a árvore idêntica (o fix do listener window do modal na v1.243.0) mais este aumento de versão. Nada mais mudou; o tarball do npm 1.243.1 já continha o fix.

Notas

  • O código do aplicativo é idêntico ao v1.243.1 — apenas a tag implantável é nova (o git fetch --tags do servidor recusou a v1.243.1 re-apontada).
  • Deliberadamente não feito: forçar a atualização da tag no servidor (nunca se move uma tag publicada).

[1.243.1] — 2026-10-08

Corrigido

  • O modal «Registrar resultado» agora realmente fecha ao navegar. A guarda do v1.243.0 ouvia hashchange em document, onde o evento nunca dispara — a correção chegou morta e o modal continuava bloqueando a próxima vista (detectado em produção pela rodada de regressão v1.243.1). A guarda escuta em window; o teste também despacha lá e foi verificado vermelho contra o código quebrado.

[1.243.0] — 2026-10-08

A camada do navegador protege seu trabalho e fala o seu idioma: nenhuma edição não salva se perde, nenhuma falha passa em silêncio, nenhum inglês silencioso numa UI traduzida, contraste AA em todo lugar.

Corrigido

  • Edições não salvas sobrevivem a re-renders. O editor do CV mantém um buffer dirty e o restaura em cada render — Cancelar, trocar o idioma ou navegar para o CV Studio agora perguntam em vez de descartar; o plano de carreira mantém o texto gerado da mesma forma. Falhas de carregamento em Configurações/CV Studio/plano de carreira/Memória/two-pager mostram um erro e desabilitam o Salvar em vez de sobrescrever o arquivo silenciosamente com conteúdo vazio.
  • O modal “Registrar resultado” não sobrevive mais à navegação. Abrí-lo no #/tracker e clicar em outra view o deixava bloqueando a página inteira (achado na regressão two-stand da v1.242.1) — agora ele fecha na troca de rota; seus irmãos das views de scan também param seus streams SSE e desanexam os listeners ao navegar.
  • A análise de salários e palavras-chave lê todos os alfabetos. Os chips de palavras-chave usam fronteiras de palavra Unicode (cirílico, c++, .net filtram corretamente em vez de zerar as linhas); um parser de salário compartilhado corrige $182.9K - $240K, от 100 000 руб, 100 тыс. руб., 50 000 Kč (antes 50 milhões de USD) e a semântica de faixas ($100-150K); um must-have “Alemanha ou Países Baixos” não conta mais como atendido e violado ao mesmo tempo.
  • Streams se resolvem exatamente uma vez. Uma conexão SSE interrompida entregava o erro duas vezes e podia deixar o Gerar desabilitado para sempre; todo estado terminal (done/error/close/throw, incluindo um processo filho morto) se resolve por um único caminho e reabilita o botão. O auto-pipeline descarrega seu último frame em buffer em vez de descartá-lo.
  • Privacidade e correção nos caminhos pequenos. O corpo do relatório de bug não carrega mais a URL da vaga avaliada nem os slugs de relatórios; a privacidade do CV mascara links linkedin.com/in/… soltos, nomes EM MAIÚSCULAS, e para de estragar datas europeias; a resolução de país fica com a correspondência mais longa e se recusa a adivinhar quando dois países são citados (“Cambridge, MA” não vira mais Reino Unido); o download de arquivos mantém nomes cirílicos.
  • Router e a11y. O link de pular não dá mais 404 em ontent; o item ativo da navegação carrega aria-current="page"; um <main>/um <nav>; Escape fecha o drawer mobile (que agora sai de verdade da ordem de tabulação quando fechado); scroll-padding-top mantém o foco visível além das barras fixas; o banner fixo não cobre mais a topbar.
  • O contraste é AA nos dois temas — os toasts do tema escuro eram branco sobre branco (1.10:1): toasts, botões, banners e links sobre --rausch agora resolvem por tokens invariantes de tema, guardados por testes de contrato WCAG calculados (≥ 4.5:1).
  • Todo o resto da rodada: o evaluate renderiza o aviso de truncamento; Memória/Configurações/two-pager se recusam a Salvar sobre um carregamento que falhou; o scan para de deixar seus streams SSE órfãos; os resultados do scan classificam boards servidos por API pelo registro do servidor em vez de uma regex que havia divergido; o “Proceed with Caution” do tracker é um aviso, não um badge vermelho; as estatísticas agregam status localizados; gráficos de barras são lidos corretamente em RTL; o FAB de docs não cobre mais o conteúdo em celulares; GET /app.js responde 404 em vez do shell do index; GET /api/runners lista suas ações.

Adicionado

  • O dinamarquês entra na detecção automática — um navegador da-DK agora recebe a UI em dinamarquês (o dicionário estava completo; era a detecção que não o enxergava); zh-Hant* mapeia para chinês tradicional. cv-diagnostics — a checklist de currículo — está totalmente localizado (28 chaves diag.* × 17). 26 rótulos de field-spec adicionados; 31 chaves mortas removidas; as listas de provedores não afirmam mais que o Hermes é “o último na ordem automática”; os aria-labels do docs-fab/menu estão localizados.

Notas

  • Testes 4812 → 5036 unitários, de navegador 118 + novos contratos de navegação/contraste, média de cobertura 98.15 % linhas / 89.34 % branches (piso 96/86).
  • Fora desta versão: o redesign do #/scan (Fase 4 — v1.244.0, spec já rascunhada); uma chave de dicionário dedicada export.docxFailed (a falha reutiliza common.error); 5 lakhs INR ainda é interpretado como 5 (pré-existente).

[1.242.1] — 2026-10-08

Corrigido

  • O arquivo da release novamente não traz symlinks de node_modules. A tag git v1.242.0 carregava um symlink node_modules na raiz e site/node_modules (um git add -A generalizado os escapou dos padrões de ignore com barra final), o que quebrou a extração tar do deploy no servidor — o deploy fez rollback com segurança e a produção permaneceu em 1.241.1. Ambos os symlinks saíram do rastreamento e os padrões de ignore foram ampliados; o tarball do npm nunca foi afetado (o npm exclui node_modules ao empacotar).

Notas

  • O código da aplicação é idêntico ao v1.242.0 — apenas empacotamento. O conteúdo de correção das fontes chega à produção com este deploy (v1.242.0 nunca chegou à produção; seu deploy no servidor reverteu pela falha de tar).
  • Deliberadamente não feito: re-etiquetar v1.242.0 (o npm já publicou 1.242.0; o tarball do registro está limpo — o npm exclui node_modules ao empacotar).

[1.242.0] — 2026-10-07

A camada de dados da varredura volta a ser confiável: fontes mortas voltam à vida, uma resposta malformada não pode mais se passar por um board vazio e uma URL de board hostil não pode mais redirecionar uma varredura.

Corrigido

  • Fontes mortas voltam à vida. justjoin (o envelope da API mudou para {data, meta.next.cursor}) lê o catálogo completo por um cursor walk com os novos campos de salário; nofluffjobs responde 400 sem salaryCurrency — a query exigida pelo pai é enviada e os resultados paginam (pageTo). himalayas e jobicy viam só as ~20 vagas mais recentes de mais de 100 mil — ambos agora percorrem os catálogos completos (paginação por offset / cursor com limites de página).
  • Um 200 com forma errada lança erro na página 1. ~40 fontes liam respostas malformadas ou de challenge como “0 vagas” e a varredura “concluía” em silêncio. Novas guardas em server/lib/sources/_shape.mjs (requireArray / requireContainer / requireObject) tornam a falha barulhenta em todas as famílias; uma falha numa página posterior mantém as páginas já coletadas; a paginação para pelo comprimento bruto da página, não pela contagem filtrada.
  • SSRF: checagens de fornecedor por substring substituídas por pins de host exato obtidos por parse. lever (clever.com contém lever.co), greenhouse (5 hosts + os hosts legados de path-slug boards[.eu].greenhouse.io), ashby, smartrecruiters, workable, gem REST, ibm, arbeitsagentur, hecklerkoch (só www), workingnomads (http://169.254.169.254 era alcançável apesar da guarda de DNS), remoteok, remotive, rss, successfactors. redirect:'error' em todo lugar; URLs de vaga precisam ser https: no host fixado (javascript: / data: / fora do host são descartadas).
  • Uma entrada mal configurada do portals não aborta mais a varredura inteira. resolveAdapter captura os throws dos adaptadores, detectApi é envelopado por empresa, adaptadores que lançam erro devolvem null; erros de fetch do comeet editam ?token= antes de chegarem a logs ou a registros de quarentena.
  • Paginação e frescor. workday percorre tenants além das 100 primeiras vagas (páginas por offset, MAX_PAGES); telegram mantém os posts MAIS RECENTES e pagina ?before= em vez de ficar silenciosamente com os mais antigos; tencent continua quando Count está ausente; taleo interpreta tabelas de cabeçalho em francês; avature para pelo comprimento bruto da página; carimbos de data em UTC (amazon com off-by-one a leste do UTC, torre, workingnomads, trudvsem); entidades HTML decodificadas (geekjob, getmatch, hh); mycareersfuture emite o formato completo de 12 campos da vaga.
  • Detecção de Remote mais rígida. arbeitsagentur, vdab, rippling, teamtailor: porcentagem de contrato (“Verpleegkundige (100%)”, bundesweit) e a palavra “distributed” (“Distributed Systems Engineer”) não marcam mais vagas presenciais como Remote.

Adicionado

  • tests/adapters-pin-coverage.test.mjs completa a cobertura de branches dos pins de host que os pins introduziram.

Notas

  • Testes 4419 → 4812 unitários, de navegador 118. Média de cobertura 98.15 % linhas / 89.36 % branches (piso do gate 96 / 86).
  • Fora desta versão: os achados do cliente da mesma rodada de revisão (Fase 3 — views, libs, CSS/a11y, i18n); sources/habr.mjs continua sem um arquivo de teste dedicado (isenção do ratchet).

[1.241.1] — 2026-10-07

As avaliações ao vivo funcionam de ponta a ponta em todos os idiomas, o mapa de vagas posiciona vagas longas com vários locais e um deploy não derruba mais uma varredura em andamento.

Corrigido

  • As avaliações veem o método inteiro. O orçamento de contexto (144 KB) ainda cortava o final de modes/oferta.md depois que o CV e o perfil eram incluídos, então toda avaliação ao vivo perdia seus últimos blocos. O orçamento agora é de 192 KB e o limite flexível do prompt de 256 KB, definidos uma só vez em llm-dispatch.mjs.
  • Relatórios no idioma escolhido. A instrução de idioma agora abre o prompt além de fechá-lo, e palavras comuns (role, team, design…) são escritas no idioma do relatório; só nomes, tecnologias e citações literais continuam em inglês. Corrige relatórios em híndi e japonês que saíam quase todos em inglês.
  • Avaliações lentas não terminam mais em HTTP 502. O timeout da avaliação é de 300 s (antes era o padrão do provedor, 180 s).
  • Letras de bloco em híndi. Um relatório que escreve as letras A–G em devanágari (## ए), ## बी) … ## जी)) é reconhecido, e SCORE: aceita dígitos locais (३.८, ٣٫٨, 3.8) e vírgula decimal.
  • Mapa de vagas: locais longos. Uma célula como “Berlin Office · Berlin · Germany · Bosnia · …” (mais de 300 caracteres) era recusada com HTTP 400 e a vaga ficava sem marcador. Agora é usado o primeiro lugar real (·, ;, |, /, quebras de linha; segmentos “Remote …”, “(HQ)” e “Office” são ignorados); só uma consulta com mais de 2000 caracteres é recusada.
  • Deploy x varredura horária. O deploy pausa os timers de varredura e de avaliação automática, espera uma em andamento terminar (até 15 min), reinicia e sempre retoma os timers — inclusive no rollback.

Adicionado

  • Mapa de vagas em cvstart.org nos 17 idiomas, e nos READMEs em inglês e russo.
  • Capturas do mapa em todos os idiomas: uma por idioma (scripts/capture-map-screenshots.mjs), nos 17 README, no cvstart.org e numa nova subseção da ajuda, “Como usar o mapa” (§33, ×17).

Notas

  • Testes 4404 → 4419, de navegador 118. QA remoto (todas as rotas, site, links e LLM ao vivo × 17 idiomas) verde em produção.
  • Fora desta versão: avaliações em streaming; o limite de 300 s continua rígido (um modelo local lento ainda pode atingi-lo) e a ponte do Hermes mantém a chamada sem streaming.

[1.241.0] — 2026-10-07

Versão de endurecimento a partir de uma revisão de código com 28 agentes (~190 achados verificados): as classes de segurança, queda de processo e perda de dados foram fechadas, além de um mapa de vagas contribuído por @bullitt186.

Adicionado

  • Mapa de vagas (#/map) — vagas do pipeline, da última varredura e do tracker em um mapa do OpenStreetMap, geocodificadas via Nominatim com um User-Agent identificável, uma fila de 1 req/s, back-off e cache em disco. O Leaflet é servido por este servidor (sem CDN); a CSP só ganha a origem dos tiles em img-src. Contribuição de @bullitt186 (#381).
  • Guarda de requisições — o servidor recusa um Host que não atende (DNS rebinding) e gravações cross-site e GETs com efeito colateral (/api/stream/*, /api/run/*, /api/geocode). Um proxy reverso que encaminha um nome público o lista em ALLOWED_HOSTS; o workflow de deploy grava esse drop-in do systemd a partir da URL pública.
  • Gate de cobertura — npm run test:coverage:gate (scripts/coverage-report.mjs) imprime a cobertura de linhas/branches por arquivo e falha abaixo de uma linha de base crescente.

Corrigido

  • Uma requisição malformada não derruba mais o servidor. O Express 4 ignorava handlers assíncronos rejeitados, então um campo de tipo errado em /api/deep, /api/auto-pipeline, /api/career-plan, /api/tracker ou um byte NUL em /api/stats encerrava o processo; agora são respostas JSON 400, e qualquer outra rejeição vira um 500.
  • Uma varredura abortada ou com falha não apaga mais os últimos resultados. O last-scan.json é mantido quando uma varredura é interrompida ou todas as fontes falharam, e uma varredura de uma única empresa faz merge em vez de substituir. Uma entrada mal configurada no portals.yml não aborta mais a varredura inteira.
  • As avaliações veem o método inteiro. Os arquivos de modo eram cortados em 16 KB, então o oferta.md (92 KB) perdia os blocos C–G; os limites por arquivo agora são de 128 KB para modos e 64 KB para o CV, e um corte é informado. Respostas vazias ou cortadas falham em vez de gravar um relatório vazio.
  • SSRF: os saltos de redirecionamento são reverificados quanto a destinos privados, toda resposta de DNS é verificada, o prazo da varredura cobre o corpo, as chaves *_BASE_URL só vão ao host do fornecedor (Hermes/Ollama: loopback ou LAN), o pandoc roda com --sandbox.
  • Travamentos do event loop: duas regex quadráticas (remoção de HTML, sanitização de markdown) substituídas por scanners lineares.
  • Perda de dados: excluir uma linha do pipeline mantinha a compensação das outras linhas; $& em uma URL corrompia o pipeline.md; os salvamentos de mock-interview, networking e deep-research sobrescreviam arquivos anteriores; o histórico de entrevistas mantinha os primeiros 40 turnos em vez dos últimos.
  • Camada de LLM: auto-pipeline e deep research respeitam LLM_PROVIDER e todos os provedores pelo despacho compartilhado, registram o uso e informam o truncamento; evaluate/test do Gemini não salvam mais um relatório; a OpenAI usa max_completion_tokens, e um OPENAI_MODEL exclusivo do Codex é enviado como gpt-5; a descrição da vaga é isolada como dado não confiável.
  • PDFs de relatório, deep e inline passam --skip-fact-check (a verificação de fatos do CV bloqueava qualquer relatório com números); o formato de página do perfil não é mais sobrescrito.
  • Scanners: a quarentena é indexada por URL (corrigir uma URL a tenta de novo), a deduplicação dentro da execução usa a URL canônica, entradas em branco nas listas não casam mais com tudo, gravações atômicas, linhas de histórico skipped_* não são “vistas”.
  • Controle de processos: o escalonamento para SIGKILL nunca disparava; execuções em lote matam todo o grupo de processos ao desconectar.
  • CI e release: o CI agora executa os gates de paridade do changelog, de workflow e de i18n; permissões de workflow de privilégio mínimo; segredos de produção apenas no ambiente production; o pacote npm agora inclui docs/help/.
  • Testes: nenhum teste toca mais a rede nem o projeto pai real (um CAREER_OPS_ROOT explícito é sempre respeitado); algoritmos copiados substituídos pelo código de produção.

Notas

  • Testes 4056 → 4404, navegador 118. Linha de base de cobertura do servidor: linhas 96,7 %, branches 86,3 % (scripts/coverage-baseline.json).
  • As especificações agora ficam em uma única pasta, docs/sdd/specs/; o plano do programa é docs/sdd/PLAN.md, a matriz invariante→gate é docs/sdd/TRACEABILITY.md.
  • Nota para operadores: atrás de um proxy reverso, defina ALLOWED_HOSTS=<host público>; uma URL base de Ollama/Hermes em um host público agora é recusada.

[1.240.0] — 2026-10-06

Paridade com o pai — career-ops main @ 62905981 (VERSION 1.35.0, 177 commits upstream desde b39931e). Seis novas fontes — 103 → 109 (104 EN + 5 RU) —, correções espelhadas em oito provedores existentes e o formato checklist do pipeline, contribuição de @bullitt186.

Adicionado

  • Seis novas fontes — 103 → 109 (104 EN + 5 RU), 98 → 104 adaptadores EN.
    • ADP Workforce Now (workforcenow.adp.com com cid + ccId) — paginação $skip pelas linhas realmente retornadas; uma resposta sem jobRequisitions nem meta lança erro em vez de ser lida como um quadro vazio; enriquecimento de detalhes opcional (adpWorkforcenow.fetchDetails, padrão 25, limite 100).
    • Gupy (employability-portal.gupy.io) — varredura por palavra-chave em todo o quadro, das mais novas para as mais antigas, com parada antecipada em since_days; workplaceType é lido como a string singular em pt-BR para que vagas híbridas não se percam; páginas de carreira confidenciais são descartadas.
    • JazzHR (*.applytojob.com) — parser de cards com fallback para âncora simples, de modo que um redesign parcial ainda produza vagas; links de vagas sem nenhum card analisado lançam erro, e um quadro realmente vazio retorna [].
    • Startup Jobs (startup.jobs/feeds/jobs) — RSS, uma única requisição; um corpo malformado, em HTML ou truncado lança erro em vez de retornar [] (a correção de revisão pós-merge do pai, 08fe5d06); CDATA e comentários são mascarados antes de casar os itens.
    • Taleo (<tenant>.taleo.net, tre.taleo.net recusado) — GET do shell e depois POST em searchjobs; um shell sem id de portal lança erro como quadro privado; colunas identificadas pelo cabeçalho.
    • UKG Pro / UltiPro (recruiting[N].ultipro.{com,ca}) — Top=50 + Skip, totalCount nunca tomado como condição de parada, limite de 100 páginas (com teto em 1500), páginas de detalhe opcionais analisadas com uma leitura de chaves ciente de strings.

Corrigido

  • Pipeline no formato checklist do pai — o career-ops upstream escreve data/pipeline.md como seções ## Pending / ## Processed com linhas - [ ] url | …, que a UI lia como 0 pendentes e onde gravava anexando um bloco delimitado (fence) no fim do arquivo, lugar que o pai nunca consulta. parsePipeline, addPipelineUrl e removePipelineUrl agora leem e gravam esse formato (deduplicação também contra as linhas processadas; compensação como segmento rotulado note: comp …); o formato com fence não muda. Contribuição de @bullitt186 (#380).
  • País incorporado à localização — Ashby (address.postalAddress primário), Breezy (country.name) e Recruitee (location plano) acrescentam o país, a menos que o nome já o traga como palavra inteira, de modo que uma vaga remota com o Reino Unido como principal e os EUA como secundário não é mais descartada por um filtro de localização.
  • MokaHR — o host regional hire-r1.mokahr.com é aceito (a allowlist é exatamente app.mokahr.com + hire-r1.mokahr.com), o POST da API e os links das vagas usam o host do próprio quadro, e um success:false na própria resposta agora lança erro em vez de ser lido como um quadro vazio.
  • Avature — os cards article--jobs são analisados (título a partir da âncora do cabeçalho, localização a partir do span icon-address).
  • SAP SuccessFactors — uma URL de busca salva /go/<Category>/<id>[/<offset>]/ resolve para a raiz do tenant; antes ela caía em …/tile-search-results/, que responde 200 com zero tiles.
  • Deutsche Bahn — o db.jobs esvazia qualquer página de resultados que contenha uma vaga que ele não consegue renderizar; a varredura agora ordena por pubExternalDate_tdt com 1000 por página (no máximo 5 páginas), para na contagem bruta de âncoras de resultados e lança erro diante de um cabeçalho de resultados ausente ou de links de vagas não analisados, em vez de retornar um quadro incompleto.
  • HigherEdJobs — um desafio do Incapsula respondido com HTTP 200 era analisado como zero itens e lido como um quadro vazio; agora lança erro.
  • Um teste lia o projeto pai real na máquina do desenvolvedor — tests/reports-list-cache.test.mjs apontava CAREER_OPS_ROOT para um diretório temporário sem cv.md/portals.yml, que o paths.mjs ignora, então localmente contava os relatórios do pai (6 em vez de 3) enquanto o CI, sem pai ao lado, passava.

Notas

  • Testes 3782 → 4056. Novas suítes: sources-adp-workforcenow, sources-gupy, sources-jazzhr, sources-startup-jobs, sources-taleo, sources-ultipro, sources-location-country-fold, sources-deutschebahn-blanked-page, sources-higheredjobs-challenge-page, mais parsers-pipeline-checklist do #380.
  • Pai: o fork foi levado ao main upstream (144 → 0 commits atrás) com as quatro divergências da ADR-0002 verificadas intactas; a reversão do LOCATIONISH_RE cirílico feita pelo upstream não foi incorporada.
  • Já entregue na v1.239.0, não repetido: AppliTrack, NEOGOV, Red Rover, SchoolSpring, OCC Mundial, quadros embutidos do Greenhouse, Workday myworkdaysite.com.
  • Não portado, com os motivos: egress via proxy confiável (CAREER_OPS_TRUST_PROXY_EGRESS, _http.mjs / _ip-guard.mjs / _dns-cache.mjs) — o web-ui varre pelo seu próprio transporte http-json.mjs; o registro de colunas de scan-history.tsv e a deduplicação de requisições por idioma — o web-ui lê as posições 1–12, que não mudaram, e deduplica por URL; requisitionId / language do SmartRecruiters — sem consumidor no formato de vaga do web-ui; Gem isoCountry → nome do país — exige a tabela alpha-3 de 249 entradas do pai, adiado; mudanças só de CLI (modos, atualizador, doctor, payload de PDF/ATS, scripts do tracker, dashboard em Go, modo Singapura).

[1.239.4] — 2026-09-29

Patch da regressão ao vivo da v1.239.3.

Corrigido

  • Avaliações em coreano e chinês tradicional acusavam falta do resumo de pontuação. Esses relatórios chegam a 10–11 mil caracteres e o resumo vem por último, então ficava além do limite de 8192 tokens de saída. As avaliações (e o pipeline automático) agora permitem 16384 tokens e, se um provedor ainda cortar a resposta, o primeiro aviso diz isso em vez de listar blocos ausentes.
  • A verificação ao vivo lia uma resposta curta em alemão como não alemã (3 palavras vazias alemãs, nenhuma inglesa). Uma resposta em lista pode ter poucas; sem nenhuma inglesa, três agora bastam.
  • Testes 3778 → 3782.

Notas

  • As rotas de pesquisa aprofundada e de modos mantêm o limite de 8192 tokens: suas respostas não chegam a ele nas execuções ao vivo.

[1.239.3] — 2026-09-29

Patch da regressão ao vivo da v1.239.2.

Corrigido

  • Avaliações em árabe ainda falhavam na verificação A–G depois que a v1.239.2 corrigiu japonês e hindi. Um esqueleto mascarado dos títulos (sem texto do relatório no log) mostrou o motivo: o relatório árabe escreve a letra de bloco em árabe, na ordem abjad, seja o que for que o prompt peça. A verificação agora aceita أ ب ج د هـ و ز como A–G quando a letra aparece sozinha no início de um título, como já aceitava os sósias cirílicos; as mesmas letras dentro de uma palavra árabe não contam.
  • A verificação ao vivo imprime esse esqueleto mascarado quando a verificação A–G falha, para diagnosticar o próximo caso pelo log.
  • Testes 3776 → 3778.

Notas

  • A nota da v1.239.2 dizia que a verificação não seria afrouxada; para o árabe agora é, porque três execuções ao vivo mostraram que só o prompt não segura a letra latina.

[1.239.2] — 2026-09-29

Patch da regressão ao vivo da v1.239.1.

Corrigido

  • Uma avaliação em árabe traduziu as letras de bloco e uma em japonês omitiu o resumo de pontuação. A v1.239.1 repetia só o idioma na última linha do prompt. Essa linha agora diz também que a letra latina A–G de cada título e o bloco ---SCORE_SUMMARY--- (marcadores e chaves em inglês) não são traduzidos, e pede a escrita própria do idioma.
  • Testes 3775 → 3776.

Notas

  • A verificação do relatório não foi afrouxada para aceitar letras ou marcadores traduzidos: é o prompt que mantém o formato estável.

[1.239.1] — 2026-09-29

Patch da regressão ao vivo da v1.239.0.

Corrigido

  • Avaliações em japonês e ucraniano voltavam quase todas em inglês. A instrução de idioma ficava no topo do prompt, antes de dezenas de kilobytes de arquivos do projeto e da vaga em inglês. Agora o prompt repete o idioma na última linha.
  • A verificação ao vivo marcava uma varredura como falha quando o servidor já executava outra (a varredura horária, repetida logo após o deploy). O servidor recusar uma segunda varredura é o correto; a verificação agora espera e tenta de novo, por até 30 minutos.
  • Testes 3774 → 3775.

Notas

  • A varredura única do servidor não foi relaxada: uma segunda varredura continua recusada com SCAN_BUSY. Só a verificação mudou.

[1.239.0] — 2026-09-29

Paridade com o career-ops @ b39931e: cinco novas fontes e três correções espelhadas.

Adicionado

  • Cinco novas fontes (98 → 103; adaptadores em inglês 93 → 98): AppliTrack (applitrack.com/<distrito>), NEOGOV (SchoolJobs / GovernmentJobs), Red Rover (jobs.redroverk12.com/org/<id>) e SchoolSpring (<distrito>.schoolspring.com) para vagas em escolas e no setor público, e o OCC Mundial, o maior site de vagas do México (provider: occ explícito com consultas de busca). Cada uma é fixada ao seu host, usa só HTTPS e recusa redirecionamentos.
  • Testes 3500 → 3774.

Corrigido

  • Quadros embutidos do Greenhouse (job-boards.greenhouse.io/embed/job_board?for=<quadro>) apontavam para um quadro chamado “embed” e falhavam; agora o quadro vem de ?for=, e só em um host real do Greenhouse.
  • Quadros do Workday em myworkdaysite.com (/recruiting/<tenant>/<site>) não eram reconhecidos; agora são escaneados, com os links das vagas no mesmo host.
  • Um nome de relatório com parênteses (042-acme-(berlin)-….md) era cortado no primeiro ) no tracker; agora o nome inteiro é mantido.

Notas

  • Mudanças do pai que só valem para a CLI não são portadas — modelos de PDF e CV, o scaffolder, o executor em lote, a importação de career-profile, a cobertura de palavras-chave ATS, as traduções dos modos de entrevista e o host CLI pi: o web-ui não os executa.
  • Abrir esse relatório a partir do tracker ainda exige que a rota de relatórios aceite parênteses no nome do arquivo; essa rota não muda nesta versão.

[1.238.3] — 2026-09-27

As páginas continuam responsivas durante um scan, e os logs da QA remota não vazam mais o host de produção.

Corrigido

  • O painel e os relatórios levavam segundos para carregar e davam timeout durante um scan. /api/reports e /api/dashboard liam e analisavam todos os relatórios a cada requisição — cerca de 3,7 s no servidor de produção, bloqueando qualquer outra requisição; com um scan rodando, os carregamentos batiam no timeout de 30 s. Os cabeçalhos analisados agora ficam em cache por arquivo e são renovados quando o arquivo muda, então listar custa uma verificação por relatório (500 relatórios: 430 ms → 3 ms localmente).
  • Um achado da QA remota podia imprimir parte do host de produção num log público. O GitHub só mascara um segredo quando ele aparece inteiro, e uma mensagem cortada em 180 caracteres terminava no meio do host. Achados e relatórios inteiros agora são censurados antes do corte.
  • Toda avaliação por IA era marcada como malformada. O prompt nunca pedia o bloco ---SCORE_SUMMARY--- que a verificação do relatório exige (os scripts de avaliação do projeto pai pedem), e um relatório traduzido traduzia a palavra “Block”, então a verificação não encontrava as seções. Agora o prompt pede o resumo e a letra do bloco em cada título, a verificação aceita uma palavra traduzida antes da letra e o bloco de resumo é removido antes de o relatório ser exibido ou salvo.

Adicionado

  • Testes 3488 → 3500.

Notas

  • O scanner continua rodando dentro do processo do servidor; esta versão remove o outro grande bloqueio, mas não move o scan para um worker.

[1.238.2] — 2026-09-27

Correção dos 502 que o servidor de produção ainda mostrava durante o scan horário.

Corrigido

  • Páginas e arquivos retornavam 502 durante um scan. O Node fecha uma conexão keep-alive ociosa após 5 s; o Caddy reutiliza conexões ociosas por até 2 minutos. Enquanto um scan dentro do processo mantinha o servidor ocupado, o Node fechava sockets nos quais o Caddy acabara de enviar uma requisição, e o Caddy respondia 502 connection reset by peer (62 vezes num scan, com o servidor saudável). Agora o servidor mantém conexões ociosas por 125 s — mais que o proxy —, então o proxy sempre fecha primeiro.

Adicionado

  • deploy.yml → verify mostra por que o proxy respondeu 502 (tipos de erro, endereços mascarados), os 502 por minuto desde o último reinício e o resultado do scan horário.
  • release.yml pode criar uma tag ausente quando iniciado manualmente, para publicar uma versão onde tags não podem ser enviadas.
  • Testes 3486 → 3488.

Notas

  • O scan continua rodando dentro do processo do servidor e pode ocupá-lo por segundos; movê-lo para um worker não faz parte deste patch.

[1.238.1] — 2026-09-27

Correção de estabilidade após a primeira regressão ao vivo do servidor de produção: um scan por vez e correções do site.

Corrigido

  • Dois scans ao mesmo tempo derrubavam o servidor. Cada GET /api/stream/scan rodava o scanner completo dentro do processo, sem proteção; um scan iniciado pela UI coincidiu com o timer horário e o processo caiu com JavaScript heap out of memory (duas vezes, com erros 502 no meio). Agora é um scan por vez: uma segunda requisição recebe um error SSE com código SCAN_BUSY e nada começa. O workflow de deploy também aumenta o limite de memória do servidor (heap_mb, padrão 448 MB).
  • /help e /changelog do cvstart.org rolavam para os lados no celular (e /changelog em ja/zh no desktop): código longo e o índice agora quebram linha.
  • O hindi faltava nas alternativas de idioma do sitemap; a checagem i18n do site agora compara o sitemap com o registro de idiomas.
  • A Ajuda §19 mandava adicionar a chave nos “8” arquivos de idioma — são 17.

Adicionado

  • Regressão remota (.github/workflows/remote-qa.yml): cada rota × cada idioma do app implantado (somente leitura), cada página × idioma × celular/desktop do cvstart.org, cada link dos README e da wiki e, sob demanda, um scan real e chamadas de IA ao vivo em cada idioma.
  • Workflow de deploy do servidor (.github/workflows/deploy.yml): recon, deploy (com rollback exato) e verify (teste de fumaça e relatório de estabilidade).
  • Testes 3481 → 3486.

Notas

  • O uso de memória do próprio scanner não é reduzido nesta versão: a proteção impede dois scans ao mesmo tempo e o drop-in eleva o teto. Perfilar um scan real de 98 fontes exige a rede de produção.
  • O timer horário career-ops-scan não muda; o recon agora mostra quais comandos ele executa.

[1.238.0] — 2026-09-27

Paridade com o pai — career-ops main @ 993085ce (VERSION 1.34.0, 182 commits desde de2224a9). Quatro fontes novas, dez correções de provedores espelhadas, uma brecha de SSRF em todo o scanner fechada, e uma dica gratuita de ajuste de título nos resultados do scan.

Adicionado

  • Quatro fontes novas — 94 → 98 (93 EN + 5 RU), 89 → 93 adapters EN.
    • Eploy (provider: eploy) — lê o sitemap /live-jobs.xml do tenant em qualquer host de marca (somente HTTPS; literais de IP, localhost, *.local, *.internal e hosts de rótulo único são recusados). Uma resposta que não é sitemap (uma página de login) lança erro em vez de passar como zero vagas. Páginas de detalhe são opt-in (eploy: { fetchDetails, detailLimit }).
    • HiringRoom (*.hiringroom.com) — JSON-LD /portal/jobs primeiro, cards legados de /jobs como fallback; URLs de vaga em http:// no payload são reconstruídas em HTTPS.
    • PeopleSoft Candidate Gateway — detectado pelo caminho /psc/{site}/EMPLOYEE/HRMS/c/HRS_HRAM_FL.HRS_CG_SEARCH_FL.GBL em qualquer host de marca. Ele mesmo segue os redirecionamentos de sessão de mesma origem do PeopleSoft (máximo de 10 saltos, cada salto reancorado à origem do tenant e verificado contra endereços privados), reproduz o formulário “carregar mais” e marca uma varredura parcial (peoplesoftIncomplete) em vez de reportá-la como completa.
    • PrevueAPS (*.prevueaps.ca) — resolve o id do site a partir da página de vagas e então lê a API JSON.
  • Dica de ajuste de título nas linhas de #/scan. Cada resultado carrega um chip aproximado de ajuste forte / relacionado / fraco comparando o título da vaga com os cargos-alvo em config/profile.yml (target_roles.primary + nomes de arquétipo). É uma estimativa gratuita por palavra-chave: nunca filtra, reordena ou muda contagens, e o tooltip diz isso. Calculado ao servir os resultados, então uma edição do perfil aparece no próximo refresh. Traduzido ×17. Espelha o #3261 do pai.

Corrigido

  • Segurança — a proteção contra DNS-rebinding nunca rodava em um scan real. http-json.mjs só verificava os endereços resolvidos quando recebia o fetch global puro, mas ambos os scanners injetam makeTimeoutFetch(), então todo scan pulava a proteção. Uma fonte apontando para um hostname que resolvia para 10.x ou 127.0.0.1 era buscada. A proteção agora roda dentro do próprio makeTimeoutFetch, correndo em paralelo contra o timeout do scan para que um resolver travado não consiga travar um scan. Encontrado na revisão desta versão.
  • Segurança — endereços IPv6 que embutem um endereço IPv4 não eram tratados como privados. http://[::ffff:127.0.0.1]/ (que o parser de URL reescreve como [::ffff:7f00:1]) passava no isValidJobUrl. Tanto a forma pontilhada quanto a hexadecimal de ::ffff:a.b.c.d / ::a.b.c.d agora são classificadas pelo endereço embutido. Do _ip-guard.mjs do pai.
  • Rippling migrou para a API v2 do board (ats.rippling.com/api/v2/board/<slug>/jobs), com paginação (10 páginas por padrão, máximo 50), trava de host nos links de vaga e validação de slug. Uma URL de API colada agora resolve para seu slug real em vez de api.
  • Recruitee — ofertas multi-localização listam todos os locais (A · B), publicações demo (Sample) e linhas sem título são descartadas, e uma oferta é mantida quando qualquer uma das suas duas URLs é válida.
  • Pinpoint — publicações demo semeadas (o vídeo demo duplicado) são descartadas.
  • Canais do Telegram — uma primeira linha com vários | não é mais lida como Título | Empresa, e mais palavras de cargo são reconhecidas. O LOCATIONISH_RE com suporte a cirílico do fork permanece intocado (ADR-0002).
  • Workday — um 422/401/403 na primeira página é sondado uma vez; a página de manutenção ou o redirecionamento de outage do Workday marca o board como morto (colocado em quarentena no modo estrito). As requisições CXS e de detalhe agora recusam redirecionamentos.
  • BambooHR — um /careers/list redirecionado é triado pelo feed público de embed: vazio significa um board ativo sem vagas, qualquer outra coisa é um board morto.
  • Heckler & Koch / Rheinmetall — um heckler-koch.com / rheinmetall.com nu é reescrito para www. para que o transporte sem redirecionamento ainda o alcance. Ashby recusa redirecionamentos. beesite ritma páginas com o delay() cancelável.
  • Filtros de título dobram acentos dos dois lados (scanners EN e RU), então Développeur é pego por um negativo developpeur.
  • A correspondência de empresa do cooldown entende nomes não latinos e formas corporativas (marcadores CJK/coreanos, İ turco com ponto). Nomes CJK antes colapsavam para uma chave vazia e nunca davam match.
  • location_filter.strict: true falha fechado em uma localização vazia quando uma lista de permitidos ou bloqueados está configurada.
  • Liveness — novas verificações de API pública para embeds do Greenhouse (gh_jid), Arbeitsagentur e We Work Remotely; SmartRecruiters agora decide pela flag active da vaga.
  • PeopleSoft — uma ação de formulário relativa “carregar mais” é resolvida ao lado da página de busca, não na raiz da origem.

Notas

  • Testes 3210 → 3481. Novas suítes: sources-eploy, sources-hiringroom, sources-peoplesoft, sources-prevueaps, title-fit, fetch-timeout-dns-guard, cooldown-company-match-unicode, liveness-api-more-rungs, ru-scanner-title-accent-fold.
  • Não portado, com motivos: bordas de palavra Unicode no filtro de localização (o filtro do web-ui é por substring, então não há borda a corrigir — um port completo e deliberado do filtro de localização do pai fica como trabalho futuro); dedup de requisição por mesmo título (web-ui dedupa só por URL); avisos de repostagem de agregadores, escopo de domínio em blacklist, recibos de zero-não-verificado e a migração do scan-runs.tsv (web-ui não tem nenhuma dessas superfícies); o split por facetas e as tags de truncamento do Workday (web-ui busca uma página por board); enrichDate do BambooHR (sem hook de enriquecimento de data); a refatoração do sleep do _http.mjs do pai em ~20 provedores (web-ui já usa um delay cancelável compartilhado); updater, doctor, PDF, tracker/merge-tracker, validate-profile, discover-new-companies, o app Next.js de web/, bots do GitHub — exclusivos de CLI ou repassados.
  • Mantido deliberadamente diferente do pai: Heckler & Koch / Rheinmetall permanecem somente HTTPS (sem upgrade http→https); um board morto do Workday em um scan normal ainda retorna [] em vez de falhar o scan.

[1.237.1] — 2026-09-22

Patch de uma verificação de QA externa sobre a v1.237.0. A correção do Jobstreet foi confirmada de fora pela primeira vez: nos hosts AU, NZ, HK e MY, a forma antiga /id/job/<id> retorna um 404 limpo e a nova /job/<id> alcança uma rota real protegida por Cloudflare; em id.jobstreet.com vale o inverso. Os links perdidos eram reais. Dois defeitos foram encontrados, ambos no que a própria v1.237.0 havia adicionado.

Corrigido

  • O rótulo do link do livro estava localizado, mas seu destino não. Todos os 17 locales da SPA e todos os 17 locales do cvstart.org apontavam o link de rodapé de Agentic Coding Design Patterns para a edição em inglês, enquanto o único rótulo traduzido — russo, «Паттерны агентного кодинга» — prometia um livro em russo. Um novo aplicador data-i18n-href (no mesmo formato de data-i18n-title, restrito a valores absolutos https://) e uma chave footer.patternsUrl enviam ru para a edição russa e todo o resto para a inglesa. O teste de regressão afirma o invariante nas duas direções: um título traduzido implica uma URL localizada.
  • O SEEK está migrando de hosts, e o transporte recusa redirecionamentos. www.seek.com.au → au.seek.com e www.seek.co.nz → nz.seek.com: as páginas de detalhe já retornam 301, e ambos os pares de hosts servem hoje a API de busca v5 com 200. Como http-json.mjs define redirect:'error' (uma defesa contra SSRF que vale a pena manter), no dia em que o caminho da API começar a redirecionar, uma entrada AU/NZ no host antigo falharia — e com apenas os hosts www. na allowlist, ninguém conseguiria migrar. au.seek.com e nz.seek.com agora estão em ALLOWED_JOBSTREET_HOSTS; os hosts antigos permanecem, então a configuração existente e os links já construídos continuam funcionando. Imitações (au.seek.com.evil.com, au-seek.com) continuam sendo recusadas.
  • Wiki: a linha de Agentic Practices no mapa de documentação da Home era a única entre doze que não fazia o escape de | dentro de [[…|…]], então o GitHub cortava a célula — link morto, descrição oculta. Um único caractere.

Notas

  • Contagens inalteradas em 94 fontes (89 EN + 5 RU), 89 adaptadores. Testes 3201 → 3210 (+5 locale do link de rodapé, +4 hosts do SEEK).
  • Não fechável de fora, registrado em vez de reivindicado: Oracle Cloud (a Akamai bloqueou as duas sondas REST), a fixação do Personio e o appendWorkType (precisam de uma entrada em portals.yml, e a UI não tem como excluir uma), e os casos de string de liveness (sem página controlável; 40 sondas ao vivo deram 12 ao vivo, 28 inconclusivas, 0 falsas expiradas).
  • O Help ×17 agora lista ambas as formas de host para AU-Main e NZ-Main. Estrutura de títulos inalterada em 32 H2 / 122 H3.

[1.237.0] — 2026-09-21

Paridade com o pai — career-ops main @ 93c4302b (VERSION 1.33.0, 75 commits trazidos de career-ops-hq/main). Nenhuma fonte nova desta vez: quatro correções espelhadas, duas das quais estavam perdendo vagas aqui, silenciosamente.

Corrigido

  • Jobstreet / SEEK — toda vaga australiana, neozelandesa, cingapuriana, malaia e de Hong Kong apontava para uma página morta. parseJobstreetItem construía a URL de detalhe como ${baseUrl}/id/job/${id} incondicionalmente. O segmento /id/ não é um caminho — é o prefixo de localidade indonésio, e só existe nos hosts indonésios. Todo outro host da plataforma SEEK responde 404 em /id/job/<id>. O web-ui já havia adicionado www.seek.com.au, www.seek.co.nz, sg.jobstreet.com, my.jobstreet.com e hk.jobsdb.com a ALLOWED_JOBSTREET_HOSTS, então a varredura retornava essas vagas tranquilamente — cada uma apontando para um 404. O caminho agora é definido pelo host: ID_LOCALE_HOSTS (id.jobstreet.com, www.jobstreet.co.id, jobstreet.co.id) mantém /id/job/, todo o resto recebe /job/, e uma origem não analisável cai para o caminho comum em vez de lançar exceção. Dois de nossos próprios testes tinham codificado o bug — eles afirmavam hk.jobsdb.com/id/job/… e www.jobstreet.com/id/job/… — e foram corrigidos.
  • Oracle Cloud — uma página curta no meio da lista encerrava a varredura e derrubava a cauda do board. A condição de parada era listLen === 0 || listLen < PAGE_SIZE, mas o ORC serve menos linhas que o limite no meio de uma lista porque algumas linhas são filtradas no servidor. Medido a montante na American Express: TotalJobsCount 454, páginas de 200 / 199 / 54 — o 199 era lido como o fim, e as últimas 54 vagas, 12% do board, nunca eram buscadas. Agora uma página vazia sempre encerra a varredura; quando o tenant informa um total, a paginação continua até offset + PAGE_SIZE >= total; só um tenant sem total contra o qual conferir encerra numa página curta. Isso segue a convenção mais ampla de que uma API «pode retornar menos resultados do que o número solicitado … mesmo que não esteja no fim da coleção» (Google AIP-158). hasMore permanece deliberadamente ignorado — alguns tenants retornam hasMore:false em toda página de um board de 7000 vagas. Com páginas curtas não encerrando mais a varredura, MAX_PAGES agora é a única salvaguarda contra um tenant informando um total falso, por isso está coberto por seu próprio teste.
  • Liveness — “This role is closed” não era reconhecido, e uma expiração falsa é permanente. O padrão era /this job (listing )?is closed/i, que não captura as formulações igualmente comuns com role e position; em um board a montante, 111 de 111 vagas incertas usavam “role”. Ampliado para /this (?:job|role|position)(?: listing)? is closed\b(?!-)/i. O \b(?!-) final é uma guarda contra adjetivo composto: \b sozinho rejeita closedown, mas ainda casa com uma prosa real como “This role is closed-loop control of the platform”, e é o lookahead que impede isso.
  • Liveness — job expired agora fica em um nível brando que não pode superar um botão Apply visível. HARD_EXPIRED_PATTERNS é verificado antes do teste de controle de candidatura, então qualquer coisa colocada ali vence sobre uma vaga ativa. /\bjob expired\b/i disparava falsamente em quatro formatos reais de página ativa — um item de carrossel “Similar jobs”, um chip de filtro “Hide job expired”, um FAQ de rodapé e a prosa de closed-loop acima — porque o classificador recebe o texto da página inteira. Agora ele vive em SOFT_EXPIRED_PATTERNS, verificado depois do controle de candidatura e antes da heurística de página de listagem, retornando expired_body_soft; um banner isolado “JOB EXPIRED” sem controle de candidatura ainda expira. Essa direção importa porque um expired falso é gravado no histórico de varredura como skipped_expired e então filtra por deduplicação uma vaga real para fora de toda varredura posterior, indefinidamente, a menos que scan_history.recheck_after_days esteja definido.

Adicionado

  • Personio — um personio: <slug> explícito fixa o tenant. Muitas empresas incorporam seu board do Personio como um iframe em uma página de carreiras com marca própria, então careers_url aponta para o próprio domínio da empresa enquanto o feed vive em <slug>.jobs.personio.de; sem uma fixação, esses boards não resolvem a nada. O slug tem o conjunto de caracteres restrito por uma allowlist ancorada (/^[a-z0-9][a-z0-9-]{0,62}$/i), e a URL resultante ainda passa pela análise existente de new URL(), pela checagem de HTTPS e pelo portão de host PERSONIO_HOST_RE — a fixação nunca é uma forma de alcançar outro host. Um slug contendo ., /, @, :, .., espaço em branco, um - inicial, ou que exceda o limite de 63 caracteres de um rótulo DNS, cai para o comportamento atual de api/careers_url. Diferente do pai, a regex de host do web-ui diferencia maiúsculas de minúsculas, então a fixação é normalizada por new URL() — AcmeGroup resolve para acmegroup.jobs.personio.de em vez de construir um host que sua própria allowlist rejeitaria depois.
  • Jobstreet — appendWorkType, opcional (opt-in) adiciona ao título os tipos de trabalho da vaga como sufixo, por exemplo Strategy Consultant [Part time], para que filtros de título e a triagem enxerguem o tipo de contratação no momento da varredura. Desativado por padrão; um workTypes ausente ou vazio é uma operação nula.

Notas

  • As contagens de fontes e adaptadores permanecem inalteradas: 94 fontes (89 EN + 5 RU), 89 adaptadores EN. Esta versão move apenas a contagem de testes, 3164 → 3201 (+13 jobstreet, +7 Oracle Cloud, +9 liveness, +8 Personio).
  • O pai excluiu providers/telegram.mjs, consolidando em telegram-channel. O web-ui registra ambos como fontes voltadas ao usuário, então a exclusão não é seguida: ela derrubaria o registro de 94 para 93 e quebraria silenciosamente quem tiver telegram configurado. Revisitar somente como uma remoção deliberada e anunciada.
  • jd-skill-gap.mjs ganhou uma forma de cabeçalho “what we are looking for” sem contração e uma remoção de marcador em negrito. O web-ui repassa esse script em vez de espelhá-lo, então a correção chega junto com o pai implantado e não exige mudança nenhuma aqui.
  • Não portado, com os motivos: lib/latex-escape.mjs, browser-extract.mjs, openai-eval.mjs, update-system.mjs e o scaffolder (o web-ui não os espelha nem os invoca via shell); a TUI do dashboard em Go e o formulário de configuração de web/ (superfícies separadas); doctor.mjs e analyze-patterns.mjs (repasses somente leitura cuja falha branda absorve a mudança).
  • A divergência cirílica \p{L} do fork em providers/telegram-channel.mjs sobreviveu intacta à mesclagem do upstream e foi verificada byte a byte contra uma cópia anterior à mesclagem.

[1.236.0] — 2026-09-18

Paridade com o pai — career-ops main @ 6a9c84c (VERSION 1.33.0, 128 commits). Duas novas fontes, e três correções espelhadas — duas das quais estavam perdendo vagas aqui, silenciosamente.

Adicionado

Duas fontes de varredura, 92 → 94 (89 EN + 5 RU). Python.org Jobs (pythonorg) lê o feed público do board; a atribuição de empregador é obrigatória, então uma linha cujo título não nomeia nenhum empregador é descartada em vez de ser preenchida retroativamente com o nome da entrada do portal. Generalist World (generalist-world) faz scraping dos cards do board e falha ruidosamente diante de um markup que não consegue analisar — um formato de card que mudou lança exceção em vez de ser lido como um board vazio, que é o modo de falha que torna uma fonte via scraping não confiável. Ambas aparecem no filtro Fonte de #/scan, ambas têm o host fixado (regex ancorada, somente HTTPS), e ambas são conduzidas de ponta a ponta por suítes com transporte falso (43 e 36 casos).

Corrigido

Uma vaga do Workday aberta em várias cidades era descartada por um filtro de localização. O endpoint de listagem do Workday responde a uma vaga multi-localização com uma contagem, onde toda outra vaga carrega um lugar: "53 Locations". Isso não é uma localização, e buildLocationFilter casa por substring sem diferenciar maiúsculas de minúsculas — então uma vaga aberta em Austin e em outras 52 cidades não casava com nenhuma entrada allow: [austin] e era descartada, enquanto a mesma vaga listada isoladamente passava. Medido a montante, em três tenants: 53 de 291 vagas eram placeholders, então este é o caso comum, não um caso de borda. Os lugares reais da vaga existem só no documento de detalhe do CXS, então cada placeholder agora custa um GET extra contra ele, limitado a 200 por entrada — além do limite, o placeholder é deixado visível em vez de ser forjado, porque uma vaga não resolvida nunca deve parecer resolvida. Um detalhe que falha ou que não traz lugar algum deixa a vaga exatamente como a listagem a retornou: um "53 Locations" visível é honesto sobre o que não pôde ser resolvido, enquanto uma localização em branco passaria silenciosamente por todo filtro. O mesmo GET também traz startDate, então uma vaga enriquecida é datada com exatidão, em vez de pela prosa relativa do endpoint de listagem ("Posted 30+ Days Ago" → undefined); tudo o que não for um YYYY-MM-DD puro é deixado como está, em vez de passar por Date.parse, cujo fallback não-ISO é definido pela implementação. Um nível declarado não colapsa mais duas requisições em uma única repostagem. "Insurance Specialist II" é a própria declaração do empregador de que esta requisição não é irmã da outra — uma faixa salarial, escopo e número de requisição diferentes sob um mesmo título-base. O tokenizador não conseguia enxergar isso: w.length > 3 descarta todo numeral romano até VIII e todo dígito isolado, então "Insurance Specialist" e "Insurance Specialist II" eram tokenizados de forma idêntica, pontuavam um índice de Jaccard perfeito, e o detector de repostagem sinalizava a segunda como uma repostagem da primeira. O role-matcher.mjs do web-ui não tinha tratamento de nível nenhum, então isto traz o mecanismo inteiro do pai: níveis são extraídos com romano e arábico dobrados em um único número ("Nurse II" e "Nurse 2" são a mesma declaração — e esse par diferia erroneamente antes), dois níveis declarados precisam concordar, e um nível presente em apenas um dos lados é tratado como uma reescrita solta, a menos que cada título também carregue uma palavra que falte ao outro, o que são duas discordâncias, e não um enfeite. Um board do Avature fixado em um filtro percorria o board global inteiro mesmo assim. A UI de busca do Avature gera parâmetros de faceta opacos (?42386=[812132] é um filtro de país), e um operador que fixa um board restrito em portals.yml espera que a varredura percorra esse. resolveSearch reconstruía a URL como ${origin}${path} e descartava a query string por completo — então toda requisição, incluindo a primeira, percorria o board inteiro do tenant: mais páginas, mais requisições, e vagas que o operador havia deliberadamente filtrado. As facetas agora acompanham toda requisição paginada, com nosso próprio jobOffset aplicado por último, de modo que uma faceta perdida de mesmo nome jamais possa sequestrar a paginação.

Segurança

devalue 5.8.1 → 5.9.2 (transitivamente via astro), fechando o único alerta aberto do Dependabot — uma negação de serviço diante de entrada malformada. Dependência exclusiva do site; nenhum código de runtime mudou.

Notas

Não portado, cada um com o motivo. O pai deu ao lever um timeout de 30s e ao manfred um de 25s, contra seu padrão compartilhado de 10s — um não portado: o transporte de varredura do web-ui já permite 60s (DEFAULT_SCAN_TIMEOUT_MS), então ambos os boards já têm mais margem aqui do que a correção concede lá. O prefixo careers- duplicado do iCIMS também não pode ocorrer aqui: o web-ui obtém a origem do tenant diretamente de api/careers_url e nunca constrói um host a partir de um nome de tenant; seu novo percurso de host alternativo é um recurso para a configuração mais permissiva do pai, não a correção de um defeito. deutschebahn e manfred ganharam nova tentativa em abortos transitórios, o que o teto de 60s do web-ui torna bem menos urgente — anotado em vez de portado, para que um futuro timeout de percurso longo já tenha uma primeira coisa conhecida a tentar. Todo o resto no delta de 128 commits é CLI, verificador, dashboard ou web/ (o próprio frontend do pai): liveness-browser, check-jd-archive, verify-ats/verify-portals/verify-cv-facts, update-system, set-status, audit-portals, discover-ats, scaffolder, test-all, manifestos de plugin. Divergências do fork reverificadas após o pull: a correção cirílica de \p{L} em providers/telegram-channel.mjs, o providers/telegram.mjs próprio do fork, hermes em web/src/lib/clis.ts. O career-ops-hq/main upstream está totalmente mesclado (0 commits faltando). 3066 → 3164 testes; a ajuda agora declara 94 adaptadores (89 EN + 5 RU) em todos os 17 pacotes, e a estrutura de cabeçalhos permanece inalterada em 32 H2 / 122 H3.


career-ops-ui v1.267.1 · page 2/9 · mirror of the project changelog for cvstart.org