Changelog

GitHub

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

[1.26.0] — 2026-05-14

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

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


[1.25.0] — 2026-05-14

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

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

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

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

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

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

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

🚫 Adiado para um release futuro

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

🧪 Testes

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

Verificação

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

# G-014 — modo manual retorna em < 2 s mesmo com ANTHROPIC_API_KEY definida:
$ ANTHROPIC_API_KEY=sk-ant-test PORT=4317 npm start &
$ sleep 3
$ time curl -sS -X POST -H 'Content-Type: application/json' \
    -d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/x","mode":"manual"}' \
    http://127.0.0.1:4317/api/auto-pipeline | head -20
# real  0m0.1xx s  (era 1-3 min)
# event: start … event: step (×5) … event: done {"mode":"manual","prompt":"…"}

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

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

Mudanças incompatíveis

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

Fora de escopo (v1.26+)

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

[1.24.1] — 2026-05-14

Hot-fix: crash em #/config nos 8 locales (G-015).

🚑 Hot-fix crítico

  • fix(config): G-015 — replace removed Element.prototype.also call in config.js (public/js/views/config.js:371) — o N-2 da v1.22.0 removeu o monkey-patch global de Element.prototype.also e migrou cv.js para um padrão de instrução livre, mas passou batido em config.js. Resultado: #/config crashava na primeira invocação em todos os locales com c(...).also is not a function. A v1.24.1 aplica o mesmo padrão de migração de cv.js:188-201 — extrai a árvore para um const root = c(...), executa o bloco de ativação por conta própria e então return root;.

🛡️ Gate de CI

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

🧪 Testes

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

Verificação

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

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

Fora de escopo (adiado para v1.25)

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

[1.24.0] — 2026-05-14

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

📖 Refresh de profundidade do help-bundle

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

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

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

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

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

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

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

🧪 Testes

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

Verificação

$ npm test                            # 474 / 474

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

$ for url in what-is-career-ops scan-job-portals apply-for-a-job \
             batch-evaluate-offers set-up-playwright; do
    echo -n "$url: "
    grep -c "$url" docs/help/en.md
  done
# what-is-career-ops: 4
# scan-job-portals: 5
# apply-for-a-job: 3
# batch-evaluate-offers: 5
# set-up-playwright: 3

# Cenário 31.6 — cobertura 40/40 de URLs:
$ for lang in en es pt-BR ko ja ru zh-CN zh-TW; do
    echo -n "$lang: "
    for url in what-is-career-ops scan-job-portals apply-for-a-job \
               batch-evaluate-offers set-up-playwright; do
      curl -sS "http://127.0.0.1:4317/api/help/$lang" \
        | python3 -c "import sys,json; print(json.load(sys.stdin).get('markdown',''))" \
        | grep -q "$url" && echo -n "✓ " || echo -n "✗ "
    done
    echo
  done

Mudanças incompatíveis

Nenhuma.

Fora de escopo (v1.25+)

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

[1.23.0] — 2026-05-14

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

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

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

    A v1.23.0 remodela o loop de polling:

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

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

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

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

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

🖼️ Screenshots localizados do dashboard em cada README

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

🧪 Testes

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

Verificação

$ npm test
# 474 / 474

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

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

$ grep -h 'dashboard-' README*.md | sed -E 's/.*(dashboard-[^)]+).*/\1/' | sort -u
# dashboard-en.png    (somente README.md)
# dashboard-es.png    dashboard-ja.png
# dashboard-ko-KR.png dashboard-pt-BR.png
# dashboard-ru.png    dashboard-zh-CN.png  dashboard-zh-TW.png

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

Mudanças incompatíveis

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

Fora de escopo (v1.24+)

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

[1.22.0] — 2026-05-14

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

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

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

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

🔒 Resiliência

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

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

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

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

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

♿ Acessibilidade

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

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

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

🧹 Qualidade / portabilidade

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

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

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

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

📚 Docs + system prompts

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

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

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

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

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

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

🧪 Testes

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

Verificação

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

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

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

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

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

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

Mudanças incompatíveis

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

Fora de escopo (v1.23+)

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

