Changelog
GitHubTodas as mudanças relevantes, versão por versão. O mesmo arquivo é publicado no repositório e traduzido nos 19 idiomas.
[1.235.0] — 2026-09-16
Paridade com o pai — career-ops main @ 68e6b94 (VERSION 1.33.0, 42 commits). Duas correções espelhadas; uma delas estava perdendo vagas do MokaHR aqui, silenciosamente.
Corrigido
Uma segunda vaga do mesmo board do MokaHR desaparecia em vez de ser adicionada. url-key.mjs reduz a URL de uma vaga a uma chave de comparação, e descartava o fragmento incondicionalmente — razoável, já que quase todo fragmento é só apresentação (#apply, #section-2). O MokaHR é a exceção em nosso próprio registro: suas vagas são …/social-recruitment/{tenant}/{id}#/job/{n}, onde toda vaga de um tenant compartilha o mesmo caminho e o id da vaga existe somente no fragmento. Então todas normalizavam para uma única chave. Medido antes da correção: colar duas vagas diferentes do MokaHR no pipeline resultava em uma única linha — sem erro, sem aviso, nada perceptível; o filtro de novidade do scanner (en-scanner.mjs) lê essa mesma chave, então, uma vez que qualquer vaga daquele tenant estivesse no histórico de scan, todas as demais eram descartadas como “já vista”. normalizeUrl agora promove uma rota reconhecida #/job/{id} ou #/jobs/{id} a uma chave de consulta só para comparação antes de o fragmento ser descartado — o MokaHR mantém seu nome já estabelecido mokahr_job_id, para que chaves gravadas anteriormente continuem comparáveis; qualquer outro host recebe um _career_ops_fragment_job_id interno. O id promovido é anexado, não definido: uma URL que já carregue esse parâmetro contém o id de uma vaga diferente, e sobrescrevê-lo fundiria exatamente as duas coisas que a chave existe para manter separadas. Todo fragmento não reconhecido ainda colapsa, que é a metade que já estava certa.
accept-encoding agora é fixado em toda requisição a uma fonte. Sem definição, o Node negocia o que sua build do undici oferecer; a partir do Node ≥ 23 isso inclui zstd, e a resposta zstd do amazon.jobs volta truncada em exatamente 1024 bytes com status 200 — de modo que a falha aparece como Unterminated string in JSON at position 1024, como se fosse uma API malformada, e não um bug de transporte. Toda requisição agora envia gzip, deflate, br. Em nossos runtimes isso é preventivo, não um reparo: Node 18/20/22 — a matriz de CI e o servidor (22.22.1) — enviam gzip, deflate, verificado diretamente, não presumido, então nada foi truncado aqui; o que muda hoje é que br passa a ser oferecido, o que essas versões não solicitam por padrão, mas decodificam corretamente. O padrão é aplicado sem diferenciar maiúsculas de minúsculas, que é a parte que vale a pena revisar: nomes de cabeçalho não diferenciam maiúsculas de minúsculas, e fetch une duas entradas de mesmo nome em um único valor separado por vírgula, de modo que um chamador passando Accept-Encoding: identity teria recebido, de outra forma, gzip, deflate, br, identity.
Notas
Os cabeçalhos permanecem um objeto simples na fronteira do fetchImpl, onde o pai entrega ao fetch uma instância de Headers. Essa é uma divergência deliberada, não um descuido: os helpers de fetch do web-ui recebem um fetchImpl injetável, e 45 suítes de fontes leem opts.headers['User-Agent'] a partir do que capturam — uma instância de Headers não tem acesso por colchetes, então a portagem fiel é a do comportamento (sobrescrita sem diferenciar maiúsculas de minúsculas, o chamador vence), e não da implementação. O objeto do chamador é copiado, nunca alterado, já que várias fontes compartilham uma mesma constante de cabeçalho. A correção complementar do pai — aplicar um user-agent padrão sem diferenciar maiúsculas de minúsculas — é um não portado: o web-ui não injeta nenhum UA padrão; cada fonte define o que seu board exige, então não há padrão algum para colidir com o User-Agent de um chamador.
Duas novas suítes e três novos casos fixam o par: tests/http-accept-encoding.test.mjs (o valor exato fixado, cada um dos quatro helpers de fetch, e uma sobrescrita do chamador em três variações de capitalização — verificando que sobrevive uma única chave accept-encoding, que é o que o bug de mesclagem produzia em dobro) e tests/parsers-pipeline-hash-route.test.mjs, que testa a costura visível ao usuário, e não a chave: duas vagas do MokaHR precisam chegar ao pipeline, colar novamente a mesma vaga sob quatro grafias ainda precisa colapsar em uma linha, e um fragmento cosmético não pode virar identidade. Todos foram confirmados falhando antes. A fixação vive no transporte, não apenas nesses helpers: 25 das 92 fontes chamam o fetchImpl injetado diretamente e nunca passam por eles, então makeTimeoutFetch — o wrapper que ambos os scanners de fato injetam — a aplica, e esse é o único ponto de passagem comum a todas as fontes. (O pai não precisa de equivalente: providers/_http.mjs é seu único transporte.) Uma guarda estrutural agora quebra o CI se uma fonte nova chamar fetch diretamente em vez de usar o transporte que recebeu, e um caso comportamental conduz o greenhouse, uma das 25, pelo wrapper para provar que o cabeçalho chega ao fio.
Não portado, com o motivo: liveness-browser.mjs (proteção de reuso do Playwright em modo com interface — o web-ui não conduz um navegador com interface gráfica), check-jd-archive.mjs (sem repasse no web-ui), test-all.mjs (harness do pai; a divergência vpFixtureEnv do fork permanece intacta), web/ (o próprio frontend do pai, não espelhado), manifestos de plugin e SIGNATURES.md (exclusivos do repositório). batch/batch-runner.sh (os & escapados antes da interpolação do sed) e jd-skill-gap.mjs (resolve cv.md a partir da raiz de dados) são repassados somente leitura — o web-ui aciona via shell as cópias do pai, então ambos chegam sem mudança de código aqui, e o de jd-skill-gap deixa o repasse de /api/jds/:id/skill-gap mais correto: cv.md agora é resolvido a partir da raiz de dados, e não do diretório em que o processo foi iniciado. 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. Contagem de fontes inalterada em 92 (87 EN + 5 RU), então a ajuda permanece intocada. 3042 → 3066 testes.
[1.234.0] — 2026-09-15
Paridade com o pai — career-ops main @ 56cce8f (VERSION ainda 1.32.0, 58 commits além da 1.32.0). Uma correção espelhada em 15 fontes, mais uma proteção para que não volte.
Corrigido
Um único id de vaga malformado não esvazia mais a página inteira. encodeURIComponent lança URIError diante de um substituto UTF-16 (surrogate) solitário, e um payload JSON pode carregar um — JSON.parse('"\\uD800"') o preserva. Quinze fontes construíam a URL de cada vaga a partir de um id ou slug controlado pelo host dentro do laço de parse, então uma vaga malformada abortava o laço e perdia todas as vagas daquela página; nada era registrado como falha da fonte, pois a exceção se propagava antes que o catch por empresa tivesse algo a relatar. O projeto pai corrigiu isso em providers/_safe-url.mjs (safeEncodeURIComponent → null em URIError, relança qualquer outro erro) e passou o id de cada laço por ele, descartando apenas a vaga problemática quando o retorno é null. Esse helper agora é server/lib/sources/_safe-url.mjs, e a mesma mudança é aplicada às treze fontes espelhadas — alibaba, arbeitsagentur, bamboohr, feishu-jobs, garena, jibeapply, manfred, meituan, mokahr, phenom, thehub, tkms, vdab — e a duas fontes exclusivas do web-ui que a nova proteção revelou logo na primeira execução, jobstreet e trudvsem, que tinham o mesmo formato de laço e o mesmo lançamento latente. O escopo é deliberado: só ids vindos de API passam pelo helper. Valores derivados de configuração mantêm seu encode estrito e falham ruidosamente — o office do garena (um segmento de portals.yml) permanece no urlSegment que lança exceção, enquanto seu id de API recebe um idUrlSegment que retorna null; o corpName do csod e o slug do 4dayweek validado por SLUG_RE são colocados em lista de permissão nominalmente, com o motivo registrado.
O fallback de título do rheinmetall podia lançar exceção diante de um href malformado. Quando um card não tem título, o título é reconstruído a partir do slug da URL via decodeURIComponent, que lança exceção diante de uma sequência percentual inválida no href obtido por scraping — o mesmo formato de «uma linha ruim mata a página inteira», agora do lado da decodificação. Agora ele decodifica de forma defensiva e recorre ao slug bruto como alternativa.
O registro de fontes agora ignora arquivos com prefixo _. A descoberta automática importava todo .mjs em server/lib/sources/, exceto a si mesma, e avisava sobre qualquer arquivo sem export const meta — então o helper gerava um aviso na inicialização. Foi adotada a convenção do pai: um underscore inicial significa não é uma fonte; esses arquivos não são importados nem geram aviso. Um arquivo não declarado e sem meta continua avisando, e o teste de descoberta agora verifica as duas metades (confirmado falhando com o filtro antigo). O script de build do site sync-assets.mjs conta os arquivos de adaptadores em disco pela mesma regra, então o helper não aciona mais sua guarda contra quedas silenciosas.
Notas
tests/sources-url-encoding-surrogate.test.mjs espelha a suíte do pai em três partes: o contrato do helper (um substituto pareado — um emoji comum — ainda é codificado; um lançamento vindo do próprio toString do valor é um bug do chamador e se propaga), o comportamento por fonte para as quinze (um id ruim + um id limpo → sem exceção, o limpo é mantido, o ruim é descartado, cada um exercitado através do parser puro exportado pela fonte ou, no caso do thehub, seu fetcher com um transporte falso), e uma proteção no nível da fonte: todo arquivo convertido precisa importar o helper e chamá-lo, nenhum arquivo pode referenciá-lo sem importá-lo, e nenhum arquivo não revisado pode montar uma URL de vaga com um encodeURIComponent isolado — uma fonte nova com o formato antigo falha no CI com o número da linha responsável. Os quinze casos por fonte foram confirmados falhando antes da portagem.
Tudo o mais no delta do pai não foi portado, com o motivo: batch/batch-runner.sh --cli é uma flag exclusiva de CLI (o web-ui não invoca o batch runner via shell); invite-match.mjs agora resolve set-status.mjs a partir da raiz do código — o web-ui não faz esse repasse; company-funded.mjs recebeu a mesma mudança de safe-url internamente, e o formato de saída do seu --json, que /api/company-funded interpreta, permanece inalterado; test-all.mjs só moveu um orçamento de timeout (a divergência vpFixtureEnv do fork permanece intacta); web/ é o frontend do próprio pai e não é espelhado; AGENTS.md/SIGNATURES.md são exclusivos do repositório. 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. Contagem de fontes inalterada em 92 (87 EN + 5 RU), então a ajuda permanece intocada. 3022 → 3042 testes.
[1.233.2] — 2026-09-10
Corrigido — dois alertas CodeQL js/remote-property-injection de alta severidade.
Corrigido
O laço que copia os valores salvos para process.env percorria Object.entries(safe) — um objeto construído a partir do corpo da requisição — com uma guarda explícita contra chaves de protótipo. O código era seguro, pois safe só é preenchido a partir de KNOWN_KEYS, mas o analisador não consegue seguir essa restrição entre dois laços. Agora percorre o array constante, então o nome da propriedade é comprovadamente um literal de módulo. O comportamento é idêntico.
Notas
O teste que fixava a guarda era um grep do texto-fonte. Foi trocado por um estrutural e um comportamental que envia __proto__ como JSON cru — um literal de objeto definiria o protótipo em vez de criar a propriedade. 3021 → 3022 testes.
[1.233.1] — 2026-09-10
Corrigido — o guia não descrevia o controle que a v1.233.0 acrescentou, e a mesma seção ainda trazia instruções que deixaram de valer há 200 versões.
Corrigido
DOC-1 — a entrada «Usar o padrão (…)» expressa o estado «a chave não está definida»; a diferença para escolher o mesmo valor na lista é invisível e relevante. Três itens no ### Behavior existente ×17.
DOC-2 — uma linha da tabela não nomeava chave nenhuma: a célula trazia (server uses default UA), descrevendo HH_USER_AGENT, removido na v1.19.0, e mandava registrar em dev.hh.ru — inútil desde a v1.65.0. As três menções obsoletas foram embora; a linha de solução de problemas foi reescrita, pois metade continua verdadeira: desde julho de 2026 o hh.ru devolve HTTP 451 a IPs não russos.
A tabela de chaves lista 6 de 45 enquanto o título promete um registro. Uma frase ×17 a declara uma seleção e aponta para #/config.
Notas
O novo help-recognized-keys.test.mjs confere a primeira coluna das tabelas de chaves contra o KNOWN_KEYS ao vivo. 3018 → 3021 testes; a ajuda continua em 32 H2 / 122 H3.
[1.233.0] — 2026-09-10
Corrigido — três achados no caminho de salvar do #/config; o CONFIG-3 foi causado pela própria correção da v1.232.1.
Corrigido
CONFIG-3 — uma escolha deliberada que coincidia com o valor semeado era descartada como «não tocado». initial confunde duas origens: semeado do .env e semeado do defaultValue, e no segundo caso a igualdade é ambígua. Corrigido registrando seededFromFile ao lado de initial; onde a semente veio do arquivo nada muda, e onde veio do padrão o dirty vira uma segunda pista válida.
Remover uma chave informava «· 0» porque o servidor só devolvia o que gravou. Agora responde também com removed, e o aviso conta os dois.
Um dropdown podia ser definido mas nunca limpo: nenhum dos 18 tinha opção vazia. Agora cada um começa com «Usar o padrão (…)». Uma nova chave i18n ×17.
Notas
Linha de base unitária inalterada (3018); navegador 111 → 116.
[1.232.1] — 2026-09-10
Corrigido — Salvar em #/config gravava campos que o usuário nunca tocou.
Corrigido
CONFIG-2. A v1.57.1 semeia um controle sem valor com seu defaultValue para que o campo mostre o que o servidor de fato usa, enquanto save() consultava dirty apenas para segredos e enviava todos os outros campos sempre. Assim o valor posto ali para EXIBIÇÃO viajava como escolha do usuário: mudar um dropdown enviava 28 chaves e fixava 18 que ninguém abriu. O que muda é o status de cada ajuste — de «não definido, siga o padrão do projeto» para «fixado no .env».
Notas
Comparar com cfg.values não basta: um select sem valor mostra seu defaultValue enquanto o armazenado é ''. Agora compara-se com aquilo que semeou cada controle. O número de chaves gravadas voltou ao aviso. 105 → 111 testes de navegador.
[1.232.0] — 2026-09-09
Corrigido — dois defeitos de uma passada de QA no navegador; nenhum veio da v1.231.5.
Corrigido
PROFILE-1 — #/profile rolava a página inteira para o lado. O valor era um <div> anônimo só com estilos de fonte inline, então seu overflow-wrap continuava normal. Uma URL do LinkedIn não oferece ao CSS nenhum ponto de quebra, logo o min-content é a string inteira, e min-width: auto nos itens de grade impede encolher — a carta estourou a trilha, a linha e o documento (23 px de rolagem real a 1280 px). Corrigido com a classe .card-value e overflow-wrap: anywhere, mais .card-row > * { min-width: 0 }. anywhere, não break-word: só anywhere conta no cálculo do min-content.
CONFIG-1 — POST /api/config aceitava qualquer valor num campo select. {"LLM_PROVIDER":"not-a-provider"} retornava 200 e era escrito no .env; depois nada falhava, pois o resolvedor caía silenciosamente no fallback. LLM_PROVIDERS já era exportado de env-config.mjs — o validador nunca o consultou.
Notas
Só LLM_PROVIDER é validado: o domínio real dos modelos é do fornecedor e muda entre nossas versões. Um valor já salvo é tolerado, pois o formulário reenvia todos os campos não secretos a cada gravação. Também foi adicionado o descritor de OLLAMA_API_KEY. 3013 → 3018 testes.
[1.231.5] — 2026-09-09
Segurança — sete avisos fechados, um deles uma execução remota de código crítica. Nenhum arquivo de código-fonte mudou.
Segurança
Mesclado do Dependabot: multer 2.2.0 → 2.3.0 (alta — negação de serviço por vazamento de descritores de arquivo ao abortar) e svgo 4.0.2 → 4.1.0 no site (alta e média — removeScripts deixava passar links executáveis).
Depois mais cinco, revelados pelo reescaneamento que essas mesclagens dispararam. Cada faixa afetada já era atendida pelo ^ existente, então bastava atualizar: astro 7.1.0 → 7.3.2 — crítica, execução remota de código pela otimização de imagens AVIF (corrigida em 7.2.8), e um bypass de autorização (7.2.4); sharp 0.35.3 → 0.35.4 — alta, vulnerabilidades do libheif; e js-yaml 4.3.1 → 4.3.2 em ambos os manifestos — alta.
Notas
“Só dependências” aqui não é o mesmo que “sem risco”. js-yaml analisa cada configuração que o app lê, multer aceita cada upload, e o Astro avançou uma versão menor. Verificado: test:ci 3013/3013, suíte de navegador 4/4, e uma reconstrução completa do site com as mesmas 86 páginas e todos os 17 espelhos. npm audit: 0 vulnerabilidades.
[1.231.4] — 2026-09-07
Corrigido — a v1.231.3 corrigiu o depois do clique em Doctor, não o momento. Relatado de novo, do celular e do desktop.
Corrigido
O withSpinner ainda reescrevia o botão enquanto a requisição estava em voo. A v1.231.3 fez com que ele capturasse os nós filhos e os restaurasse no finally, e eles voltavam — mas o sinal de ocupado continuava executando replaceChildren com o texto '⏳ ' + original. Assim, por toda a duração de doctor.mjs, o quadrado de 36 px continha ⏳ 🩺Doctor e virava uma pílula larga que quebrava a linha; no desktop aparecia como um ⏳🩺Doctor amontoado. Um botão com filhos elemento não é mais reescrito: seu sinal é a classe .is-loading, que agora troca o ícone por uma ampulheta em CSS, de modo que a geometria não pode mudar: 36 px parado, 36 px em voo, 36 px depois.
Notas
A lição é sobre a asserção, não sobre o código. O portão da v1.231.3 esperava aria-busy limpar e só então media — por construção via apenas o estado que o finally já havia consertado. A nova verificação segura a promessa aberta e amostra no meio do voo. 3012 → 3013 testes.
[1.231.3] — 2026-09-07
Corrigido — clicar em Doctor quebrava a barra superior no celular. Relatado de um telefone no mesmo dia em que v1.231.2 saiu.
Corrigido
UI.withSpinner achatava os filhos do botão que decorava. Seu sinal de ocupado passava por textContent: guardar button.textContent, escrever '⏳ ' + original, escrever de volta. Atribuir textContent substitui todos os filhos por um único nó de texto — e a v1.231.2 tinha acabado de dividir cada ação da barra superior em .btn-ico + .btn-label, ocultando o rótulo sob max-width: 900px. Então o primeiro toque em Doctor destruía os dois <span> de vez: as regras móveis ficavam sem nada para casar, o rótulo voltava como texto puro e o quadrado de 36 px crescia até virar uma pílula 🩺Doctor que montava sobre o alternador de tema. Não se curava — só recarregar a página restaurava a marcação. A correção captura os nós filhos e os restaura com replaceChildren; botões só de texto, que são a maioria dos 24 pontos de chamada, seguem idênticos.
- FIND-3 — o campo de busca encolhia até 8 px num telefone de 320 px. Ao pôr a barra em uma linha só, o searchbar ficou como o único item flexível, e o changelog anterior dizia isso: «pode encolher até nada». E encolheu: 8 px de campo a 320 px, 28 px a 340 px, um caractere de um placeholder de 21. Clicável e digitável, mas ilegível como campo. Nenhum
min-widthresolve: a 320 px a barra tem 32 px de padding, um menu de 40 px e 162 px de ações, sem 120 px sobrando. Então abaixo de 420 px — a largura em que o campo enfim passa de 100 px sozinho — ele se esconde atrás de uma lupa e se expande por toda a barra ao toque: 182 px a 320 px, 222 px a 360 px, 252 px a 390 px, sem overflow em largura alguma. O botão mantém um único nome acessível e informa o estado poraria-expanded, e a troca 🔍/✕ é CSS puro. Acima de 420 px nada muda. Encontrado por uma passada de regressão em navegador contra a build ao vivo.
Notas
É a mesma classe de defeito da armadilha do applyI18n() apontada na v1.231.2 — código atribuindo textContent a um elemento que agora tem filhos elemento — alcançada por outro chamador. A lição se generaliza: dividir o conteúdo de um elemento em <span> filhos transforma cada escritor de textContent já existente naquele elemento em um bug latente.
Travado duas vezes: node:vm sobre o withSpinner real e um clique em navegador real a 320 px. Ambos confirmadamente falhavam no código antigo. 3009 → 3012 testes.
[1.231.2] — 2026-09-06
Corrigido — a barra superior mobile quebrava em duas linhas, o servidor rodava um pai mais antigo e o redirect do cvstart.ru dava 404 em qualquer caminho que já nomeasse um idioma.
Corrigido
- A barra superior mobile voltou a uma única linha, com ações só de ícone. Os botões eram forçados para uma segunda linha de largura total, então o telefone mostrava
[☰ · busca]acima de[🔔 🌙 Doctor Abrir Scan]— duas pílulas largas em linha própria. Agora são quadrados de 36 px (🩺 / ⚡) ao lado do sino e do alternador de tema, e tudo cabe em uma linha a 320 px. Medido, não estimado: a largura do documento é igual à viewport a 320, 360, 390 e 430 px, e em toda a faixa 560–760 px, onde antes os botões se sobrepunham à barra de busca. O idioma não afeta mais o layout mobile, pois o rótulo está oculto e o botão é um quadrado fixo; acima de 900 px os rótulos voltam inalterados. - O servidor rodava o pai 1.31.0 embora a versão tenha sido compilada contra 1.32.0.
/api/healthno servidor de produção informavaparentVersion: 1.31.0. Foram sincronizados 95 arquivos de execução e dois removidos. Detectado por QA de navegador na superfície em produção. - O
cvstart.ruprefixava/ru/a qualquer caminho, sem olhar o que já havia./ru/helpvirava/ru/ru/helpe/en/helpvirava/ru/en/help; ambos 404. O primeiro é o que se obtém copiando uma URL do site principal; o segundo trancava o visitante de língua inglesa fora da versão em inglês. Agora um caminho que já nomeie um dos 16 prefixos passa como está,ené removido (o inglês fica na raiz) e o resto continua recebendo/ru/.
Alterado
- 98 prompts de QA superados movidos para
qa/archive/superseded-prompts/. Oqa/README.mddocumenta desde a v1.137.0 que só o prompt da versão atual fica no nível superior — a regra não foi aplicada por 95 versões. - Outros cinco prompts saíram do nível superior: diziam-se atuais sem o ser.
REGRESSION-FINAL.mdchamava a si mesmo de «o único prompt de regressão autoritativo e independente de versão», mas seus §§11–15 são cinco registros de ciclos encerrados (v1.55.x → v1.59.7) e sua linha de base de ajuda declarada era 28 H2 / 103 H3 em 16 idiomas contra 32 / 122 e 17 hoje: um agente de QA que o seguisse teria falhado no §9 logo no primeiro passo.QA-REGRESSION-PROMPT.mdtinha nome sem versão e conteúdo de v1.226.0.QA-REGRESSION-PROMPT-v1.76.0-FULL.mdestava fixado 155 versões atrás,REGRESSION-PROMPT-FINAL.mdencerra o ciclo v1.58.52 → v1.59.10 eQA-FULL-REGRESSION.mdduplicava todos eles com base v1.131.1. Junto foiPARENT-SYNC-WORKLIST-v1.26.0.md(o projeto pai já está em 1.32.0), e o datadoUX-AUDIT-2026-07-06.mdmudou parareports/. Restam quatro drivers, com as próprias linhas de base atualizadas aos valores reais de v1.231.2.qa/README.mdfoi reescrito para descrever a pasta que existe.
Notas
- Duas armadilhas de especificidade agora comentadas em
app.css: as regras de desktop são declaradas abaixo do blocomax-width: 900px, então com especificidade igual elas vencem. applyI18n()atribuiel.textContent, então a chave i18n foi para um<span>interno; no botão apagaria o ícone a cada troca de idioma.- O primeiro rsync do pai abortou no meio em um arquivo que não existe mais localmente e mesmo assim informou sucesso. Deploys do pai são verificados por checksum, nunca por código de saída.
[1.231.1] — 2026-09-06
Corrigido — a varredura de contagens da v1.231.0 reescreveu o nome da conta em todos os links.
Corrigido
- Cada link
Fighter90virouFighter92. A varredura da contagem de fontes da v1.231.0 trocava90por92com uma guarda de limite de dígito, que descarta corretamente290e901mas casa com o90dentro deFighter90. 562 links nos 17 READMEs, nos 17 pacotes de ajuda e nos espelhos de ajuda do site, mais 138 no wiki, apontavam para uma conta inexistente; o bannerproviders.pnge todos os links de editar no GitHub davam 404. Detectado por um usuário, pela imagem ausente. - Cinco referências históricas a versões se deslocaram. A varredura
85 → 87transformouv1.85.0emv1.87.0, inventando uma versão nunca lançada.
Notas
- Um limite de dígito não basta: protege um número dos dígitos vizinhos, não de fazer parte de um identificador ou de uma string de versão. As contagens estão corretas: 92 fontes, 87 adaptadores EN, 3009 testes.
- O repositório pai não foi afetado.
[1.231.0] — 2026-09-05
Adicionado — duas fontes de varredura do career-ops pai v1.32.0 (Collage, Telegram estrito), o modo REST do Gem e um defeito que o port revelou no código do próprio pai.
Adicionado
- Duas fontes novas, 90 → 92 (87 EN + 5 RU),
ALL_ADAPTERS85 → 87. Ambas sem token.- Collage (
provider: collage) — a API pública de sites de vagas do Collage HR. O segmento de caminho é o endereço do site que o inquilino escolheu, não um slug do nome da empresa, então é lido deapi:ou de uma URLsecure.collage.co/jobs/<endereço>e nunca é adivinhado: um endereço adivinhado varreria o quadro de outra pessoa. - Telegram (estrito) (
provider: telegram-channel) — lê as mesmas préviast.me/s/<canal>que a fontetelegramexistente, com a troca oposta: um post vira uma linha só se nomear um empregador E apontar para uma página de vaga. Digests, posts de empregador oculto e posts de só contato são descartados. Baixa cobertura por design — a montante mediu-se 32-77% dos posts passando — mas cada linha carrega um empregador real e um link real. Ambas são publicadas, como no pai.
- Collage (
- O Gem ganhou um modo REST opcional ao lado da rota GraphQL (
api: https://api.gem.com/job_board/v0/<board>/job_posts). Nunca é derivado de um id de quadro, e a rota GraphQL continua sendo o padrão. Portado do pai #3783.
Corrigido
- O filtro de localização do
telegram-channelnão enxergava cirílico e nomeava cidades como empregadores. OLOCATIONISH_REdo pai usava\b, um limite só ASCII que nunca dispara junto ao cirílico, então todas as alternativas russas nele eram inalcançáveis eSenior Engineer | Москваretornava “Москва” como empregador. É exatamente a substituição de campo errado que toda a política de atribuição existe para impedir, e atingia o público principal do provedor. O port do web-ui usa verificações Unicode, o mesmo que o pai já faz uma constante abaixo emROLE_WORD_RE. Fixado por um teste que falha na forma ASCII. Ainda não corrigido a montante.
Notas
- O trabalho de recuperação por facetas do Workday no pai (#3851, #3874) não precisou de port — o web-ui não pagina o Workday de forma alguma, um único POST com
offset: 0. providers/_types.jsmudou apenas na documentação.- Testes: 2962 → 3009 (+47).
[1.230.0] — 2026-09-05
Alterado — três correções de provedores vindas do career-ops pai v1.32.0, e um defeito que o próprio port revelou.
Adicionado
- O Welcome to the Jungle aceita uma expressão
filtersdo Algolia aplicada no servidor (wttj: { filters: "offices.country_code:FR AND contract_type:full_time" }). Uma palavra-chave sozinha não estreita um quadro global: quem decide quaismax_hitsresultados chegam é o ranking de relevância do Algolia, não os filtros do scanner — os nossos rodam depois, sobre o que já chegou, então tudo além do teto é invisível por melhor que combine. Um filtro reduz o conjunto em vez de reordená-lo, e por isso o teto por consulta sobe de 200 para 1000. Com um filtro,queriesvira opcional: a consulta vazia significa “tudo o que passar pelo filtro”. Portado do pai #3757.
Corrigido
- O Radancy não avisa mais “truncated” quando um inquilino simplesmente serve menos vagas do que o próprio banner anuncia. O projeto pai conferiu
data-total-resultscontra nove inquilinos reais: cinco bateram, mas quatro — um pequeno e um enorme — entregaram de 10% a 56% menos vagas únicas do que o anunciado, sem nenhuma linha duplicada. O banner exagera o que o índice de busca do inquilino realmente serve, então comparar a contagem acumulada com ele dava falso positivo na maioria dos quadros. O aviso agora só aparece quando a caminhada foi interrompida pelo nosso própriomax_jobs/max_pages, o único caso sobre o qual quem chama pode agir;totalResultsfica na mensagem como contexto e nunca decide. Portado do pai #3839. - O Radancy contorna o cache em toda requisição de fragmento JSON. Uma camada de cache diante dessa rota repete respostas obsoletas em alguns inquilinos: a mesma vaga reaparece páginas depois enquanto outra nunca é servida — reproduzido nove vezes em nove rio acima. Os cabeçalhos
Cache-Control/Pragmanão mudaram nada; um parâmetro aleatório por requisição resolveu. Ele precisa ser único por chamada, nunca derivado do número da página: um parâmetro determinístico ainda dá ao cache uma chave estável e, com ela, o mesmo mapeamento estável e errado. Portado do pai #3839. - As listas de escritórios do Greenhouse têm ordem determinística. O Greenhouse não promete a ordem de
offices[]entre respostas, e o conjunto recuperado da árvore/officesvem na ordem do percurso. Ambos chegam ao campolocationda vaga, então um quadro que reordenasse seus escritórios reescrevia essa string — e uma vaga inalterada era lida como nova. O pai corrigiu o conjunto (#3839); o web-ui tinha uma segunda instância do mesmo defeito que o pai não tem — o fallbackoffices[0], usado quando a vaga não traz localização própria — e ambos agora são ordenados.
Notas
- A correção do iCIMS no pai (#3728) não precisou de port. Ela lê todas as entradas
jobLocationdo JSON-LD da página de detalhe, porque alguns inquilinos emitem uma entrada inteiramenteUNAVAILABLEantes do endereço real. O iCIMS do web-ui pega a localização do HTML da página de listagem e nunca abre uma página de detalhe. - A mudança em
_http.mjsnão precisou de port. Ela extraiisRefusedRedirectErrorpara um segundo consumidor que o web-ui não tem; o comportamento deisRetryableErrornão muda. - A deduplicação por localização (#3751) não é espelhada — vive no pipeline
scan.mjsda CLI do pai. - Registro sem alterações: 90 fontes (85 EN + 5 RU). Testes: 2956 → 2962 (+6).
[1.229.0] — 2026-09-03
Adicionado — quatro fontes de varredura do career-ops pai v1.31.0: Built In, Feishu Jobs, Garena e MokaHR.
Adicionado
- Quatro fontes novas, 86 → 90 (85 EN + 5 RU),
ALL_ADAPTERS81 → 85, todas sem token. Built In é um agregador dos nove mercados do quadro de tecnologia dos EUA e não traz consulta padrão — uma entrada semqueries:nemcategories:não varre nada e avisa, porque uma fonte compartilhada não deve carregar os termos de busca de uma pessoa; um mercado fora da lista permitida é recusado, nunca trocado em silêncio pelo quadro nacional. Feishu Jobs aceita duas formas de host e mais nenhuma, e elas usam caminhos diferentes para a página da vaga. Garena é de empresa única:officemolda o link, não a listagem. MokaHR devolve a resposta cifrada (envelope AES-128-CBC com a chave ao lado) — ofuscação, não segurança, mas sem decifrar não se lê nada.
Corrigido
htmlToTextpodia deixar um abridor de tag incompleto vazar para o texto.safe <img src=x onerror=1saía comosafe <img src=x onerror=1. Agora cada decodificação é seguida de uma limpeza e o abridor final perde o sinal de menor. Portado do pai (#3491).
Notas
- Nada foi portado do conserto de Workday do pai. Ele recupera inquilinos cujo backend limita a paginação no offset 2000 — o web-ui não pagina Workday (um POST em
offset: 0, 100 linhas), então não há limite a contornar. - Os consertos de
verify-cv-factsemerge-trackernão exigiram port: o web-ui executa esses scripts do checkout do pai. - Testes: 2909 → 2956 (+47).
[1.228.5] — 2026-08-30
Corrigido — a versão informada descrevia o arquivo em disco, não o código em execução.
Corrigido
-
/api/healthreliapackage.jsona cada requisição, então um processo desatualizado informava uma versão que não estava executando. O QA encontrou uma instância local respondendoversion: 1.228.4enquanto devolvia 404 para/api/ping, uma rota adicionada em 1.228.3: iniciada antes do deploy, servindo o código antigo e informando a versão do arquivo novo. É exatamente assim que um deploy que copia arquivos mas nunca reinicia parece um sucesso: a única string que um operador confere é a que não consegue ver o problema. Mesma classe do link simbólico da v1.228.1, em que o rsync abortava no meio da transferência e ainda assim relatava sucesso.A versão agora é capturada no carregamento do módulo, descrevendo o código em execução. Um processo desatualizado informa a versão antiga — a verdade, visível de imediato.
parentVersioncontinua por requisição: descreve o checkout do projeto pai, que este processo não carrega e que pode legitimamente mudar sob um servidor em execução.
[1.228.4] — 2026-08-30
Corrigido — a sonda de vida sem autenticação fazia trabalho de disco a cada requisição.
Corrigido
GET /api/pinglia e analisavapackage.jsona cada requisição. É o único endpoint alcançável sem credenciais, então uma leitura de disco mais uma análise de JSON por requisição é uma alavanca de negação de serviço entregue a qualquer um — o CodeQL sinalizou como falta de limitação de taxa. A versão agora é lida uma única vez no registro da rota; não pode mudar sem reiniciar, então o manipulador não faz E/S nenhuma. Limitar a taxa teria contido o dano; remover o trabalho remove a alavanca.- O mascaramento do nome real do dono do perfil fora do loopback agora está fixado por um teste.
/api/healthjá o substituía porhidden, mas apenas porque essa linha divide o guardahidden ?? valuecom a raiz do projeto. Nada testava isso, então uma edição que desse valor próprio à linha teria vazado o nome de uma pessoa real para a rede local sem nada perceber.
[1.228.3] — 2026-08-30
Fixed — the docs assistant could not answer in English, Telegram company names arrived truncated, and a monitor had no way to see the service was up.
Fixed
-
The docs assistant answered “no matching help sections were found” in English while the same question worked in Russian. Retrieval was fine; the context builder was not.
buildAskPromptdidif (ctx.length + sec.body.length > MAX_CONTEXT) break— abreak, so one oversized section ended the loop and left the context empty, and the assistant truthfully reported it had nothing. The English §5 had grown to 16 081 bytes against a 14 336-byte budget (partly because v1.228.0 addedtelegram_channelsto it); the Russian §5 was 13 009 and fit. Same ranking in both languages — §5 was first — only the size differed.Two changes: a section past 6 KB is now split at its
###boundaries, sotelegram_channelsis its own retrievable chunk (1 340 bytes) that ranks first in both languages; and an oversized section is skipped or truncated rather than ending the loop. The guide went from 32 chunks to 75. -
A Telegram company label yielded the sentence after it, not the name.
Компания: FinCore Technology (продуктовая разработка, высоконагруженные системы)becameFinCore Technology (продуктовая разработка, высоконагруженны— capped at 60 characters, cut mid-word. Three of 141 rows. The name now ends at the first separator that introduces a description — a spaced dash (soCoca-Colasurvives), a comma, semicolon, pipe or opening parenthesis — and any remaining cap falls on a word boundary. This source attributes a company as fact; a truncated sentence is as wrong as a guessed name, only less obviously so.
Added
GET /api/ping— a liveness probe that leaks nothing.{ ok, version }and no more./api/healthreports absolute paths, the profile owner’s real name and which API keys are set: legitimate for an authenticated operator, never for the public internet. A monitor behind a Basic-auth gate previously had no way to see the service was up at all.
[1.228.2] — 2026-08-30
Fixed — a two-word search in /api/scan-results matched the phrase, not the words, and under-reported badly.
Fixed
-
?q=now matches every term independently instead of the literal phrase. The query was a singleincludes(q)over title + company + location, soпродакт менеджерfound only rows carrying those two words in that exact adjacency — 32 of the 162 that carry both.Менеджер продуктаandМенеджер по развитию продуктаwere invisible to it: the same words, reordered or with filler between them.This is how a narrow match becomes a wrong answer. The Telegram assistant asked for all Продуктовый менеджер reported 9 — faithfully, because 9 was what the endpoint said. The count it quoted was honest; the endpoint’s was not. Word order and the words between are not something a person typing two words intended to pin.
Matching is AND across terms, so widening precision costs nothing: a row must still carry every term.
менеджер разработчикreturns zero rather than the union.
[1.228.1] — 2026-08-29
Fixed — a dangling symlink in the repo that silently truncated deploys.
Fixed
-
.claude/skills/refero-designwas a tracked symlink to a path that has never existed here. It pointed at../../.agents/skills/refero-design, which resolves to.agents/at the repo root; there is no such directory, and the link arrived in the v1.118.0 parity pack from a machine where the surrounding folders nested differently. Anyone cloning the repo has been getting a broken link ever since.It was not inert. rsync aborts on it mid-transfer — a deploy today reported success while the server kept running the previous version, and the mismatch only surfaced because the version was checked afterwards. The parent project’s
node test-all.mjsalso crashes outright when a web-ui checkout sits inside its tree, because its fixture copy walks the working directory and cannot stat the link. Nothing referenced it: the onlyreferomatches in the repo are the Polish word preferowany.
[1.228.0] — 2026-08-29
Adicionado — canais do Telegram como fonte de varredura e um modo de consulta paginado em /api/scan-results.
Adicionado
-
Canais do Telegram agora são uma fonte varrível. Um bloco
telegram_channels:noportals.ymlos lista; cada um é lido pela prévia públicahttps://t.me/s/<canal>. Registro: 85 → 86 fontes (81 EN + 5 RU),ALL_ADAPTERS80 → 81.O Telegram não publica RSS e a Bot API não lê um canal que o bot não administra — nenhuma das rotas óbvias serve. A prévia
/s/é HTML puro renderizado no servidor: sem autenticação, sem JS, sem cookie. Verificado em 16 canais: 15 devolveram 20 posts cada num GET simples.channel:aceita qualquer forma de escrever. O parser se ancora nas três coisas que um redesign do Telegram moveria por último:data-post, o contêiner do texto e a data ISO<time datetime>. -
GET /api/scan-resultsganhou um modo de consulta paginado.?q=<texto>&limit=50[®ion=ru|en][&set=filtered|fresh][&offset=N]devolve{ total, returned, offset, limit, set, rows }, ondetotalé a contagem ANTES da paginação. Sem parâmetros a rota continua devolvendo o snapshot inteiro: a tabela#/scandepende dessa forma.
Corrigido
- O assistente do Telegram respondia com uma fração dos dados. Seu skill já apontava para
/api/scan-results, mas a rota só sabia devolver o snapshot inteiro — cerca de 2 MB — que não cabe no contexto de um agente. Diante de «encontre todos os Product Manager» relatou 9 correspondências onde o snapshot tinha centenas, e disse que «trabalhava com um índice local limitado». Era verdade: via o endpoint mas não conseguia consumi-lo. Com o modo paginado a mesma pergunta custa 2 KB e devolve umtotalhonesto. O skill também foi reescrito: pedia resumir os posts novos, ou seja apontava parafreshem vez defiltered.
Notas
- Um post de canal é prosa, não um registro de vaga. Não há campos estruturados: o título vem da primeira linha substantiva, e empresa/local/faixa de rótulos explícitos. Separar vagas reais de anúncios e digests fica com o
title_filterexistente, o mesmo já ajustado para outras fontes. São pistas, não uma listagem. - A empresa nunca é adivinhada. Sem rótulo, a linha é atribuída ao canal (
@rabotaphp): um empregador errado entra no tracker como fato. - Uma análise vazia é um erro, não «sem vagas». O t.me responde com redirecionamento a um canal privado ou inexistente; devolver zero linhas leria como um dia calmo e esconderia um erro de digitação na configuração para sempre.
jobGeeksfaz exatamente isso (HTTP 302) e deliberadamente não está configurado. - A detecção de remoto usa uma fronteira de palavra Unicode em vez de
\b, que é só ASCII — a mesma armadilha que o filtro de títulos sofreu na v1.227.3, aqui pega por um teste antes de publicar. - 15 canais configurados e verificados ao vivo: 299 posts, 15/15 canais. 30 testes novos. Testes: 2865 → 2895.
- The README test counts jumped by hundreds, and no, this release did not add them. The localized READMEs carried unit-test totals frozen at 1856 / 1945 / 1955 / 549 / 419 / 284 / 2527 depending on the file — each stale since a different release, and several contradicting the badge at the top of the same page. They now all read 2895, verified by running the suite rather than by copying a badge. Playwright (70 / 90 / 12 → 101), e2e smoke (20 → 21), route modules (12 / 32 → 37), smoke flows (12 → 22) and the test-file count (218 → 329) were stale in the same way. The
Totalrow also repeated the unit count, so it under-summed the three suites below it; it now reads the real sum (3040).
[1.227.5] — 2026-08-29
Corrigido — a varredura regional esgotava a memória do servidor, e uma suíte do Playwright falhava por seu próprio desmonte.
Corrigido
- A varredura regional derrubava o servidor por OOM. O
ru-scanneracumulava todos os resultados brutos de todas as consultas e só deduplicava e filtrava no fim. A lista de consultas é deliberadamente cheia de quase-sinônimos (Golang,Go разработчик,Golang разработчик,Senior Go…), então a mesma vaga volta uma vez por consulta e o array retinha várias vezes a contagem única — com as descrições, que são o grosso do objeto e que os filtros descartam quase inteiramente. Medido: 742 MB de heap numa execução real de 21 consultas. O servidor limita o heap do Node a 490 MB (o V8 dimensiona a partir dos 956 MB de RAM da máquina), então a varredura derrubava o serviço comFATAL ERROR: Reached heap limit— quatro vezes num dia. Agora deduplicação e filtragem acontecem por consulta, e a mesma execução caiu para 177 MB, menos 76%. As contagens relatadas não mudam: o total único é carregado por umSetde strings de URL. net::ERR_SOCKET_NOT_CONNECTEDderrubava o CI aleatoriamente. Três suítes do Playwright chamamwindow.stop()para interromper requisições em voo antes de afirmar que não houve erros de console. O Chromium relata esse único ato com três nomes conforme o estado do socket:ERR_ABORTEDao cancelar,ERR_EMPTY_RESPONSEem voo eERR_SOCKET_NOT_CONNECTEDquando o socket já sumira. O filtro compartilhado listava só os dois primeiros. Apareceu na varredura de locales emtr, enquantoplaywright-formseplaywright-smokeestavam a uma execução azarada do mesmo.
Notas
- Por que só começou a falhar agora. A lista de consultas foi de 14 para 21 ao incluir buscas de product manager. A memória da implementação antiga escalava com o total de resultados, então +50% de consultas passou do teto da máquina.
- A mudança de deduplicação preserva o comportamento, e isso é testado.
tests/ru-scanner-dedup-order.test.mjsfixa a direção difícil — um duplicado posterior que FALHA nos filtros precisa expulsar um anterior que passou — e roda 200 verificações aleatórias de equivalência contra a implementação antiga. - O filtro de console segue estreito. Só a família de requisições canceladas é benigna; conexão recusada, falha de DNS, reset, 5xx e exceções JS ainda derrubam a asserção.
- Duas hipóteses foram testadas e descartadas em vez de enviadas: timeout do Caddy (config limpa,
flush_interval -1correto para SSE) e orequestTimeoutpadrão de 5 minutos do Node (uma reprodução em escala mostrou que ele não corta uma resposta longa em streaming). A evidência real foiReached heap limitno journal. Testes: 2860 → 2865.
[1.227.4] — 2026-08-29
Corrigido — word: / stem: ainda eram ignorados pelo content_filter; a v1.227.3 corrigiu apenas o filtro de títulos.
Corrigido
- O
content_filternão respeitava os prefixos. A v1.227.3 ensinou os prefixosword:/stem:aotitle_filter, mas deixou ocontent_filtercom seu própriolower.includes(k)cru, então ali uma entrada com prefixo continuava sendo comparada como o texto literal"word:java"— o mesmo no-op silencioso, um filtro adiante. Ocontent_filterlê a DESCRIÇÃO da vaga e seu padrão sempre foi substring simples: por isso um negativo simplesjavarejeita tudo que apenas mencione «JavaScript»; o prefixo é como se opta por sair disso em uma entrada. Portado ocompileContentKeyworddo pai, que compartilha a maquinaria de prefixos mas deliberadamente não ancora palavras curtas: o filtro de títulos ancora siglas de 2–3 letras porque «COO» dentro de «Coordinator» é sempre errado, ao passo que uma sequência curta dentro de um parágrafo em prosa costuma ser intencional (aws,gcp,sql,go).
Notas
- Encontrado na revisão por IA do PR da v1.227.3, que notou que o código não cobria o que o próprio comentário afirmava — dizia «
title_filter/content_filter» com apenas o caminho de títulos ligado. O comentário agora separa explicitamente: ambos honram os prefixos, só o de títulos ancora siglas. - Também verificado a partir dessa revisão, e inalterado: trocar a regra de siglas de 2–3 letras de
\bpela fronteira Unicode é idêntico em comportamento para ASCII e mais estrito só onde deve ser —COO_leadecoo1continuam mantidos (sublinhado e dígito são caracteres de palavra em ambos),Coordinatormantido, «Директор COO» e «VP «Продукт»» corretamente descartados. 14 casos-limite conferidos. - Entradas sem prefixo do
content_filtermantêm sua correspondência anterior byte a byte, uma descrição vazia ou ausente ainda passa, e umword:/stem:vazio não casa com nada em vez de tudo. 6 casos novos emtests/content-filter.test.mjs. Testes: 2852 → 2858.
[1.227.3] — 2026-08-29
Corrigido — os prefixos word: / stem: do filtro de títulos eram ignorados em silêncio, deixando essas linhas de filtro sem efeito.
Corrigido
- O web-ui não implementava os prefixos
word:/stem:do projeto pai.title_filter(econtent_filter) usam por padrão correspondência de substring sem diferenciar maiúsculas, e é por isso que um negativo simplesinterntambém rejeita «International Product Manager» e «Internal Tools Engineer». Em vez de mudar esse padrão — uma quebra para toda instalação já configurada — o pai tornou a precisão opcional por entrada:word:interncasa apenas a palavra inteira estem:agentprecisa iniciar uma palavra, podendo continuar (separando «Agentforce» de «Reagents»). O web-ui não tinha nenhum dos dois, então casava a entrada com o texto literal"word:intern", que não aparece em nenhum título de vaga. A linha virava um no-op silencioso: o mesmoportals.ymlfiltrava corretamente pela CLI e não filtrava nada aqui, de modo que toda vaga de estágio excluída ainda chegava à tabela#/scan. Portado dotitle-keywords.mjsdo pai, incluindo a fronteira de palavra ciente de Unicode (\p{L}\p{M}\p{N}_) em vez de\b, que é só ASCII e casava no meio de palavras em títulos acentuados ou cirílicos.
Notas
- Encontrado ao revisar um
portals.ymlreal que acabara de adotarword:internjustamente para que uminternsimples parasse de descartar «International Product Manager». O prefixo funcionava na CLI; o web-ui silenciosamente mantinha todos os estágios. Agora o comportamento é idêntico. - Os padrões não mudaram. Uma palavra-chave sem prefixo mantém a correspondência anterior: substring para frases e para tudo que contenha caracteres não alfabéticos (
.NET,SAP,L&D), e ancoragem em limites de palavra para siglas de 2–3 letras. Umword:/stem:vazio é tratado como erro de digitação e não casa com nada — como negativo, um padrão vazio casaria com todos os títulos e vetaria uma varredura inteira por causa de um dois-pontos perdido. - A regra de siglas de 2–3 letras passa a usar essa mesma fronteira Unicode: antes usava
\b, entãocooeword:coodivergiam em títulos não ASCII — duas grafias de uma mesma regra. - 6 casos novos em
tests/title-filter.test.mjs. Testes: 2846 → 2852.
[1.227.2] — 2026-08-29
Corrigido — #/config rolava lateralmente em celulares estreitos.
Corrigido
- MOBILE-1:
#/configtransbordava horizontalmente em viewports ≤ 352 px. Cada campo do tipo select carregava ummin-width: 300pxinline. Um estilo inline vence a folha de estilos, então o controle não conseguia encolher junto com seu contêiner: num viewport de 320 px a borda direita do select caía em 353 px, a página ganhava rolagem horizontal e os controles saíam pela direita. Afetava os 18 selects de provedor/modelo (cfg-llm-providere todos oscfg-*-model). Agoramin-width: min(300px, 100%)+max-width: 100%, então os confortáveis 300 px valem só quando há espaço. Aparelhos no limiar ou abaixo: iPhone SE (1ª ger., 320 px), Androids antigos, tela externa do Galaxy Fold.
Notas
- Por que as passagens móveis anteriores não pegaram. 360 px e 375 px têm folga suficiente para esconder o transbordo, e as suítes Playwright rodam numa janela de 1280 px. Nada no CI jamais mediu um viewport mais estreito que 375 px. O novo
tests/playwright-narrow-viewport.mjspercorre as rotas com formulários a 320 px e falha diante de qualquer transbordo horizontal, nomeando na mensagem o elemento mais largo; um segundo caso fixa que nenhum select do#/configultrapassa o viewport. Verificou-se primeiro que ele reproduzia o bug (scrollWidth 353 > clientWidth 320, 18 selects com borda direita 353). Playwright: 99 → 101. - Conhecido e propositalmente ainda sem gate: 280 px (tela externa do Galaxy Fold) ainda transborda ~5 px, por causa diferente — conteúdo longo dentro dos blocos
<details>do cartão de configuração e a barra lateral de 256 px contra um viewport de 280 px, não omin-widthinline que esta versão corrige. Refluir numa largura que nenhum celular de massa declara é trabalho à parte; a listaWIDTHSda suíte já está pronta. - Também foi corrigido o prompt de QA da v1.227.1, que chamava a rota de follow-up de
#/modes/followup. As mode-pages registram o slug puro (Router.register(cfg.slug)), então a rota é#/followupe#/modes/followupdá 404 corretamente. Apenas documentação. - Suíte unitária inalterada em 2846 (a cobertura nova é de navegador).
[1.227.1] — 2026-08-29
Corrigido — a divisão de fontes na ajuda não fechava com ela mesma, e o banner do GitHub era impresso como marcação bruta no app.
Corrigido
- §17 dava um total e uma divisão que não somavam. Todos os pacotes de ajuda diziam «o registry
server/lib/sources/traz 85 adapters — 78 em inglês + 5 russos». 78 + 5 = 83, não 85; em ja/ko a deriva chegava a 77 (= 82). Cada fonte nova subia o total e deixava a parcela intacta, e os dois números vinham divergindo há várias versões. Agora a divisão é 80 EN + 5 RU nas 17 locales. Isso importa além da estética:docs/help/<lang>.mdé o ÚNICO corpus que embasa o «Ask the docs», então quem perguntasse quantas fontes existem recebia dois números contraditórios na mesma frase. - A mesma frase fixava uma âncora de versão obsoleta. «A partir da v1.213.0 o registry …» sobreviveu em 16 locales enquanto o contador mudou duas vezes (83 na v1.219.0, 85 na v1.226.0). A âncora já fora corrigida à mão na v1.210.1 e simplesmente derivou de novo. Foi removida em favor de texto sem versão: o contador é verificado contra o registro vivo, que não envelhece.
- O banner de provedores era impresso como marcação escapada no topo de
#/help. v1.225.0/v1.225.1 registraram que ele era «removido no app peloUI.md()(sem suporte a imagens)». Isso confunde duas coisas:UI.md()escapa primeiro — escapa cada byte da origem antes de qualquer transformação markdown, o que é a fronteira XSS correta, mas o oposto de remover. Ninguém removia o banner porque ninguém tentava, e as 17 locales abriam#/helpcom 268 caracteres do literal<p align="center"><img src="https://…. Uma nova entrada compartilhada (server/lib/help-markdown.mjs::stripGithubOnlyBlocks) descarta blocos HTML de imagem isolados na leitura, tanto paraGET /api/help/:langquanto para o corpus do docs-assistant. Os arquivos mantêm o banner, então o GitHub continua exibindo a vitrine. - A coluna «#» do painel de cadência estava vazia desde a v1.117.0. Ela lia
entry.appNum, mas ofollowup-cadence.mjsemitenum—appNumnunca existiu no payload. Agora ambos são lidos. A linha «Next up» da v1.227.0 herdou o mesmo erro e é corrigida junto.
Notas
- Os quatro passaram despercebidos porque nada os vigiava. Os gates de ajuda (
canonical-docs-coverage,help-ui,help-ru-config-section) contam CABEÇALHOS — 32 H2 / 121 H3 — e nenhum teste jamais leu um número dentro de um parágrafo nem olhou o que a entrada da ajuda realmente serve. Duas suítes novas fecham isso:tests/help-source-counts.test.mjs(o total declarado é igual aSOURCES.length; as parcelas são a divisão EN/RU viva e somam o total; o parágrafo não fixa versão) etests/help-banner-strip.test.mjs(o banner fica no disco para o GitHub, nunca sobrevive à filtragem, a primeira linha renderizada de cada pacote é seu cabeçalho, a filtragem permanece estreita e ambos os consumidores passam por ela). Verificou-se que ambas falham contra exatamente esses bugs antes do commit. Testes: 2837 → 2846. - Sem mudanças em registro, scanner ou contratos de rotas; as fontes seguem em 85 (80 EN + 5 RU) e
ALL_ADAPTERS80.
[1.227.0] — 2026-08-28
Adicionado — SEEK Hong Kong (JobsDB), um filtro de somente pendentes nos follow-ups e empregadores confidenciais mostram quem os intermediou.
Adicionado
- JobsDB Hong Kong pela fonte Jobstreet/SEEK existente. Hong Kong opera sob a marca JobsDB, mas roda na mesma plataforma SEEK atrás do mesmo endpoint v5, então não precisa de fonte separada: basta
hk.jobsdb.comna lista de hosts permitidos esiteKey: HK-Main. O registro não muda: 85 fontes (80 EN + 5 RU) eALL_ADAPTERS80 — é um MERCADO novo sobre um adaptador existente, não um adaptador novo. - Filtro de somente pendentes no painel de cadência (
#/modes/followup). Uma nova caixa de seleção reduz o painel ao que é acionável agora. A condição vem daurgencyque ofollowup-cadence.mjsdo projeto pai já calcula (urgent/overdue), nunca dostatusdo tracker, que éapplied/responded/interviewe não casaria com nada. Quando nada está pendente, o painel nomeia o follow-up agendado mais próximo em vez de exibir um vazio. A lógica fica no módulo puropublic/js/lib/followup-view.js, espelho dofollowup-view.mjsdo pai. - Empregadores confidenciais são atribuídos à agência que os intermediou. Uma vaga com o empregador omitido é registrada como
?no tracker, com o recrutador na colunaVia. O#/trackermostrava um?cru, então várias linhas confidenciais ficavam indistinguíveis e o resolvedor de logos não tinha o que buscar. Agora a linha lê “Confidencial · via <agência>”, resolve o logo pelo nome da agência e é encontrável por esse nome na busca. Apenas apresentação: o campo canônicocompanynunca é sobrescrito com o recrutador. Novo módulo puropublic/js/lib/company-presentation.js, espelho docompany-presentation.mjsdo pai.
Corrigido
- Jobstreet/SEEK chamava um endpoint obsoleto. A fonte ainda apontava para chalice-search v4 (
/api/chalice-search/v4/search), que a SEEK aposentou; o pai migrou para/api/jobsearch/v5/searche o web-ui nunca acompanhou. Agora toda entrada Jobstreet/SEEK consulta v5. O formato do item v5 é lido nativamente (URL construída a partir deid,locations[0].label,advertiser.description,salaryLabel), o parâmetro de projeçãosolrFields(só v4) some, e uma entrada cujoportals.ymlainda fixe o caminho v4 morto mantém o hostname do mercado enquanto o caminho é reconstruído em v5 — sem edição do usuário.
Notas
- Paridade com career-ops 1.30.0 (HEAD do pai
8d64f65→6cc46a4; oVERSIONdele ainda marca 1.30.0 — o release-please fica atrás damain). Não portado, com motivos: preenchimento de localização JSON-LD do iCIMS — a fonte iCIMS do web-ui não tem ganchoenrichDate()(só páginas de listagem), então não há caminho a corrigir; stderr não fatal do CLI (#1974) — o runner do web-ui decide falha só pelo código de saída e nunca teve essa heurística; rastreio de caminhos do Turbopack — o web-ui não usa bundler; correções docv-sync-check— repassadas somente-leitura e fail-soft; sequenciamento do Playwright emscan.mjs, respostas do Block H, guardas de updater/doctor/update-system, o verificador de sintaxe recursivo, o dashboard em Go e as autotestagens dejd-skill-gap/verify-cv-facts— superfícies só-CLI que o web-ui não invoca, ou autotestes sem mudança de comportamento. Testes: 2818 → 2837.
[1.226.0] — 2026-08-27
Adicionado — duas fontes de vagas vietnamitas: ITviec e CareerViet.
Adicionado
- ITviec + CareerViet (Vietnã). Dois portais vietnamitas com host fixado entram no scanner via entradas
provider: itviec/provider: careerviet: ITviec (itviec.com, tecnologia) e CareerViet (careerviet.vn, ampla cobertura). Ambos são scrapers HTML sem tokens, com host-pin anti-SSRF, só-HTTPS, fail-soft por empresa e limites de página. O registro agora traz 85 fontes (80 EN + 5 RU),ALL_ADAPTERS80 — ambos aparecem no filtro Fonte do#/scan.
Notas
- Paridade com career-ops 1.30.0 (parentVersion 1.29.0 → 1.30.0). Não portado (só CLI/comunidade ou já coberto): o Hired Wall,
/calibrate, clean-markers, packs de templates, doctor, pdf/LaTeX e as CLIs de eval/tracker/scan que o web-ui não invoca; o ajuste de_html-to-texteroleFuzzyMatchjá existiam; o prefixotitle-filter stem:fica adiado. Conjunto de testes: 2818.
[1.225.1] — 2026-08-27
Corrigido — o banner de logotipos de provedores agora aparece no README e na ajuda.
Corrigido
- O banner da v1.225.0 não aparecia no GitHub: o
images/providers.svgtinha umfont-familyinválido (aspas duplas aninhadas) rejeitado pelo GitHub, o<img>usava umsrcrelativo (o HTML<img>não resolve caminhos relativos no GitHub) e o bloco<p>não tinha uma linha em branco final. Agora aponta para umimages/providers.pngrenderizado via URL absoluta, com separação por linhas em branco; o SVG também foi corrigido para XML válido.
Notas
- Somente docs; sem mudança de código ou testes. A vitrine do cvstart.org e os blocos da app já estavam corretos (SVG inline, não este arquivo). 2784
[1.225.0] — 2026-08-27
Adicionado — um banner de logotipos de provedores no README e no guia de ajuda.
Adicionado
- Banner de logotipos de provedores. Um único
images/providers.svgautônomo — os 18 provedores de LLM com o logotipo real da marca (simple-icons, CC0) para os 11 que publicam um e um monograma com a cor da marca para os 7 que não — agora encabeça o README (×17) e o guia de ajuda integrado. (O app renderiza os blocos com o logotipo real; o banner é renderizado no GitHub e removido no app pelo renderizador seguro contra XSS.)
Notas
- Somente docs — sem mudança de código ou testes; o SVG é embutido (sem recurso remoto). Fontes de varredura sem mudança: 83. Conjunto de testes: 2784.
[1.224.0] — 2026-08-26
Alterado — os blocos de provedores LLM agora mostram os logotipos reais da marca.
Alterado
- Logotipos reais dos provedores. Os blocos de provedor — campos de chave API nas Configurações, as linhas de Uso, o chip do painel, o resumo “Ativo” das Configurações, o resultado da ⚡ eval e o banner de boas-vindas — agora mostram o logotipo real da marca dos 11 provedores que publicam um ícone de código aberto (Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub · DeepSeek · Kimi · MiniMax · Mistral · Ollama; SVGs de caminho único do simple-icons, CC0). Os 7 sem ícone publicado (Hermes · GLM/Z.ai · Grok · Together · Fireworks · BytePlus Ark · Volcengine Ark) mantêm o monograma com a cor da marca. Continua seguro para a CSP por construção: o caminho do logo é uma constante estática embutida — sem recurso remoto, sem
innerHTML.
Notas
- Sem mudança de rota ou comportamento; a mesma API
ProviderLogo.el(slug).provider-logo.jscresceu (os caminhos de logo embutidos). Fontes de varredura sem mudança: 83. Conjunto de testes: 2784.
[1.223.0] — 2026-08-26
Corrigido — uma avaliação ⚡ ao vivo bem-sucedida era pintada como erro; além disso, instale o coworker do OpenWorker direto da URL do GitHub ou .zip.
Corrigido
- Tom do badge de resultado da ⚡ eval. Uma avaliação ao vivo bem-sucedida não é mais colorida como falha. Os provedores em processo (OpenRouter, DeepSeek, …) não retornam
codede subprocesso, então o estritocode === 0era falso e todo sucesso saía vermelho (mesmo mostrando “exit 0”). O tom agora segue o resultado — só um subprocesso que sai com código diferente de 0 fica vermelho; um resultado ao vivo sem código ébadge-ok, e o sufixo “· exit N” só aparece quando há um código real.
Alterado
- Instale o coworker do OpenWorker da URL do GitHub / .zip. A documentação do coworker agora descreve o instalador de um comando (
curl … | bash, idempotente — reutiliza um career-ops / web-ui existente) e as três vias de instalação no OpenWorker (URL do GitHub ·.zip· importação de arquivo único) emdocs/integrations/openworker.md, na ajuda §32 (×17) e no README.
Notas
- Fechada uma lacuna de cobertura antiga: a fonte Recruitee agora tem um teste ponta a ponta da correção de ângulo entre aspas da v1.214.2 (um
>dentro de um atributo de tag não deve vazar para a descrição). Fontes de varredura sem mudança: 83. Conjunto de testes: 2783.
[1.222.0] — 2026-08-26
Alterado — as dicas de campo dos provedores estendidos (DeepSeek … Volcengine Ark) agora estão localizadas nos 17 idiomas.
Alterado
- Dicas de campo de provedores localizadas. Cada
config.<slug>Hintdos 11 provedores compatíveis com OpenAI adicionados em v1.216.0–v1.217.0 (DeepSeek · GLM/Z.ai · Kimi · MiniMax · Mistral · Grok · Together · Fireworks · Ollama · BytePlus Ark · Volcengine Ark) — as dicas de chave API, modelo e URL base em Configurações → Chaves API — agora é traduzida nos 17 idiomas em vez de recorrer ao inglês. URLs de cadastro, ids de modelo e o marcador ⚡ de eval ao vivo são preservados na íntegra.
Notas
- Sem mudança de comportamento — o
hintFallbackem inglês emfield-specs.jsainda protege uma chave ausente, e um novo teste de contrato garante que cadahintKeydos descritores de campo resolve nos 17 idiomas. Fontes de varredura sem mudança: 83. Conjunto de testes: 2779.
[1.221.0] — 2026-08-26
Segurança — defesa em profundidade contra DNS-rebinding no caminho de fetch do scanner.
Segurança
- O núcleo HTTP do scanner (
fetchJson/fetchTextemserver/lib/http-json.mjs) agora resolve o host de cada fonte no caminho de rede real e recusa conectar se resolver para um endereço privado, loopback, link-local, CGNAT ou de metadados de nuvem — fechando um vetor de DNS-rebinding. Os hosts do scanner já são fixados a domínios públicos, então é defesa em profundidade; o caminho de URL do usuário já tinha fixação de conexão mais forte (safe-fetch.mjs). O guard roda apenas no transportefetchreal — umfetchImplde teste injetado nunca é resolvido.
Notas
- Sem mudança de comportamento em uma varredura saudável. Fontes de varredura inalteradas: 83. Testes: 2775.
[1.220.0] — 2026-08-26
Adicionado — varra várias categorias do Get on Board a partir de uma única entrada.
Adicionado
- Uma entrada
getonbrdpode definircategories: [programming, operations-management, machine-learning-ai](ou um únicocategory:) em vez de apenas o feedprogramming; as vagas são deduplicadas por URL entre categorias, com limite de 12. Entradas existentes ficam idênticas (o padrão continuaprogramming).
Notas
- Apenas servidor; o host-pin SSRF (www.getonbrd.com) e
redirect:'error'não mudam; um slug de categoria inválido é rejeitado antes de qualquer requisição. Fontes de varredura inalteradas: 83. Testes: 2771.
[1.219.0] — 2026-08-26
Adicionado — Torre entra no scanner, e agora todo rótulo de provedor está correto.
Adicionado
- Torre (
provider: torre) — o marketplace de talentos pan-LatAm do torre.ai, busca pública sem autenticação com vagas remotas que não chegam ao Greenhouse/Lever/Ashby; uma requisição limitada por entrada. O registro traz agora 83 fontes (78 inglesas + 5 russas). Também: monograma do provedor ativo no banner de boas-vindas.
Corrigido
- A varredura do a16z Speedrun parava cedo em um board que rotaciona (uma página curta no meio da varredura era tratada como a última, truncando para 149 de ~300); agora confia no
total_pagesdo feed. E o banner de boas-vindas resolvia o provedor a partir de um mapa de 4 entradas (mostrando o slug cru para os outros 14) — agora usa o rótulo+monograma dos 18; a nota de provedores das Configurações vai de 7 a 18.
Notas
- A opção de várias categorias por entrada do Get on Board ainda não está aqui (requer um refactor no nível da fonte). Testes: 2768.
[1.218.0] — 2026-08-26
Adicionado — emblemas de provedor (e nomes corretos) onde quer que um provedor apareça.
Adicionado
- Monogramas de provedor no chip do painel, no resumo “Ativo” das Configurações e no cabeçalho do resultado da ⚡ avaliação.
Corrigido
- Rótulos obsoletos: três telas resolviam o nome a partir de um mapa de 5 entradas (o resultado da avaliação rotulava como “Gemini” qualquer provedor não-Anthropic); agora usam
ProviderStatus.label(18). A contagem de chaves mostrava “/ 7” → 18. A lista de slugs do hintLLM_PROVIDERganhouark/arkcnnos 17 idiomas.
Notas
- Apenas cliente, seguro para CSP. Sem mudança de servidor. Fontes de varredura: 82. Testes: 2758.
[1.217.1] — 2026-08-26
Reforço de testes — cobertura de endpoint para todo o catálogo de provedores.
Adicionado
- Um teste parametrizado de
run<Provider>que verifica endpoint, modelo padrão e autenticação Bearer para Kimi, MiniMax, Mistral e Fireworks.
[1.217.0] — 2026-08-26
Adicionado — Ark entra na lista de LLM (18 provedores). BytePlus Ark e Volcengine Ark — os modelos Doubao da ByteDance — agora são provedores ⚡ ao vivo, pelo mesmo núcleo compatível com OpenAI e uma URL base por região.
Adicionado
- BytePlus Ark (
ARK_API_KEY, internacional) e Volcengine Ark (ARK_CN_API_KEY, China) — Chat Completions compatível com OpenAI viarunOpenAICompatible(). DefinaARK_MODEL/ARK_CN_MODEL(um modelo Doubao ou seu id de endpointep-…);ARK_BASE_URL/ARK_CN_BASE_URLtrocam de região. Ambos entram na ordemauto(por último), com seu monograma em Configurações e Uso. A interface web abrange agora 18 provedores ao vivo.
Notas
- Mesmo caminho de configuração confiável + verificação de esquema http(s) do restante (nunca o validador SSRF de URL de vaga). Fontes de varredura inalteradas: 82. Testes: 2755.
[1.216.0] — 2026-08-26
Adicionado — mais nove provedores de LLM, cada um com seu próprio emblema de marca. Suas avaliações ⚡ ao vivo agora podem rodar via DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together, Fireworks ou um Ollama totalmente local — a uma chave de distância, cada um marcado por um monograma ao lado do seu campo em Configurações.
Adicionado
- Nove provedores compatíveis com OpenAI. DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together AI, Fireworks AI e Ollama (totalmente local, sem chave). Defina uma chave — ou
OLLAMA_BASE_URLpara o Ollama — em Configurações e ele entra na ordemautoapós o Hermes; fixe qualquer um comLLM_PROVIDER. O Together também hospeda o Inkling da Thinking Machines (thinkingmachines/Inkling). A interface web já abrange 16 provedores ao vivo. - Emblemas de provedor. Um mosaico de inicial na cor da marca, seguro para CSP, marca cada provedor ao lado do seu campo de chave em Configurações e na sua linha da página de Uso — SVG inline, sem logos externos, nada sai da sua máquina.
Notas
- As URLs base dos provedores são configuração confiável que passa por
runOpenAICompatible()com verificação de esquema http(s) (o loopback do Ollama é permitido), nunca o validador SSRF de URL de vaga. Fontes de varredura inalteradas: 82. Testes: 2752.
[1.215.0] — 2026-08-26
Adicionado — rode toda a sua busca de vagas a partir do OpenWorker. Um novo coworker conduz este pipeline pelo app de coworker de IA open source do Andrew Ng, e a ajuda no app agora o cobre para que o assistente Pergunte aos docs te guie.
Adicionado
- Coworker do OpenWorker. Um coworker sem código do OpenWorker — Fighter90/career-ops-coworker — roda toda a busca de vagas (varrer, pontuar, adaptar, rastrear, dar retorno) e pode abrir este painel, tudo do seu desktop. É uma persona de um único arquivo em 17 idiomas, verificada como instalável no próprio loader do OpenWorker.
- Ajuda §32 “Run it from OpenWorker” (todos os 17 idiomas) documenta isso, para que o assistente Pergunte aos docs do app e o botão flutuante de ajuda respondam sobre ele. Também linkado no rodapé do landing, no README, na wiki e em
docs/integrations/openworker.md.
Notas
- Apenas docs + site — sem mudança de código do app. Fontes de varredura inalteradas em 82. Conjunto de testes: 2742.
[1.214.2] — 2026-08-25
Corrigido — o pipeline HTML→texto compartilhado (descrições de Greenhouse / Recruitee) não vaza mais atributos de tag na descrição quando um valor de atributo contém um caractere >.
Corrigido
- Descrições HTML com
>dentro de um atributo são limpas corretamente. O removedor de tags por trás das descrições de Greenhouse e Recruitee usava um casamento ingênuo que parava no primeiro>, então uma tag como<a title="salary > 100k">deixava o texto de atributo final (100k">) na descrição. Agora ele respeita valores de atributo entre aspas (simples ou duplas), remove a marcação antes de decodificar entidades (para que uma aspa codificada dentro de um atributo não vire um delimitador falso), e ainda preserva um<>literal em texto puro.
Notas
- Conjunto de testes: 2742. Fontes de varredura inalteradas em 82. Nada mais novo do pai nesta rodada: as demais mudanças são retransmitidas em somente leitura (correções de follow-up / weekly-digest / rejection-latency / salary-gap não exigem mudança no web-ui) ou não são superfícies do web-ui (um plugin externo X/Xquik BYO-key, correções de CLI/verify). Os itens opcionais — descrições de detalhe do SmartRecruiters, pulo de agregadores no detect-reposts, prefixos de título
stem:/word:, proteção anti-DNS-rebinding — seguem na fila.
[1.214.1] — 2026-08-25
Corrigido — as descrições do Ashby agora têm o mesmo limite de tamanho que Greenhouse e Recruitee, então um filtro de conteúdo se comporta igual nos três boards e um board não pode inchar o cache de varredura.
Corrigido
- As descrições do Ashby agora respeitam o mesmo limite de tamanho dos outros boards. A v1.214.0 alimentava o corpo do Ashby no filtro de conteúdo, mas — ao contrário de Greenhouse e Recruitee, que limitam a 4000 caracteres — o do Ashby era mapeado direto sem limite (alguns 4× mais longos). Então a mesma palavra do
content_filterpodia casar no Ashby mas não no Greenhouse só dependendo de onde caísse no texto, e um board podia adicionar centenas de KB ao cache de varredura que a#/scancarrega. O Ashby agora é limitado aos mesmos 4000 caracteres (seu texto já é plano, então é truncado, nunca tem tags removidas).
Notas
- Conjunto de testes: 2738. Fontes de varredura inalteradas em 82.
[1.214.0] — 2026-08-25
Corrigido — as descrições de Greenhouse, Ashby e Recruitee agora alimentam o filtro de conteúdo, então filtros de palavras / país / visto realmente casam com esses boards; e “Associate <título sênior>” (incluindo cargos acadêmicos) não é mais classificado como entry-level.
Corrigido
- Os filtros de conteúdo agora funcionam em Greenhouse, Ashby e Recruitee. Esses boards agora carregam o texto completo de cada vaga como sua descrição, então um
content_filter(ou um filtro de palavras de país / visto) casa com o corpo da vaga em vez de deixar o board passar às cegas. Ashby e Recruitee trazem o corpo no payload da lista de graça; o Greenhouse já fazia. Os três passam por um único pipeline HTML→texto compartilhado. - “Associate Director” / “Associate Professor” é sênior, não entry-level. O classificador de senioridade (usado pelo
skip_tiers:) tratava a palavra “associate” como marca de entry-level mesmo quando prefixa um título sênior. Agora lê “Associate <substantivo sênior>” — incluindo cargos acadêmicos (Associate Professor, Associate Dean) — como sênior, mantendo as variantes júnior reais (Associate Attorney, Associate Editor) como entry-level, e ainda classifica um “Intern, Associate Dean” inicial como estágio.
Notas
- Conjunto de testes: 2736. Fontes de varredura inalteradas em 82. Na fila para depois: enriquecimento opcional de descrições do SmartRecruiters (um fetch de detalhe por vaga), o pulo de boards
aggregator: truedo detect-reposts, e os prefixos de título de palavra ancoradastem:/word:— cada um uma configuração opcional, não uma mudança de comportamento padrão. O endurecimento anti-DNS-rebinding continua na fila para sua própria versão de segurança.
[1.213.0] — 2026-08-22
Adicionado — MyCareersFuture, o banco de vagas nacional de Singapura, como fonte de varredura. Corrigido — vagas do Greenhouse agora carregam seu texto completo para os filtros de conteúdo funcionarem, e vagas remotas do Ashby não ficam mais escondidas atrás de uma localização só-cidade.
Adicionado
- MyCareersFuture (Singapura) (mycareersfuture.gov.sg) — uma nova fonte de varredura sem tokens para o banco de vagas público nacional de Singapura, mantido pela Workforce Singapore. Selecione-a no filtro Fonte em
#/scan, ou adicione uma empresa comprovider: mycareersfuturee uma lista opcional dekeywords(recorre aos cargos-alvo do seu perfil, como o Job Bank). Lê a API de busca pública, com host fixado, sem chave.
Corrigido
- Vagas do Greenhouse agora podem ser filtradas por conteúdo. Os boards do Greenhouse são buscados com o corpo completo da vaga, decodificado para texto puro como a descrição — então um
content_filter(ou um filtro por palavras de país/visto) que lê a descrição agora casa de fato com as vagas do Greenhouse em vez de deixá-las passar às cegas. - Vagas remotas do Ashby não são mais descartadas por um filtro de cidade. O Ashby mantém o modelo de trabalho (Remote/Hybrid/Onsite) separado da cidade do escritório, então uma vaga totalmente remota ainda aparecia como p. ex. “San Francisco” — e um filtro de localização bloqueando essa cidade escondia uma vaga que você poderia aceitar. Agora “Remote” é anexado à localização quando a vaga é remota, e
workplaceTypevence umisRemotedesatualizado para não rotular errado uma vaga híbrida ancorada no escritório.
Notas
- Fontes de varredura: 82 (77 em inglês + 5 russas). Conjunto de testes: 2724. Um endurecimento contra DNS-rebinding (validar o endereço resolvido de um host antes de conectar) está na fila para uma versão dedicada — precisa de um design próprio de web-ui, não de um port direto.
[1.212.1] — 2026-08-21
Corrigido — a landing do cvstart.org contava a menos as fontes de vagas do scanner (mostrava 80 e omitia o Job Bank (Canadá)); agora volta a bater com as 81 do app, e o build do site falha ruidosamente se as duas divergirem.
Corrigido
- A contagem de “Fontes de vagas” da landing voltou a ficar em sincronia com o app. Após v1.212.0, o cvstart.org mostrava 80 boards e faltava o novo chip do Job Bank (Canadá), enquanto o app, o dropdown de varredura e o guia de ajuda listavam 81. A landing monta sua lista carregando o registro ao vivo do scanner, e uma fonte falhou ao carregar nesse build pela forma como importava uma dependência YAML, então foi descartada em silêncio. Agora o Job Bank carrega essa dependência de forma preguiçosa, igual ao resto do app no momento da varredura, então sempre aparece.
- O build do site agora se recusa a publicar uma contagem de fontes divergente. Se o registro enumerar menos fontes do que existem em disco (a assinatura de uma fonte que falhou ao carregar), o build falha com uma mensagem clara em vez de publicar o número errado.
Notas
- O comportamento do app não muda — o scanner sempre teve as 81 fontes; só a landing foi afetada. Fontes de varredura: 81 (76 em inglês + 5 russas) — inalterado. Conjunto de testes: 2687.
[1.212.0] — 2026-08-21
Adicionado — Job Bank (Canadá), o board de empregos nacional federal. Removido — EchoJobs (o feed agora está bloqueado por anti-bot). Corrigido — boards com Consider voltam a retornar resultados e vagas Lever multi-localização não escondem mais metade das localizações.
Adicionado
- Job Bank (Canadá) (jobbank.gc.ca) — uma nova fonte de varredura sem tokens para o serviço nacional de emprego federal do Canadá, um board de alto volume que nenhum agregador cobre bem. Selecione-a no filtro Fonte em
#/scan, ou adicione uma empresa comprovider: jobbankcae uma lista opcional dekeywords(recorre aos cargos-alvo do seu perfil). Lê o feed ATOM público, com host fixado, sem chave.
Removido
- EchoJobs — aposentada. Seu feed público agora está atrás de proteção anti-bot e não retorna nada, então mantê-la só desperdiçava um slot de varredura.
Corrigido
- Boards com Consider voltam a retornar resultados. O Consider agora exige um handshake anônimo (um GET que semeia um cookie de sessão + token CSRF) antes de aceitar a busca; sem ele a requisição era rejeitada em silêncio e o board parecia vazio.
- Vagas Lever multi-localização não escondem mais metade das localizações. O Lever põe uma cidade primária em
locatione o resto emallLocations; ler só a primária fazia uma vaga aberta em Barcelona E Montevidéu parecer só-Barcelona (e ser descartada por engano por um filtro de localização). Agora as duas são mescladas.
Notas
- Ritmo entre páginas mais suave (250 ms, antes 150) nos boards paginados, para ser cortês com sites de carreira de host único. Fontes de varredura: 81 (76 em inglês + 5 russas) — inalterado (entra Job Bank, sai EchoJobs). Conjunto de testes: 2685.
[1.211.0] — 2026-08-19
Adicionado — Yourator, job board tech de Taiwan. Corrigido — entidades acentuadas de título/empresa agora decodificam, e uma empresa com acento no nome não é mais sinalizada por engano.
Adicionado
- Yourator (yourator.co) — uma nova fonte de varredura sem tokens para o mercado tech e digital de Taiwan. Selecione-a no filtro Fonte em
#/scan, ou adicione uma empresa comprovider: yourator. Lê a API JSON pública (sem chave, sem navegador), percorre todas as páginas do board e emite o link real do empregador (o ATS dele) com os parâmetros de rastreamento removidos.
Corrigido
- Entidades acentuadas agora decodificam em todo lugar. O decodificador HTML compartilhado ganhou as letras Latin-1 (
é→ é,ç→ ç, …), então um board europeu que escreveDéveloppeurouFrançaisnão deixa mais esse literal num título, no tracker ou num documento gerado. (Maiúsculas continuam maiúsculas —Éé É, não é — e uma busca como&constructor;agora resolve para si mesma.) - Uma empresa com acento no nome não é mais sinalizada por engano por estar no próprio domínio. “Işık” agora dobra para “isik” e casa com isik.com.tr; “Société Générale” casa com societegenerale.com. A checagem antiga apagava as letras acentuadas em vez de dobrá-las para a base ASCII.
Notas
- Fontes de varredura: 81 (76 em inglês + 5 russas). Conjunto de testes: 2667.
[1.210.1] — 2026-08-19
Corrigido — títulos de vagas e nomes de empresa do Habr Career com “&” ou aspas não chegam mais corrompidos.
Corrigido
- A fonte Habr Career agora decodifica entidades HTML no título e no nome da empresa antes de seguir adiante. Os cartões renderizados no servidor chegam com entidades (“Changellenge >>”, “Demand Forecasting & Inventory Optimization”, “ООО "М-ТЕХ"”), então um “&” não decodificado falhava em silêncio o seu próprio filtro de título com “&” — o mesmo sintoma que a versão anterior fechou em outros cinco portais — e nomes de empresa chegavam corrompidos ao tracker e aos relatórios. A decodificação de entidades está completa nas seis fontes afetadas.
Notas
- Conjunto de testes: 2644.
[1.210.0] — 2026-08-19
Adicionado — Senjob, o primeiro job board africano do scanner (Senegal); correspondência de títulos mais precisa em mais cinco portais.
Adicionado
- Senjob (senjob.com) — uma nova fonte de varredura sem tokens para o Senegal, o primeiro board africano do scanner. Selecione-a no filtro Fonte em
#/scan, ou adicione uma empresa comprovider: senjob. Lê a listagem pública por HTTP simples (sem chave, sem navegador), fixa cada requisição em senjob.com e — ao analisar HTML — trata uma listagem que de repente não retorna nada como um board quebrado (um erro visível) em vez de um país sem vagas.
Corrigido
- Títulos com “&” não descartam mais vagas em cinco portais — em beesite, Cornerstone (csod), Hacker News “Who is hiring”, Phenom e TKMS os títulos chegam com entidades HTML, então um “&” escapado num cargo como “R&D Engineer” falhava a sua própria palavra-chave “r&d” e a vaga sumia em silêncio (um veto “sales & marketing” também nunca disparava). Agora os títulos — e as localizações do Phenom — são decodificados antes de filtrar.
Notas
- Fontes de varredura: 80 (75 em inglês + 5 russas). Conjunto de testes: 2643.
[1.209.0] — 2026-08-17
Adicionado — a ajuda no app agora cobre como registrar o resultado de uma candidatura, e o “Pergunte aos docs” pode te levar até lá.
Adicionado
- A ajuda do Rastreador (§11) ganhou uma seção “Registrar um resultado” nos 17 idiomas, percorrendo o botão Resultado: escolha o que aconteceu (recusado / oferta / contratado / recusada / ignorado / avançou para entrevista), pré-visualize o que fará e registre — o que anota o resultado, arquiva o CV e a carta que você enviou, e sincroniza o Status da linha para você. O assistente flutuante “Pergunte aos docs” lê o guia, então agora te leva a esse botão em vez de só sugerir editar o Status à mão.
Notas
- Cada pacote de ajuda agora é 31 H2 / 119 H3 (era 118); os guards de paridade foram ajustados. Somente documentação — sem mudança de código ou comportamento. Conjunto: 2625.
[1.208.2] — 2026-08-16
Corrigido — no celular os botões de notificações e tema não ficam mais sobre a caixa de busca.
Corrigido
- A v1.208.1 impediu que os botões da barra superior sobrepusessem o título da página, mas num celular estreito — embora não o mais estreito — e principalmente em idiomas com rótulos longos, a barra inteira ainda se espremia numa única linha, então os botões 🔔 e 🌙 podiam ficar sobre a caixa de busca. Agora os botões de ação (notificações, tema, Diagnóstico, Abrir Scan) sempre passam para a própria segunda linha de largura total no celular, então a caixa de busca aparece inteira e nada se sobrepõe.
Notas
- No celular os botões de ação da barra vão para uma segunda linha de largura total, removendo a frágil faixa de “linha quase cheia” onde o layout distribuía o espaço negativo restante como sobreposição. Um guard do Playwright agora reproduz o gatilho exato — um idioma de rótulos longos na faixa 565–640px — e verifica que os controles da barra nunca compartilham pixels. Conjunto: 2621.
[1.208.1] — 2026-08-16
Corrigido — no celular os botões da barra superior não sobrepõem mais a página.
Corrigido
- A v1.208.0 quebrava os botões da barra superior (Diagnóstico, Abrir Scan, notificações, tema) para uma segunda linha em telas estreitas, mas a barra mantinha altura fixa, então a linha quebrada transbordava e ficava sobre o título da página. Agora a barra cresce para acomodar suas linhas e o conteúdo flui abaixo.
Notas
- A
heightfixa da barra viroumin-height, então ela cresce com o conteúdo em qualquer largura (o desktop não muda). Um guard do Playwright agora também verifica que a barra não transborde sobre a página. Conjunto: 2621.
[1.208.0] — 2026-08-16
Corrigido — o app agora cabe na tela de um celular: acabou a rolagem lateral.
Corrigido
- Em telas estreitas o app todo deslizava para o lado — a barra superior, as tabelas, os artigos de ajuda e as abas de configurações passavam da borda direita. Agora cada página cabe em qualquer largura: os botões da barra superior vão para uma segunda linha, tabelas e blocos de código largos rolam dentro da própria caixa, a ajuda empilha o índice acima do artigo, as linhas de botões/abas quebram, e caminhos ou URLs longos quebram em vez de esticar a página.
Notas
- A causa foi a clássica armadilha min-width: auto de flex/grid mais alguns elementos largos sem envoltório; resolvido com
min-width: 0nos itens de grid,overflow-wrapno markdown/títulos, uma tabela de markdown rolável e o empilhamento da grade de ajuda no ponto móvel. Um guard do Playwright verifica 0 estouro horizontal a 375 px nas rotas principais.tests/playwright-smoke.mjs. Conjunto: 2621.
[1.207.2] — 2026-08-16
Corrigido — planos de IA e perfis de orientação não aparecem mais como um despejo de código.
Corrigido
- Alguns modelos envolvem toda a resposta em uma cerca de código
markdown …. Quando isso acontecia, o plano de desenvolvimento e o perfil de orientação apareciam como um bloco monoespaçado em vez de um documento com títulos e listas. Agora a cerca envolvente é removida — só quando envolve toda a resposta e é explicitamentemarkdown/md, então uma resposta real depython/js/``` sem linguagem é preservada.
Notas
- Resolvido uma única vez na etapa compartilhada de limpeza de LLM (
cleanLlmMarkdown), então todas as rotas de IA se beneficiam, e blocos de código internos sobrevivem.tests/llm-output.test.mjs(+3). Conjunto: 2621.
[1.207.1] — 2026-08-16
Corrigido — a página inicial não transborda mais para o lado em celulares pequenos.
Corrigido
- Num celular estreito o hero — o título, a linha de introdução e o terminal de instalação — podia ser cortado pela borda direita porque um comando de instalação longo e as colunas do layout não encolhiam para a tela. Agora cabem em qualquer largura; o comando de instalação rola dentro do próprio terminal.
Notas
- Também reforçamos uma verificação E2E instável que podia falhar por um 404 transitório de um recurso: agora ignora o ruído de rede benigno (favicon / conexão / recurso falho) como as verificações irmãs, sem deixar de detectar erros reais de script. Sem mudança no comportamento da aplicação. Conjunto: 2618.
[1.207.0] — 2026-08-15
Adicionado — registre o resultado de uma candidatura direto do rastreamento.
Adicionado
- Cada linha do rastreamento ganha uma ação Resultado: escolha o que aconteceu (rejeitado, oferta recebida, contratado, oferta recusada, sem resposta, avançou para entrevista), adicione uma nota opcional, pré-visualize o status resultante e registre. Registrar arquiva os artefatos do CV e da carta enviados e sincroniza o rastreamento ao estado canônico — uma ação determinística em vez de editar o rastreamento à mão.
Notas
- Nova
POST /api/outcomeretransmite a CLI de resultados:dryRun:trueé uma prévia somente leitura (localiza a linha, informa o estado resultante, não escreve nada); uma chamada real registra. Segurança de escrita: o tipo de resultado é restrito ao conjunto conhecido e todo campo de texto é rejeitado se tiver caracteres de controle antes do shell-out (argumentos como array, spawn — sem shell).tests/outcome-route.test.mjs. Conjunto: 2618.
[1.206.0] — 2026-08-15
Documentação — o guia de ajuda integrado agora cobre os cinco recursos mais recentes, em todos os 17 idiomas.
Adicionado
- O guia de ajuda integrado —e o assistente «Pergunte à ajuda» que responde com base nele— agora documenta cinco recursos recentes: Doutor de configuração (Configurações — verifica seu CV e perfil em busca de lacunas e dados de exemplo esquecidos), Descobrir painéis ATS (Portais — encontra automaticamente o portal de vagas de uma empresa), a verificação «ainda ativa?» (Rastreamento — se uma vaga ainda está aberta), a dica «reutilizar um CV anterior?» (CV Studio — avisa quando um CV já adaptado serve para uma nova vaga) e o Registro de habilidades (Análises — registra pontuações de autoavaliação). Cinco novas subseções, traduzidas para os 17 idiomas.
Notas
- A estrutura do guia cresce para 31 H2 / 118 H3, com paridade garantida em cada idioma. Documentação de referência atualizada:
docs/architecture/API.mddocumenta as cinco rotas desses recursos, e os contadores de rotas/versão emCLAUDE.mdedocs/sdd/CONVENTIONS.mdestão em dia (36 módulos de rota). Conjunto: 2610.
[1.205.0] — 2026-08-15
Adicionado — um Registro de habilidades para anotar resultados de testes/avaliações.
Adicionado
- Um novo Registro de habilidades (Análises → Registro de habilidades) permite anotar uma autoavaliação — empresa, plataforma, habilidade, pontuação % e uma nota opcional — anexada a
data/assessments.tsv, com uma lista das entradas anteriores (mais recentes primeiro). Sem tokens, determinístico; o formato é do CLI do projeto pai.
Notas
- Nova
GET /api/assessments(faz relay da lista JSON padrão deassessment-log.mjs; à prova de falhas{available:false}) +POST /api/assessments(gravação explícita: campos passados como args de array paraassessment-log.mjs add). Segurança de escrita: qualquer campo com caracteres de controle é rejeitado (um TAB quebraria uma coluna, uma quebra de linha injetaria uma linha) → 400 antes de gravar; pontuação/limiar de 0–100; comprimentos limitados.tests/assessments-route.test.mjs. Conjunto: 2610.
[1.204.0] — 2026-08-15
Adicionado — um painel “Doutor de configuração” em Ajustes que sinaliza um CV/perfil incompleto ou com dados de exemplo.
Adicionado
- Ajustes → Doutor de configuração agora faz uma checagem sem tokens do seu
cv.mdeconfig/profile.ymle lista problemas bloqueantes (arquivos/campos faltando) e avisos (dados de exemplo remanescentes, métricas fixas) — para você pegar uma configuração incompleta antes que ela enfraqueça suas varreduras e adaptações. Somente leitura; re-execução com um clique.
Notas
- Nova rota somente leitura
GET /api/cv-sync-checkque faz relay decv-sync-check.mjsdo projeto pai, que imprime texto + código de saída (sem--json); a rota faz parsing leve das linhas estáveisERROR:/WARN:em{ok, errors[], warnings[]}— quem decide é o banner, não o código de saída. À prova de falhas{available:false}em instalações independentes.tests/cv-sync-check-route.test.mjs. Conjunto: 2602.
[1.203.0] — 2026-08-15
Adicionado — uma dica “reutilizar um CV anterior?” no CV Studio.
Adicionado
- Ao abrir uma vaga salva no CV Studio, o app agora a compara com suas outras vagas salvas (sobreposição de palavras determinística, zero tokens) e diz se a mais parecida basta para reutilizar aquele CV adaptado, reutilizá-lo com ajustes ou adaptar um novo — para você não regerar do zero uma vaga que já trabalhou.
Notas
- Nova rota somente leitura
GET /api/jds/:name/reuseque faz relay dejd-similarity.mjsdo projeto pai (sobreposição Jaccard + guarda de senioridade; JSON{decision, score, reason}) uma vez por vaga anterior (fan-out limitado a 25, a melhor vence); à prova de falhas{available:false}se faltar o script ou vagas anteriores.tests/jd-similarity-reuse-route.test.mjs. Conjunto: 2594.
[1.202.0] — 2026-08-15
Adicionado — descubra o quadro ATS de uma empresa em #/portals e comece a rastreá-lo.
Adicionado
- Em #/portals, digite o nome de uma empresa e o app sonda Greenhouse, Ashby e Lever pelo quadro público — zero LLM, sem navegador — e mostra os quadros que existem e listam ≥1 vaga. Um clique adiciona o quadro escolhido às empresas que seu scanner monitora. A sondagem é somente leitura; a gravação em
portals.ymlsó ocorre ao clicar em Adicionar.
Notas
- Novo
server/lib/discover-ats.mjs(sondagem de slug com host fixo e charset validado viasafeGetcom DNS fixado, ≤12 sondagens/requisição) +POST /api/portals/discover(somente leitura) ePOST /api/portals/track(gravação explícita:withFileLock+ emenda de texto + reverificação + renomeação atômica; só hosts ATS conhecidos, idempotente). Reutiliza o registro de adaptadores do scanner. i18n ×17.tests/discover-ats-resolver.test.mjs+tests/discover-ats-route.test.mjs. Conjunto: 2588.
[1.201.0] — 2026-08-15
Corrigido — um rastreador com cabeçalhos de coluna localizados ou variantes não fica mais em branco.
Corrigido
- Se o seu
data/applications.mdusa cabeçalhos não ingleses ou variantes —empresa/puesto/estado/fecha/enlace, ouposition/stage/link— o rastreador os lia com as chaves erradas e mostrava colunas Empresa / Cargo / Status / Data / Link em branco. Agora esses cabeçalhos são mapeados para os nomes de campo canônicos e o rastreador exibe corretamente. Um rastreador em inglês é processado igual a antes.
Notas
- Novo mapa
HEADER_ALIASESe uma dobra de normalização emparseApplications(server/lib/parsers.mjs); cabeçalhos desconhecidos ou já canônicos passam sem alteração.tests/tracker-header-aliases.test.mjs. Conjunto: 2563.
[1.200.0] — 2026-08-15
Adicionado — verificação com um clique “ainda ativa?” para vagas em ATS no seu rastreador.
Adicionado
- Em #/tracker, uma candidatura cuja URL seja uma publicação de Greenhouse / Lever / Ashby / Workday / SmartRecruiters agora mostra um botão “Ainda ativa?”. Um clique consulta o JSON público do próprio ATS — zero tokens, sem navegador — e mostra Ativa / Expirada / Desconhecida, para você identificar vagas mortas sem abrir cada uma. Conservador por design: só um 404/410 definitivo vira Expirada; o ambíguo fica Desconhecida (nunca uma falsa Expirada).
Notas
- Novos
server/lib/liveness-core.mjs+liveness-api.mjse uma rota somente leituraGET /api/liveness?url=(sem gravações, sem LLM). Seguro contra SSRF: a URL passa porisValidJobUrle a API do ATS é acessada só viasafeGet(DNS fixado) com host fixo e segmentos validados.tests/liveness-core.test.mjs+tests/liveness-route.test.mjs. Conjunto: 2557.
[1.199.0] — 2026-08-15
Corrigido — tabelas largas agora rolam lateralmente em vez de serem cortadas.
Corrigido
- Na página Scan (e em todas as outras tabelas — Rastreador, Estatísticas, Uso, Painel) uma tabela mais larga que a janela ficava cortada sem barra de rolagem, deixando as últimas colunas inacessíveis. Agora tabelas largas exibem uma barra de rolagem horizontal quando necessário, então toda coluna permanece acessível em qualquer largura.
Notas
.table-wrapempublic/css/components.csspassou deoverflow: hiddenparaoverflow-x: auto(espelha o contêiner.reports-scroll); a borda arredondada é preservada.tests/table-wrap-scroll.test.mjs. Conjunto: 2540.
career-ops-ui v1.267.1 · page 3/9 · mirror of the project changelog for cvstart.org