[1.21.0] — 2026-05-14

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

🛡️ Segurança

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

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

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

🔒 Concorrência

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

♿ Acessibilidade

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

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

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

🌐 i18n

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

🧪 Testes

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

Verificação

npm test                              # 461 / 461
npm run test:e2e:browser              # 32 / 32
node --test tests/ssrf-redirect-rebind.test.mjs tests/path-traversal.test.mjs \
  tests/concurrent-tracker-write.test.mjs tests/rate-limit.test.mjs \
  tests/a11y-form-wires.test.mjs      # 34 testes novos, todos verdes

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

# Rate-limit em bind público:
HOST=0.0.0.0 LLM_RATE_LIMIT=3/60s npm start &
for i in 1 2 3 4; do
  curl -sS -o /dev/null -w '%{http_code} ' -X POST -H 'Content-Type: application/json' \
    -d '{"jd":"…"}' http://0.0.0.0:4317/api/evaluate
done
# → 200 200 200 429

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

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

Fora de escopo (v1.22+)

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

[1.20.0] — 2026-05-13

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

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

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

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

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

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

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

🌍 Paridade de README não-EN

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

🧹 Alias /api/scan-ru/config aposentado

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

🧪 Testes

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

Verificação

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

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

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

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

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

Mudanças incompatíveis

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

Fora de escopo (v1.21+)

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

[1.19.0] — 2026-05-13

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

♿ Passe de contraste WCAG 1.4.3

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

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

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

🛠️ HH_USER_AGENT removido da UI

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

🧪 Testes

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

Verificação

npm test                              # 427 / 427

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

# HH_USER_AGENT não mais em /api/config:
curl -s http://127.0.0.1:4317/api/config | jq '.values | keys'
# → ["ANTHROPIC_API_KEY","ANTHROPIC_MODEL","GEMINI_API_KEY","GEMINI_MODEL","HOST","PORT"]
# (sem HH_USER_AGENT)

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

Fora de escopo (v1.20+)

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

[1.18.0] — 2026-05-13

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

🚪 Breaking

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

♿ Acessibilidade (passe WCAG 2.2 AA)

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

📚 i18n long-tail

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

🛠️ Misc

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

🧪 Testes

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

Verificação

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

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

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

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

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

Fora de escopo (v1.19+)

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

[1.17.0] — 2026-05-13

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

🐛 Correções

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

✨ Features

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

♿ Acessibilidade

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

📚 Documentação

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

🛠️ Tooling

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

🧪 Testes

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

Verificação

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

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

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

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

Fora de escopo (v1.18+)

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

[1.16.0] — 2026-05-13

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

✨ Features

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

🛠️ Tooling + CI

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

🎨 UX

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

📚 i18n long-tail

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

🧪 Testes

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

Fora de escopo (v1.17+)

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

Verificação

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

# Smoke curl do auto-pipeline:
curl -N -X POST http://127.0.0.1:4317/api/auto-pipeline \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/4567"}'

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

[1.15.0] — 2026-05-13

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

✨ Features

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

📚 Documentação

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

🧹 Limpezas carryover

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

🧪 Testes

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

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

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

Verificação

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

[1.14.0] — 2026-05-13

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

✨ Features

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

📚 Docs

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

🧪 Testes

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

Verificação

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

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

Fora de escopo (follow-up adiado)

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

[1.13.0] — 2026-05-13

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

✨ Features

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

🎨 Correções de UX

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

🧪 Testes

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

Fora de escopo (follow-up adiado)

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

Docs

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

[1.12.0] — 2026-05-13

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

✨ Features

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

🐛 Correções

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

📝 Branding / docs

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

🧪 Testes

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

📋 Interno

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

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

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

[1.11.1] — 2026-05-13

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

📝 Docs

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

✨ UI

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

📋 QA

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

Fora de escopo (adiado)

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

Postura de testes

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

Docs

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

[1.11.0] — 2026-05-13

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

📝 Docs

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

✨ Melhorias de UI

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

🔧 Interno

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

Auditoria (gaps adiados, NÃO neste release)

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

Postura de testes

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

Docs

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


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