## [0.9.0] - 2026-07-07

### Alterado (GAP-WS-106 — ergonomia da CLI: flags globais, erros acionáveis, auto-degradação de feature)
- Nove flags agora são `global = true` em `CliArgs` (`src/cli.rs`), aceitas ANTES OU DEPOIS do subcomando `deep-research`: `-n`/`--num`, `-f`/`--format`, `-o`/`--output`, `-t`/`--timeout`, `-l`/`--lang`, `-c`/`--country`, `-p`/`--parallel`, `-q`/`--quiet`, `-v`/`--verbose` (verbose hoisted por simetria com `conflicts_with = "quiet"`). Estende o precedente do GAP-WS-058/059/B3 (que hoistou `--allow-lite-fallback`, `--pre-flight`, `--global-timeout`) para as flags mais usadas.
- `run()` em `src/lib.rs` substituiu `RootArgs::parse()` por `try_parse()`; ao detectar `ErrorKind::UnknownArgument` para uma flag local conhecida posicionada após o subcomando, anexa uma dica explicando que a flag deve aparecer ANTES do subcomando (caso agora raro — só flags locais como `--pages` disparam; as 9 hoisted aceitam qualquer posição). `DisplayHelp`/`DisplayVersion` seguem deferidos para `Error::exit()` para preservar exit 0.
- Novo helper público `is_known_global_flag(&str) -> bool` em `src/cli.rs` casa as 9 shorts/longs hoisted e todas as longs locais de `CliArgs`.

### Corrigido (GAP-WS-106 — auto-degradação de feature substitui fail-fast exit 2)
- `execute_deep_research` (`src/lib.rs`): sem Chrome utilizável (build sem feature `chrome`, `DUCKDUCKGO_SEARCH_CLI_NO_CHROME=1` ou falha na detecção do Chrome) o subcomando NÃO ABORTA MAIS com exit 2 (`INVALID_CONFIG`) citando `--no-news` — agora aplica `effective_no_news = true` automaticamente COM warning no stderr (via `output::emit_stderr`) e prossegue web-only. `--no-news` permanece como opt-in/noop explícito para retrocompatibilidade.
- `build_config` (`src/lib.rs`): `--vertical news|all` em build sem `chrome` (ou com `DUCKDUCKGO_SEARCH_CLI_NO_CHROME=1`) NÃO RETORNA MAIS `Err(InvalidConfig)` (exit 2) — rebaixa para `VerticalMode::Web` COM warning no stderr e prossegue. `convert_vertical` ganhou `#[cfg_attr(not(feature = "chrome"), allow(dead_code))]`.

### Adicionado (GAP-WS-106 — testes)
- `tests/global_flags.rs` (novo): cobertura ponta-a-ponta do Sintoma A (flag desconhecida NÃO dispara a dica; flag local conhecida após o subcomando DISPARA a dica PT-BR via `deep-research --pages 3 rust`) e Sintoma C (`deep-research` aceita `--no-news` implícito em build sem chrome).
- `src/cli.rs::mod tests`: três testes de regressão (`quiet_global_aceito_apos_subcomando`, `output_global_aceito_apos_subcomando`, `is_known_global_flag_cobre_todas_as_flags_do_root_parser`).

### Validação
- `cargo build` e `cargo build --no-default-features` — ZERO erros/warnings
- `cargo test` — 430 (default) / 412 (`--no-default-features`) testes passando
- `cargo clippy --all-targets` — ZERO warnings em ambas as configurações
- `cargo fmt --check` — ZERO diferenças
- Smoke: `--version` exit 0; `--help` exit 0; `deep-research --pages 3 rust` imprime a dica de posicionamento

### Observação
- A skill embutida `duckduckgo-search-cli-pt` no `CLAUDE.md` ainda diz que `--vertical news` é proibido sem chrome; após v0.9.0 ele é ACEITO COM WARNING + rebaixamento para Web. O `CLAUDE.md` é intencionalmente intocado (regra do projeto proíbe editá-lo); a imprecisão será corrigida em um ciclo futuro de manutenção da skill.

## [0.8.9] - 2026-07-06

### Adicionado (GAP-WS-104 — busca cobria APENAS a vertical web, a vertical de notícias NUNCA era visitada)
- Nova flag `--vertical <web|news|all>` (default `web`): opt-in para a vertical de notícias do DuckDuckGo (`ia=news&iar=news`)
- `--vertical news` retorna apenas notícias (`resultados: []`); `--vertical all` retorna web E notícias na MESMA sessão Chrome (warm-up único, news best-effort)
- A vertical news roteia EXCLUSIVAMENTE pelo transporte Chrome-primary — a SERP news exige renderização JavaScript e NÃO tem fallback HTTP (endpoints html/lite estruturalmente não possuem vertical news)
- Após a navegação, a CLI faz poll do DOM renderizado pelo módulo React `[data-react-module-id="news"]` (loop `tokio::time::sleep` com timeout); no timeout ainda extrai e deixa a cascata decidir
- Cascata de extração: Estratégia A (seletores semânticos da seção `[news]` do `selectors.toml`, hot-fixáveis sem recompilar) → Estratégia B (fallback agnóstico de classe baseado em âncoras externas + heurística de data relativa para padrões PT "há N ..." e EN "N ... ago")
- Links internos duckduckgo.com são filtrados; resultados deduplicados por URL preservando ordem; thumbnails protocol-relative resolvidas para `https://`
- Captura do HTML news usa cap de 1 MiB (SERP web mantém 256 KiB) — a SERP React de notícias é mais pesada
- `--num` limita resultados news da mesma forma que limita web (padrão GAP-WS-090)
- Novos campos no envelope JSON, emitidos SOMENTE com `--vertical news|all`: raiz `noticias[]` (`posicao`, `titulo`, `url` garantidos; `fonte`, `data_relativa`, `thumbnail` opcionais), raiz `quantidade_noticias` e `metadados.vertical_usada` — o modo web default permanece byte-idêntico à v0.8.8
- `data_relativa` é mantida verbatim como renderizada pelo DuckDuckGo (ex. "há 2 horas", "3 hours ago") — sem conversão para data absoluta nesta iteração
- Nova variante de `ZeroCause` `vertical-sem-resultados`: zero notícias legítimo (SERP renderizada sem articles) ⇒ exit 5, NÃO 6; interstitial anti-bot no body news continua classificando como `anti-bot`
- Total do exit code agora soma `quantidade_noticias`: news-only com notícias encontradas ⇒ exit 0; `--vertical all` com web>0 e news=0 ⇒ sucesso com `noticias: []`
- Guardas de configuração (exit 2, `INVALID_CONFIG`): `--vertical news|all` rejeita múltiplas queries (`--queries-file` ou múltiplas posicionais), o subcomando `deep-research`, builds sem a feature `chrome` e `DUCKDUCKGO_SEARCH_CLI_NO_CHROME=1`
- `--fetch-content` segue atuando SOMENTE sobre `resultados[]` — resultados news nunca passam por fetch de conteúdo
- Alternativa rejeitada documentada: o endpoint interno `news.js?vqd=` foi descartado porque o token `vqd` rotaciona por query e o endpoint é não documentado e instável

### Adicionado (GAP-WS-105 — deep-research agora executa a vertical news por padrão, dual web + news)
- `deep-research` agora varre a vertical news por PADRÃO: cada sub-query executa como `--vertical all` — a MESMA sessão Chrome navega a SERP web e depois a SERP de notícias (nenhuma sessão extra, nenhuma pista paralela dedicada)
- Nova flag `--no-news` (exclusiva do deep-research): opt-out que rebaixa todas as sub-queries para a vertical web pura — recomendada para CI e ambientes sem Chrome
- Guard fail-fast: sem um Chrome utilizável (feature `chrome` não compilada, `DUCKDUCKGO_SEARCH_CLI_NO_CHROME=1` ou falha na detecção do Chrome) e sem `--no-news`, o subcomando aborta ANTES do fan-out com exit 2 (`INVALID_CONFIG`) e mensagem citando `--no-news`
- Agregação de notícias roda em espaço de score RRF SEPARADO (`aggregate_news` / `AggregatedNewsItem`) — scores news NUNCA são fundidos com o RRF web (scores computados sobre listas distintas não são comparáveis); dedupe por URL canônica; empates desfeitos por recência (parse interno de `data_relativa` como "há 2 horas" / "3 hours ago"; o JSON mantém a string VERBATIM)
- Novos campos no envelope do deep-research, SEMPRE serializados: raiz `noticias[]` (`posicao`, `titulo`, `url`, `score`, `ocorrencias` garantidos; `fonte`, `data_relativa`, `thumbnail` opcionais) — array vazio com `--no-news` ou zero notícias; raiz `quantidade_noticias`; `metadados.total_noticias_unicas`
- Novos campos OPCIONAIS por sub-query: `metadados.sub_queries[].quantidade_noticias` (omitido com `--no-news` ou quando a news ficou indisponível) e `metadados.sub_queries[].news_indisponivel` (`true` quando a varredura news era esperada mas o Chrome caiu em voo e a sub-query degradou para HTTP web — nunca silêncio)
- Síntese dual (`--synthesize`): a seção web mantém o formato atual sob ~70% do `--budget-tokens` e uma seção "Notícias recentes" consome os ~30% restantes; com `--no-news` ou zero notícias o formato do relatório permanece inalterado
- Exit codes: exit 0 quando QUALQUER vertical produziu resultados (web>0 OU news>0); exit 5 (`ZERO_RESULTS`) somente quando web E news estão ambos vazios
- Batch multi-query agora ACEITA `--vertical news|all` (guard removido): `--queries-file` e múltiplas queries posicionais funcionam — cada query roda sua própria sessão Chrome no fan-out paralelo, e cada item de `buscas[]` carrega seus próprios `noticias[]` / `quantidade_noticias`

### Corrigido (pós-revisão do GAP-WS-104 — F1..F7)
- F1: falha de runtime do Chrome (launch ou navegação) sob `--vertical news` (news-only) propagava erro cru até `lib.rs`, produzindo stdout VAZIO com exit 1 e quebrando a garantia de que `-f json` sempre emite envelope JSON — agora um envelope estruturado no padrão `failure_output` é emitido (`resultados: []`, `noticias: []`, `erro`/`mensagem` preenchidos, `causa_zero: resposta-invalida` ⇒ exit 6 sob strict, exit 5 sob opt-out legado `DUCKDUCKGO_ZERO_CAUSE_STRICT=false`)
- F2: `--pre-flight` sondava o endpoint HTML web (reqwest) incondicionalmente, mesmo sob `--vertical news` onde a news é Chrome-only sem fallback HTTP — um falso positivo abortava com exit 3 sem nunca tentar a SERP de notícias; o probe agora roda SOMENTE quando a execução inclui a vertical web (`web`/`all`), e o modo news-only registra aviso informativo de pulo no stderr
- F3: sob `--vertical all` com web>0 e a SERP de notícias bloqueada por interstitial anti-bot, o diagnóstico era descartado silenciosamente (`causa_zero` fica `None` por semântica — descreve o zero TOTAL do envelope); um `tracing::warn` estruturado no stderr agora carrega o diagnóstico e a sugestão de ação — SEM campos novos no envelope JSON
- F4: gate `cargo clippy --all-targets --no-default-features -- -D warnings` voltou a ficar verde — `use crate::extraction;` sob `#[cfg(feature = "chrome")]` e `mut effective_identity_tag` anotado com `#[cfg_attr(not(feature = "chrome"), allow(unused_mut))]`
- F5: o token de cancelamento (Ctrl+C/timeout global) era descartado em TODO o transporte Chrome (não usado no caminho web, ausente no caminho news) — as operações Chrome de launch/web/news agora disputam o token via `tokio::select!` no nível do pipeline, retornando a mesma classe de erro de cancelamento do caminho reqwest (`network_error`, "execution cancelled") e desligando o browser no ramo cancelado
- F6: `news_meta_from_ancestors` no extrator de notícias parava de subir a cadeia de ancestrais assim que `fonte` OU `data_relativa` era encontrado, perdendo o outro campo quando eles vivem em níveis de ancestral diferentes — agora sobe até 4 níveis enquanto QUALQUER campo ainda for None (o valor mais interno vence); coberto pelo teste de regressão `news_meta_from_ancestors_finds_date_above_source_level` (vermelho→verde)
- F7: `parse_news_selector` usava um caminho de panic estilo `expect()` quando um seletor vindo do `selectors.toml [news]` falhava no parse — substituído por match aninhado livre de panic com fallback para o seletor universal `*` (OnceLock), coberto pelo teste `news_selectors_defaults_all_compile`

### Validação
- `cargo build` — ZERO erros
- `cargo clippy` — ZERO warnings
- `cargo fmt --check` — ZERO diferenças
- `cargo test` — ZERO falhas
- Fixtures: SERP Estratégia A (7 articles, 1 armadilha interna filtrada), SERP com classes ofuscadas (Estratégia B), SERP vazia (`noticias: []`)
- Contrato do modo web byte-idêntico à v0.8.8 (sem `noticias`/`quantidade_noticias`/`vertical_usada` emitidos)


## [0.8.8] - 2026-06-24

### Corrigido (GAP-WS-089 — spawn_virtual_display() lock files stale esgotam pool Xvfb 99..200)
- `spawn_virtual_display()` iterava displays 99..200 verificando APENAS `Path::exists()` no lock `/tmp/.X{N}-lock`
- NÃO verificava se o PID dentro do lock file estava vivo
- Após ~100 execuções canceladas ou falhas, TODOS os slots ficavam com lock stale de processos mortos
- Resultado: Xvfb SEMPRE falhava mesmo estando instalado, Chrome caía para headless, DuckDuckGo bloqueava com anti-bot (exit 6)
- Correção: `is_lock_stale()` lê PID do lock, verifica via `/proc/{pid}`, remove lock e socket stale antes de reusar slot

### Corrigido (GAP-WS-090 — flag --num completamente ignorada quando Chrome headed é usado)
- `--num 1`, `--num 3`, `--num 5` todos retornavam 10 resultados (uma página DDG completa)
- Chrome primary extraía TODOS os resultados da página HTML sem truncar pelo `--num` pedido
- Correção: truncar `agregado.results` para `min(num, len)` ANTES de computar `quantidade_resultados`

### Corrigido (GAP-WS-091 — skill documenta --region mas flag real é --country/-c)
- Skill documentava `--region <CÓDIGO>` como flag válida
- Flag real na CLI era `--country`/`-c` (default `br`)
- Correção: adicionado `alias = "region"` ao arg `--country` no Clap

### Corrigido (GAP-WS-092 — skill documenta .metadados.quantidade_resultados mas campo estava só no nível raiz)
- Skill documentava campo em `.metadados.quantidade_resultados`
- Campo existia APENAS em `.quantidade_resultados` (raiz do `SearchOutput`)
- Correção: campo compat `result_count_compat` adicionado a `SearchMetadata`, populado via `fill_compat_fields()` antes da emissão

### Corrigido (GAP-WS-093 — skill documenta .metadados.endpoint_usado mas campo estava só no nível raiz)
- Skill documentava campo em `.metadados.endpoint_usado`
- Campo existia APENAS em `.endpoint` (raiz do `SearchOutput`)
- Correção: campo compat `endpoint_used_compat` adicionado a `SearchMetadata`, populado via `fill_compat_fields()` antes da emissão

### Corrigido (GAP-WS-094 — --num ignorado no path batch/paralelo)
- `execute_query_with_cancellation()` no path batch NÃO truncava resultados pelo `--num`
- Fix do GAP-WS-090 cobria APENAS `execute_single_search()` (path single-query)
- `--num 2` com `--queries-file` retornava 10 resultados por busca
- Correção: truncar `agregado.results` para `min(num, len)` antes de computar `quantidade` em `execute_query_with_cancellation()`

### Corrigido (GAP-WS-095 — identidade_usada null quando Chrome headed é usado com Auto)
- `identidade_usada` retornava `null` em buscas Chrome headed com `identity_profile = Auto`
- `effective_identity_tag` era `None` porque `browser_profile_for_cli_identity(Auto)` retorna `None`
- Chrome selecionava UA do pool via `chrome_only_ua_for_platform()` mas NÃO propagava a tag de volta
- Correção: após Chrome headed ter sucesso, buscar identidade correspondente no pool pela UA e popular `effective_identity_tag`

### Descartado (GAP-WS-096 — skill documenta --allow-lite-fallback DEVE vir ANTES de deep-research mas Clap aceita em ambas posições)
- Skill documenta: "flag --allow-lite-fallback DEVE vir ANTES do subcomando deep-research — Clap rejeita após o subcomando com exit 2"
- Comportamento real: Clap aceita `--allow-lite-fallback` em AMBAS as posições sem erro
- NÃO é bug da CLI — a skill documenta restrição que não existe. CLI está correta

### Corrigido (GAP-WS-097 — skill documenta .metadados.nivel_cascata mas campo não era populado)
- Campo `cascade_level` (serializado como `nivel_cascata`) existia no struct mas era SEMPRE `None` e omitido por `skip_serializing_if`
- Campo real com valor era `cascade_level_observed` (serializado como `cascata_nivel_observado`)
- Correção: `fill_compat_fields()` agora popula `cascade_level` com o valor de `cascade_level_observed`

### Descartado (GAP-WS-098 — --fetch-content sem --max-content-length)
- Investigação revelou que `--fetch-content` SEM `--max-content-length` funciona com default interno de 4096
- Casos de `conteudo: null` são falhas individuais de fetch para URLs específicas (timeout, bloqueio, etc.)
- NÃO é bug da CLI — é comportamento esperado quando reqwest não consegue acessar a URL
- A skill documenta como PROIBIDO por razões de boas práticas (memória ilimitada), NÃO porque a CLI falha

### Corrigido (GAP-WS-099 — ZeroResultsSuspeito não produzia exit code 6)
- Enum `ZeroCause` tem 6 variantes: `Legitimo`, `FiltroSilencioso`, `GhostBlock`, `AntiBot`, `RespostaInvalida`, `ZeroResultsSuspeito`
- O match de exit code 6 em `lib.rs` cobria APENAS 4 variantes — faltava `ZeroResultsSuspeito`
- Quando classificador retornava `ZeroResultsSuspeito`, `zero_cause_non_legitimo` era `false` e exit code caía para 5 em vez de 6
- Correção: adicionado `ZeroResultsSuspeito` ao match arm em AMBOS branches (Single e Multi)

### Corrigido (GAP-WS-100 — tamanho_conteudo reportava tamanho do body HTML original em vez do texto truncado)
- `content_size` era setado com `size_original` (bytes do body HTML bruto retornado por `extract_http_content()`)
- O texto extraído era truncado via `apply_readability(html, max_size)` mas `tamanho_conteudo` ignorava a truncagem
- Resultado: `--max-content-length 500` retornava `tamanho_conteudo: 18594` quando o campo `conteudo` tinha ~494 chars
- Correção: usar `text.len()` para `content_size` em vez de `size_original`

### Descartado (GAP-WS-101 — skill documenta --region br-pt mas flag espera country code simples)
- Skill documenta: `--region br-pt` como exemplo de uso
- Comportamento real: `--country`/`--region` recebe APENAS o country code (`br`, `us`, `uk`)
- `format_kl(lang, country)` concatena `country-lang`, então `--region br-pt` gera `br-pt-pt` (duplicado)
- NÃO é bug da CLI — `format_kl` funciona corretamente. É gap de documentação da skill (CLAUDE.md), que está PROIBIDO de alterar

### Corrigido (GAP-WS-102 — deep-research nivel_cascata SEMPRE null)
- `cascade_level` no `DeepResearchMetadata` era derivado de `o.metadata.cascade_level` (campo compat)
- `fill_compat_fields()` popula `cascade_level` a partir de `cascade_level_observed` — mas roda APÓS o pipeline retornar
- Durante execução do deep-research, `cascade_level` ainda era `None` em todos os sub-query outputs
- Correção: ler de `cascade_level_observed` (campo real) em vez de `cascade_level` (campo compat)

### Corrigido (GAP-WS-103 — exit code 6 SUSPECTED_BLOCK ausente no --help)
- Seção EXIT CODES do `--help` listava apenas exit codes 0-5
- Exit code 6 (`SUSPECTED_BLOCK`) era emitido pela CLI mas NÃO documentado no help
- Operadores usando `--help` como referência NÃO sabiam que exit 6 existia
- Correção: adicionada linha `6    Suspected block (zero results with non-legitimate causa_zero)` ao after_long_help

### Validação
- `cargo build` — ZERO erros
- `cargo clippy` — ZERO warnings
- `cargo fmt --check` — ZERO diferenças
- `cargo test` — ZERO falhas
- Chrome headed via Xvfb funciona após limpeza de lock stale (GAP-WS-089)
- `--num N` respeitado nos paths single, batch e deep-research (GAP-WS-090, 094)
- Alias `--region` funciona conforme documentado (GAP-WS-091)
- Campos compat `.metadados.quantidade_resultados`, `.metadados.endpoint_usado`, `.metadados.nivel_cascata` presentes (GAP-WS-092, 093, 097)
- `identidade_usada` populado para Chrome headed Auto (GAP-WS-095)
- `ZeroResultsSuspeito` emite exit 6 (GAP-WS-099)
- `tamanho_conteudo` reflete tamanho real do conteúdo (GAP-WS-100)
- Deep-research `nivel_cascata` populado (GAP-WS-102)
- `--help` lista exit codes 0-6 (GAP-WS-103)


## [0.8.7] - 2026-06-23

### Corrigido (GAP-WS-072 — código ignora display nativo ($DISPLAY/$WAYLAND_DISPLAY))
- Linux desktop com GNOME/KDE caía em modo headless mesmo com display ativo
- macOS e Windows SEMPRE caíam em headless (Cloudflare detectava, 0 resultados)
- Adicionada `has_native_display()` que detecta display nativo por plataforma
- macOS/Windows retornam `true` (Quartz/DWM sempre ativos), Linux checa `$DISPLAY`/`$WAYLAND_DISPLAY`

### Corrigido (GAP-WS-073 — Chrome headed mostra janela visível ao usuário)
- Quando Chrome rodava headed, a janela aparecia na tela do usuário
- `--window-position=-32000,-32000` NÃO funciona no GNOME/Mutter (clamp para dentro da tela)
- Correção: spawnar Xvfb privado mesmo com display nativo — Chrome headed em display virtual isolado
- Se Xvfb indisponível: fallback para headless (invisível) com mensagem de instrução

### Corrigido (GAP-WS-074 — UA Safari/Firefox enviado com TLS fingerprint Chromium)
- Pool de identidades podia selecionar UA Safari ou Firefox para busca Chrome
- Cloudflare cruza JA3/JA4 TLS fingerprint (Chromium) contra UA (Safari) — mismatch detectado
- Adicionado filtro `chrome_only_ua_for_platform()`: APENAS UA Chrome com browser Chromium

### Corrigido (GAP-WS-075 — flags de launch do Chrome aumentam bot score)
- Faltavam flags anti-detecção: `--disable-features=AutomationControlled,TranslateUI`, `--disable-infobars`
- `--disable-extensions` era flag suspeita que aumentava bot score — REMOVIDO

### Corrigido (GAP-WS-076 — stealth scripts incompletos contra Cloudflare 2026)
- `navigator.webdriver` setado para `false` em vez de `undefined` (Chrome real tem `undefined`)
- Adicionados: filtro de stack trace CDP, Permissions API (clipboard, geolocation), prevenção de leak CDP via WebSocket

### Corrigido (GAP-WS-077 — Chrome navega direto para URL de busca sem warm-up)
- Chrome navegava direto para a URL de busca sem visitar duckduckgo.com antes
- Cloudflare resolve o JS challenge na primeira visita e seta cookies
- Correção: navegar para duckduckgo.com ANTES da URL de busca com delay pseudo-aleatório (800-1500ms)

### Corrigido (GAP-WS-078 — CLI não detecta SO nem auto-instala dependências (Xvfb))
- Adicionada `detect_linux_distro()` lendo campo ID de `/etc/os-release`
- Adicionada `detect_linux_variant()` detectando distros imutáveis (Silverblue, Kinoite, NixOS, Guix, ostree)
- Adicionada `try_auto_install_xvfb()` usando `sudo -n` (não-interativo)
- Suporta 22+ distros: Fedora, RHEL, CentOS, Rocky, AlmaLinux, Ubuntu, Debian, Mint, Pop, Zorin, Elementary, Kali, Arch, Manjaro, EndeavourOS, Garuda, openSUSE, SLES, Alpine, Amazon Linux, Void, Gentoo
- Distros imutáveis: pula auto-install e mostra comando manual

### Corrigido (GAP-WS-079 — auto-install Xvfb não chamado no branch de display nativo)
- `try_auto_install_xvfb()` só era chamado no branch sem display (servidor)
- No branch com display (desktop), quando `spawn_virtual_display()` falhava, caía para headed nativo (janela visível)
- Correção: chamar `try_auto_install_xvfb()` + re-tentar `spawn_virtual_display()` TAMBÉM no branch de display nativo

### Corrigido (GAP-WS-080 — Stdio::null() escondia output do gerenciador de pacotes)
- Auto-install usava `.stdout(Stdio::null()).stderr(Stdio::null())` — usuário não via NADA durante instalação
- Correção: trocado para `.stdout(Stdio::inherit()).stderr(Stdio::inherit())` — output em tempo real

### Corrigido (GAP-WS-081 — mensagens de falha visíveis apenas via tracing (invisíveis com -q))
- Todas as mensagens de sucesso/falha do auto-install iam apenas para `tracing::warn`/`info`
- Com `-q` (quiet), usuário não via NADA sobre resultado da instalação
- Correção: adicionado `eprintln!` com cores ANSI para TODOS os estados

### Corrigido (GAP-WS-082 — instruções de install manual apenas no branch de display nativo)
- O `eprintln` com instruções de instalação existia APENAS no branch `has_native_display()`
- No branch de servidor (sem display), quando Xvfb falhava, caía silenciosamente para headless
- Correção: adicionadas instruções em ambos os branches

### Corrigido (GAP-WS-083 — distros faltando nas instruções manuais de instalação)
- Texto hardcoded de instruções cobria apenas Fedora, Ubuntu, Arch
- Faltavam: Alpine, Void, Gentoo, Amazon Linux, NixOS, Guix, openSUSE, SLES e derivadas
- Criada `xvfb_manual_instruction()` com match para 22+ distros

### Corrigido (GAP-WS-084 — inconsistência apt vs apt-get entre código e instruções)
- Código de auto-install usava `apt-get` mas mensagem de instrução manual dizia `apt`
- `apt-get` é o binário correto para scripts (`apt` é wrapper interativo)
- Padronizado em `apt-get` tanto no código quanto nas instruções

### Corrigido (GAP-WS-085 — nenhuma mensagem exibida ANTES da tentativa de auto-install)
- `sudo -n` era executado sem nenhum output prévio ao usuário
- Correção: adicionado `eprintln!` mostrando que Xvfb não foi encontrado e auto-install será tentado
- Mostra o comando exato (ex: `sudo dnf install -y xorg-x11-server-Xvfb`)

### Corrigido (GAP-WS-086 — leitura redundante de /etc/os-release em detect_linux_variant())
- `detect_linux_variant()` chamava `detect_linux_distro()` internamente, relendo `/etc/os-release`
- Eliminada dependência circular — agora busca diretamente por `\nid=nixos` e `\nid=guix`

### Corrigido (GAP-WS-087 — AggregatedItem.title serializa como "title" em vez de "titulo")
- Busca normal serializa campo como `"titulo"` via `SearchResult` com serde rename
- Deep-research usa `AggregatedItem` que NÃO tinha serde rename — schema inconsistente
- Correção: adicionado `#[serde(rename = "titulo")]` no campo title de `AggregatedItem`

### Corrigido (GAP-WS-088 — DeepResearchOutput sem campo query top-level)
- `SearchOutput` tem campo `.query` no top-level do envelope JSON
- `DeepResearchOutput` NÃO tinha `.query` — só `metadados.query_original`
- Consumidores que usam `.query` uniformemente recebiam null no deep-research
- Correção: adicionado campo `pub query: String` populado com `args.query.clone()`


## [0.8.6] - 2026-06-22

### Alterado (GAP-WS-066 — cargo install falha no Windows — btls-sys exige NASM+CMake)
- BREAKING BUILD: substituiu `wreq` (BoringSSL) por `reqwest` + `rustls-tls` (TLS puro em Rust)
- Elimina 4 pré-requisitos de build no Windows: NASM, CMake, Perl, MSVC cl.exe
- `cargo install duckduckgo-search-cli` agora funciona no Windows apenas com o toolchain Rust
- Crates removidos: `wreq`, `wreq-util`, `brotli`, `brotli-decompressor`, `alloc-no-stdlib`
- Preflights removidos do build.rs: `nasm_in_path`, `cmake_in_path`, `cl_in_path`, `perl_in_path`
- Renomeado `src/wreq_cookie_adapter.rs` para `src/cookie_adapter.rs`
- Persistência de cookies reescrita: usa `reqwest::cookie::Jar` + extração de header via `CookieStore::cookies()`
- Descompressão brotli removida (DuckDuckGo nunca serve brotli para endpoints HTML)
- Fallback HTTP perde emulação de fingerprint TLS via BoringSSL (Chrome headed é primário desde v0.8.0)
- ADR-0001 (wreq/BoringSSL) substituída pela ADR-0008 (reqwest/rustls)
- Stack TLS unificada: `rustls` em todos os componentes (chromiumoxide + reqwest)

### Corrigido (GAP-WS-067 — `--num 0` aceito sem validação)
- `--num 0` era aceito silenciosamente, produzindo busca que nunca retornaria resultados úteis
- Adicionado `value_parser(clap::value_parser!(u32).range(1..))` para rejeitar zero no parsing de argumentos

### Corrigido (GAP-WS-068 — docs dizem `--synth-format plain` mas clap espera `plain-text`)
- 4 arquivos de documentação declaravam `plain` como valor válido para `--synth-format`
- O derive `ValueEnum` do clap converte `PlainText` para `plain-text` (kebab-case)
- Corrigido `plain` para `plain-text` em AGENTS.md, AGENTS.pt-BR.md, HOW_TO_USE.md, HOW_TO_USE.pt-BR.md

### Corrigido (GAP-WS-069 — doc comment em decompress.rs menciona 'wreq' sem contexto de migração)
- `src/decompress.rs:39` dizia "brotli removed in v0.8.6 with wreq" — clarificado para mencionar a migração wreq-to-reqwest

### Corrigido (GAP-WS-070 — 4 receitas em MIGRATION.md com flags globais após subcomando)
- 4 receitas de deep-research em MIGRATION.md e MIGRATION.pt-BR.md tinham `-q -f json` DEPOIS do subcomando
- clap exige flags globais ANTES do subcomando — receitas causavam `unexpected argument '-q'`
- Flags reordenadas para aparecer antes de `deep-research` em todas as 4 receitas

### Documentação (GAP-WS-071 — 10+ docs descreviam wreq/BoringSSL como stack TLS atual)
- 13 arquivos de documentação atualizados para refletir reqwest+rustls-tls como stack TLS atual
- Referências históricas de wreq em seções de changelog v0.7.x preservadas com notas de contexto
- Afetados: README, SECURITY, INVERSIONS, CONTRIBUTING, HOW_TO_USE, AGENTS, ADR-0002/0005/0007


## [0.8.5] - 2026-06-21

### Corrigido (GAP-WS-065 — Chrome headless detectado pelo Cloudflare — 0 resultados)
- Regressão CRÍTICA: `--headless=new` do Chrome é detectável pelo anti-bot do Cloudflare
- Todas as queries retornavam 0 resultados com interstitial `anomaly-modal` desde a v0.8.1
- Causa raiz: o fix do GAP-WS-060 mudou Chrome de headed para headless por padrão
- Cloudflare identifica headless Chrome via sinais JS (`navigator.webdriver`, protocolo CDP, ausência de plugins)
- Correção: criar display virtual Xvfb privado automaticamente, rodar Chrome headed dentro dele
- Chrome roda headed (passa anti-bot) mas o usuário NÃO vê nenhuma janela
- `builder.env("DISPLAY", ":99")` passa o display virtual APENAS ao processo filho do Chrome
- Limpeza do Xvfb é automática via `Drop` do `ChromeBrowser`
- Fallback: se Xvfb não disponível, cai para headless (com risco de anti-bot)
- Nova env var: `DUCKDUCKGO_CHROME_HEADLESS=1` para forçar modo headless


## [0.8.4] - 2026-06-21

### Corrigido (GAP-WS-064 — `cascade_level_observed` sempre `null` no path paralelo)
- Batch queries e sub-queries do deep-research nunca reportavam `cascata_nivel_observado` nos metadados JSON
- Causa raiz: `cascade_level_observed: None` hardcoded no success path de `search_one_query` em `parallel.rs`
- Correção: reutilizar `derive_cascade_level_from_attempts` de `pipeline.rs` (agora `pub(crate)`)
- Queries únicas via `pipeline.rs` já estavam corretas — este fix traz paridade


## [0.8.3] - 2026-06-21

### Corrigido (GAP-WS-062 — metadata `chrome_attempted` incorreta no path paralelo)
- `parallel.rs` reportava `tentou_chrome: true` mesmo com `DUCKDUCKGO_SEARCH_CLI_NO_CHROME=1` desabilitando Chrome em runtime
- Causa raiz: `chrome_attempted = cfg!(feature = "chrome")` é constante de compilação, sempre `true` quando a feature chrome está habilitada
- Correção: check runtime agora inclui env var `NO_CHROME` — `cfg!(feature = "chrome") && NO_CHROME != "1"`
- Afeta batch queries (`--queries-file`) e sub-queries do deep-research
- `pipeline.rs` (queries únicas) já estava correto — este fix traz paridade entre ambos os paths

### Corrigido (GAP-WS-063 — `identity_used` sempre `null` no path de sucesso do paralelo)
- Batch queries via `--queries-file` nunca reportavam `identidade_usada` nos metadados JSON, mesmo com `--identity-profile chrome-linux`
- Causa raiz: `identity_used: None` hardcoded no success path e early-return de `search_one_query`
- Correção: chamar `identity_tag_for_cli_identity(config.identity_profile, None)` em ambos os paths
- Queries únicas via `pipeline.rs` já estavam corretas — este fix traz paridade


## [0.8.2] - 2026-06-21

### Corrigido (GAP-WS-061 — deep-research ignora flags de busca do root)
- `execute_deep_research` agora herda todas as flags de busca do root CLI em vez de usar defaults hardcoded
- `--num`, `--lang`, `--country`, `--endpoint`, `--retries`, `--proxy`, `--timeout`, `--parallel`, `--max-content-length`, `--identity-profile` são respeitados quando posicionados ANTES do subcomando `deep-research`
- Anteriormente hardcoded: `num_results=10`, `language="en"`, `country="us"`, `retries=2`, `proxy=None`
- Agora herda valores do usuário: `--num 5 --lang pt --country br deep-research "query"` funciona como esperado
- `--allow-lite-fallback`, `--pre-flight`, `--identity-profile` propagados do root args
- Removida chamada morta `initialize_logging(0, false, false)` (subscriber já inicializado antes do dispatch de subcomando)


## [0.8.1] - 2026-06-21

### Corrigido (GAP-WS-060 — Chrome abre janela visível em desktops)
- Chrome agora roda em modo headless (`--headless=new`) por PADRÃO em todas as plataformas
- Anteriormente, Chrome abria janela GUI visível em qualquer desktop com `$DISPLAY` setado (Linux, macOS, Windows)
- `DUCKDUCKGO_CHROME_VISIBLE=1` habilita modo headed para depuração
- `DUCKDUCKGO_CHROME_XVFB=1` habilita modo headed via xvfb-run para evasão anti-bot em servidores headless
- ZERO janelas Chrome visíveis durante execução normal da CLI
- Função `which_xvfb_run()` renomeada para `is_xvfb_requested()` com semântica correta


## [0.8.0] - 2026-06-19

### Corrigido (GAP-AUD-003 — classificação causal de zero-result)
- **CR1 — `total == 0` mapeava direto para exit 5 sem inspeção causal**. `src/lib.rs:241-243` agora distingue 5 causas semanticamente diferentes (`Legitimo`, `FiltroSilencioso`, `GhostBlock`, `AntiBot`, `RespostaInvalida`) e emite exit 6 (`SUSPECTED_BLOCK`) quando a causa não é legítima. Exit 5 preservado para zero genuíno.
- **CR2 — `pre_flight_blocked` agora roda em adição ao classificador causal**. O ramo legacy continua emitindo exit 3, mas o novo classificador captura casos onde pre-flight estava desligado (default).
- **CR3 — `--pre-flight` continua opt-in para preservar BC**. Mas o classificador roda automaticamente quando `quantidade_resultados == 0`, então o operador padrão agora se beneficia sem precisar aprender sobre a flag.
- **CR5 — `SearchMetadata.pre_flight_fired` permanece `bool` por BC**. Mas agora coexiste com `causa_zero` que captura nuances causais.
- **CR6 — `causa_zero` adicionado ao envelope JSON**. Campo `metadados.causa_zero: Option<ZeroCause>` serializa como kebab-case (`"anti-bot"`, `"ghost-block"`, etc.).

### Corrigido (Bug #1 — descompressão de resposta HTTP)
- **`wreq 6.0.0-rc` envia `accept-encoding: gzip, deflate, br` mas não descomprime automaticamente**. O body retornado por `Response::text()` / `Response::bytes()` chegava como bytes gzip-comprimidos (≈9.2 KB binários em vez de ≈14 KB de texto plano — taxa 65% consistente com gzip level-6 default). `detectar_interstitial_com_match` realizava `body.contains("anomaly-modal")` em bytes binários e falhava silenciosamente, fazendo o classificador rotular `Legitimo` em ambiente comprovadamente bloqueado pelo Cloudflare.
- **Novo módulo `src/decompress.rs`** inspeciona `Content-Encoding` e despacha para `flate2::read::MultiGzDecoder` (gzip), `flate2::read::ZlibDecoder` (deflate) ou `brotli_decompressor::Decompressor` (br). `MultiGzDecoder` lida com streams gzip concatenados transparentemente.
- **7 call sites substituídos** (`src/search.rs:403`, `src/search.rs:776`, `src/lib.rs:637`, `src/pipeline.rs:311`, `src/content.rs:180`): todas as chamadas de `response.text().await` agora passam pelo decompressor antes de virar `String`.
- **`tokio::task::spawn_blocking`** envolve o decode sync para evitar bloquear o reactor do tokio em payloads grandes.
- **`DECOMPRESSION_MAX_OUTPUT = 32 MiB`** como cap de segurança contra gzip bombs via `Read::take(cap + 1)` que aborta a descompressão quando o stream excede.
- **3 variantes em `CliError`**: `PayloadTooLarge { max, actual }`, `UnsupportedEncoding(String)`, `InvalidUtf8(FromUtf8Error)`. Saída JSON mantém `error: "http_error"` para BC.
- **`flate2 = "1"` adicionado a `Cargo.toml`** — já estava transitivo via `wreq` features `gzip`+`deflate`, declarado explícito para visibilidade estável.

### Corrigido (Bug #2 — BC opt-out documentado como drift de semver)
- `DUCKDUCKGO_ZERO_CAUSE_STRICT=false` afeta SOMENTE o exit code (mapeia 6 → 5 legacy), mas o campo `causa_zero` permanece publicado no envelope JSON. A política `#[serde(skip_serializing_if = "Option::is_none")]` garante que clientes v0.7.x que NUNCA rodam classificador não veem mudança alguma. Clientes que rodam contra ambiente bloqueado com opt-out ativo recebem `causa_zero` mesmo pedindo exit 5 legacy — isso é informação diagnóstica aditiva, alinhada com o padrão de mudanças additive-skip_serializing_if usado em v0.6.4 (`identidade_usada`) e v0.7.9 (`pre_flight_fired`). Documentado na seção Guia de Migração abaixo.

### Adicionado
- **`ZeroCause` enum em `src/types.rs`** com 5 variantes marcadas `#[non_exhaustive]` para forward compat. Serializa como kebab-case.
- **`docs/decisions/0006-stealth-shell-classification-v0-8-0.md`** (GAP-NEW-003 / GAP-NEW-008) — ADR documentando a decisão arquitetural do branch CR4b (4 condições simultâneas: body_len >= 4000 + !result__a + InterstitialKind::None + assinatura DDG). Inclui alternatives considered (threshold dinâmico, ML classifier, marker probing) e validation (proptest com 64 cases).
- **`pipeline::classify_zero_result`** — classificador puro, sem I/O, com chain causal documentada em `docs/decisions/0004-zero-cause-classification-v0-8-0.md`.
- **`pipeline::sugestao_proxima_acao_para_zero`** — strings PT-BR determinísticas por variante, alinhadas ao padrão `sugestao_mitigacao_com_marker`.
- **`SearchMetadata.zero_cause: Option<ZeroCause>`** + **`SearchMetadata.sugestao_proxima_acao: Option<String>`** no envelope JSON.
- **`MultiSearchOutput.causa_zero_histogram: BTreeMap<String, u32>`** agregado automaticamente em multi-query; BTreeMap garante ordem lexicográfica determinística.
- **`AggregatedSearchResult.first_body: String`** exposto para o classificador distinguir ghost-block de zero genuíno.
- **`DUCKDUCKGO_ZERO_CAUSE_STRICT` env var** para BC opt-out (default ON; aceita `false`/`0`/`no`/`off`).
- **`exit_codes::SUSPECTED_BLOCK: i32 = 6`** adicionado à tabela de exit codes.
- **`docs/decisions/0004-zero-cause-classification-v0-8-0.md`** — ADR documentando a decisão arquitetural e a chain causal patch→efeito.
- **12 unit tests em `src/pipeline.rs`** cobrindo todas as 5 variantes do enum + mensagens de sugestão.
- **`assert_eq!(SUSPECTED_BLOCK, 6)` em `src/error.rs`** — teste stale atualizado.

### Mudado
- `Cargo.toml` bumped 0.7.10 → 0.8.0
- `Cargo.lock` regenerated
- **`src/ddg_class_watch.rs` movido para `examples/ddg_class_watch.rs`** (GAP-OPS-002). Módulo era declarado em `lib.rs:56` mas sem call sites em produção ou testes; agora vive como exemplo invocável via `cargo run --example ddg_class_watch`. `fn main()` adicionada com HTML de demonstração que imprime relatório e sai com código 1 quando detecta classes novas (sinal de alerta para bump de `RESULT_PAGE_SELECTORS` em `src/probe_deep.rs`). Referência histórica em CHANGELOG [0.7.10] P19 preservada — módulo ESTAVA em `src/` em v0.7.10.
- Tabela de exit codes agora congelada como semver-additive (0-5 estáveis, 6+ adicionados sem reassign).
- `gaps.md` GAP-AUD-003 marcado como `RESOLVIDO em v0.8.0`.

### Guia de Migração v0.7.x → v0.8.0

**Exit code 6 é aditivo, não substitui exit 5.** Clientes que ramificam em `exit 5` podem continuar funcionando sem mudanças via BC opt-out:

```bash
# Restaura comportamento v0.7.x: exit 5 sempre para total == 0
export DUCKDUCKGO_ZERO_CAUSE_STRICT=false

# Default v0.8.0: exit 6 quando causa_zero é não-legítimo
duckduckgo-search-cli "blocked query" -f json
```

**Campo `metadados.causa_zero` é aditivo diagnóstico.** Clientes que parseiam JSON devem tratá-lo como `Option<String>` (pode estar ausente). Valores possíveis: `"legitimo"`, `"filtro-silencioso"`, `"ghost-block"`, `"anti-bot"`, `"resposta-invalida"`.

**Mesmo sob `DUCKDUCKGO_ZERO_CAUSE_STRICT=false`, o JSON mantém `causa_zero`.** Isso é informação diagnóstica útil — exit code legacy, envelope novo. Documentado em `src/error.rs:60-66`.

**Resposta HTTP agora é descomprimida transparentemente.** Quem intercepta bytes brutos do socket (proxy, mitm) verá headers `Content-Encoding` mas o body entregue ao código de aplicação está sempre em texto plano.

### Validação
- `cargo build --release --offline` build OK
- `cargo clippy --all-targets --offline -- -D warnings` zero warnings
- `cargo test --offline` 378 testes passando, 0 falhando
- E2E wiremock gzip-encoded: exit 6 + `causa_zero: "anti-bot"` + sugestão acionável
- E2E `DUCKDUCKGO_ZERO_CAUSE_STRICT=false`: exit 5 + `causa_zero: "anti-bot"` no JSON (drift documentado)
- E2E Chrome-primary: 10 resultados via Chrome, `usou_chrome: true`, exit 0
- E2E deep-research: 38 resultados únicos, 20 referências na síntese, exit 0
- Tabela de exit codes agora congelada como semver-additive (0-5 estáveis, 6+ adicionados sem reassign)
- `gaps.md` GAP-AUD-003, GAP-NEW-005, GAP-NEW-006, GAP-NEW-007 marcados como `RESOLVIDO em v0.8.0`

### Adicionado (Chrome headed como transporte PRIMÁRIO de busca — GAP-NEW-005, GAP-NEW-006, GAP-NEW-007)
- Chrome headed via `xvfb-run` é agora o transporte PRIMÁRIO de busca
- `src/browser.rs:46` — constante `STEALTH_SCRIPTS` com 17 sinais JavaScript stealth
- Spoofing de fingerprint Canvas, WebGL, AudioContext via injeção CDP
- `navigator.webdriver` configurado como `false` antes da navegação
- Spoofing de `navigator.plugins`, `navigator.languages`, objeto `chrome`
- `navigator.connection`, `navigator.maxTouchPoints` com valores realistas
- `which_xvfb_run()` detecta automaticamente o binário `xvfb-run` no Linux
- `ChromeBrowser::launch()` usa modo headed quando `DISPLAY` ou `xvfb-run` disponível
- Modo headless é FALLBACK quando display e xvfb-run estão indisponíveis
- `execute_chrome_search()` em `src/pipeline.rs` — pipeline de busca Chrome-first
- `execute_chrome_search_pub()` wrapper público para deep-research em `src/parallel.rs`
- `parallel.rs` deep-research usa pipeline Chrome via `execute_chrome_search_pub()`
- wreq permanece APENAS para `--fetch-content` e requisições `--probe` HTTP
- Emulação TLS do wreq via `Emulation::Chrome136` em `src/http.rs` (fecha GAP-NEW-005)
- Campo `SearchMetadata.tentou_chrome` adicionado (serializado como `tentou_chrome`)
- `SearchMetadata.usou_chrome` agora `true` quando Chrome-primary tem sucesso
- Inicialização de tracing movida para ANTES do dispatch de subcomando em `src/lib.rs`
- Flag `-q` do deep-research agora silencia corretamente a saída de tracing

### Pré-requisitos (v0.8.0)
- Linux: `sudo apt install xvfb` (Debian/Ubuntu) ou `sudo dnf install xorg-x11-server-Xvfb` (Fedora)
- Linux: Google Chrome ou Chromium deve estar instalado
- macOS: Chrome deve estar instalado; xvfb não necessário (display nativo)
- Windows: Chrome deve estar instalado; xvfb não necessário (display nativo)
- Feature `chrome` habilitada por padrão no `Cargo.toml`
- Para compilar sem Chrome: `cargo build --no-default-features`

### Validação (Chrome-primary)
- 378 testes unitários passando, 0 warnings do clippy
- E2E busca simples: 10 resultados via Chrome, `usou_chrome: true`, exit 0
- E2E deep-research: 38 resultados únicos, 20 referências na síntese, exit 0
- ZERO bloqueios do Cloudflare com Chrome headed + 17 sinais stealth

## [0.7.10] - 2026-06-17

### Corrigido (UX anti-bot + observabilidade + endurecimento e2e + 5 correções de bug)
- **GAP-WS-60 (CRÍTICO, propagação do pino de identidade) — `--identity-profile` agora propaga a identidade selecionada para `failure_output` (`src/pipeline.rs`) e `error_output` (`src/parallel.rs`) via novo helper `identity_tag_for_cli_identity` em `src/identity.rs`**. Antes da correção, o pino de identidade (`identidade_usada`) só aparecia no caminho de SUCESSO; em falha, era sempre `null`. Consumidores agora conseguem correlacionar uma falha a uma identidade específica do pool de 12.
- **GAP-AUD-001 (auditoria local) — pino `identidade_usada` agora presente em failure paths** (era sempre `null`). Mesmo helper canônico `IdentityProfile::tag()` é reusado em sucesso e falha, garantindo paridade de formato `<family>-<platform>-<seed16hex>` (ex.: `chrome-linux-33333333cccc0003`).
- **GAP-AUD-002 (auditoria local) — `cargo bench --bench pre_f_light_latency` agora roda Criterion corretamente** após adicionar `[[bench]] harness = false` em `Cargo.toml`. Antes da correção, o harness padrão reportava `running 0 tests` em vez de rodar os 5 cenários de benchmark, dando falsa impressão de "sem regressão".
- **B1 (CRÍTICO) — `--pre-flight` não emite mais dois objetos JSON concatenados no stdout**. `src/pipeline.rs:297` chamava `print_line_stdout` direto, depois retornava um `SearchOutput` que o caller em `src/lib.rs` serializava de novo via `emit_result`. Consumidores com `| jaq '.resultados'` quebravam. Removido o print antecipado; o `SearchOutput` carrega o contexto do pre-flight no envelope e o caller serializa exatamente uma vez.
- **B2 (CRÍTICO) — `pre_flight_blocked` retorna exit 3 (era 0)**. A tabela `EXIT CODES` do `--help` promete exit 3 para "DuckDuckGo 202 block anomaly", mas o caminho de pre-flight caía no `Ok(output)` que retornava `SUCCESS`. `src/lib.rs` agora detecta `output.error == Some("pre_flight_blocked")` e retorna `exit_codes::RATE_LIMITED_OR_BLOCKED` (3) antes da serialização.
- **B3 (MÉDIO) — `--global-timeout` agora é global e aceito em subcomandos**. A flag vivia em `CliArgs` (sub-árvore) sem `global = true`, então `duckduckgo-search-cli deep-research --global-timeout 30 query` falhava com `error: unexpected argument '--global-timeout' found`. Movida para `RootArgs` com `#[arg(global = true)]`; `lib.rs` hoista o valor via `root_global_timeout_seconds` e propaga para o subcomando `deep-research`.
- **B4 (CRÍTICO) — `--probe-deep` standalone agora retorna exit 3 quando detecta captcha**. O probe reportava `status: "captcha"` no JSON mas o CLI retornava exit 0. Agora, quando `InterstitialKind != None`, retorna `exit_codes::RATE_LIMITED_OR_BLOCKED` (3). Permite ramificar no exit code em vez de parsear o JSON.
- **B5 (FALSO POSITIVO confirmado) — `--require-results` funciona corretamente**. Teste inicial mostrou exit 0 porque `user-agents.toml` e `selectors.toml` ainda não existiam; após `init-config` o caminho retorna exit 4 (`GLOBAL_TIMEOUT`) corretamente. Sem mudança necessária.

### Adicionado
- **v0.7.9 P1-P7 — `detectar_interstitial_com_match` retorna `(&'static str, InterstitialKind)` com marker literal**. Helper novo em `src/probe_deep.rs` que permite distinguir qual marker Cloudflare/DDG foi detectado (vs. detecção heurística de ghost-block).
- **v0.7.9 P4b — `sugestao_mitigacao_com_marker` retorna string com marker literal**. Helper novo que injeta o marker real (ex.: `cf-challenge`, `anomaly-modal`) na mensagem de mitigação em vez de "ghost-block" genérico. Versão original marcada com `#[deprecated(since = "0.7.10")]`.
- **v0.7.9 P3 — `SearchMetadata.pre_flight_fired: bool` adicionado ao envelope**. Quando `cfg.pre_flight == true && ghost-block`, o campo fica `true`. Permite consumidores distinguirem busca normal de busca com pre-flight acionado.
- **v0.7.9 P5 — `--allow-lite-fallback` e `--pre-flight` viraram `global = true`**. Ambas as flags são aceitas antes e depois de subcomandos como `deep-research`. Fechou GAP-WS-58/59 com zero regressões.
- **v0.7.10 P5 — scheduler de probe-deep integrado em `execute_single_search`**. Quando `cfg.pre_flight == true`, o pipeline roda um probe mínimo antes da busca real e aborta em captcha/ghost-block.
- **v0.7.10 P6/P17 — `insta = "1"` adicionado e teste de snapshot para os 8 marcadores Cloudflare 2026**. Captura regressão se alguém remover string de marker.
- **v0.7.10 P7/P16 — `src/proxy_detection.rs` novo módulo com `ProxyKind::{None, Transparent, Cloudflare, Corporate}`**. Heurística de inspeção de response headers (Vivo Fiber, Gigaweb, Cloudflare) com 8 testes cobrindo ISPs brasileiros.
- **v0.7.10 P4 — `--require-results` em `deep-research`**. Quando setado + fan-out zero, retorna exit 4 (`GLOBAL_TIMEOUT`) com stderr "exiting non-zero".
- **v0.7.10 P9 — `examples/pre_flight.rs`**. Demonstra uso combinado de `--pre-flight` + `--allow-lite-fallback`.
- **v0.7.10 P10 — `docs/decisions/0003-pre-flight-scheduler-v0-7-10.md`**. ADR documentando decisão arquitetural do scheduler.
- **v0.7.10 P14 — `benches/pre_flight_latency.rs` + `BENCHMARKS.md`**. Benchmark Criterion com 5 cenários (baseline / pre-flight limpo / pre-flight bloqueado / com marker / signal false / signal true).
- **v0.7.10 P19 — `src/ddg_class_watch.rs`**. Módulo de monitoramento runtime de templates DDG.
- **v0.7.10 P19 — `scripts/pre-publish-gate.sh`**. 7 gates sequenciais antes de `cargo publish`: fmt, clippy, test, coverage ≥80%, sem refs a v0.7.9 stale, publish dry-run válido, CI main verde.
- **v0.7.10 P19 — `skill/duckduckgo-search-cli-{en,pt}/eval-queries.json` +4 queries (q47-q50)**. Smoke test de `--version 0.7.10`, feature-test de pino, feature-test de pre-flight, feature-test de require-results.

### Mudado
- `Cargo.toml` bumped 0.7.8 → 0.7.10
- `Cargo.lock` regenerado
- `gaps.md` GAP-WS-58 e GAP-WS-59 marcados como `RESOLVIDO`


## [0.7.9] - 2026-06-16

### Corrigido
- **GAP-WS-58 (CRÍTICO, ghost-block) — `detectar_interstitial` agora classifica body sub-4KB sem `result-page-signal` como `InterstitialKind::Cloudflare`**. Limiar conservador de 4KB evita falsos positivos em respostas válidas de baixa densidade. Helper `has_result_page_signal` checa presença de classes DDG (`nrn-react-div`, `react-article`, `module--results`, `js-react-aria-results`).
- **GAP-WS-59 (ALTO, markers 2026) — 5 marcadores Cloudflare novos + 1 marker DDG novo** (detalhes em v0.7.8 também).
- **GAP-WS-59 (ALTO, flag global) — `--allow-lite-fallback` hoisted para `RootArgs` com `global = true`**. Fecha o caminho de "unexpected argument" em deep-research.
- **`Config.pre_flight` adicionado** com default `false` para opt-in.


## [0.7.8] - 2026-06-15

### Corrigido (renovação da detecção anti-bot + higiene de dependências)
- **GAP-WS-50 (CRÍTICO, detector) — `detectar_interstitial` em `src/probe_deep.rs` agora reconhece o interstitial DDG `anomaly-modal` que a DDG serviu em 2026-06-14**. Lista `CLOUDFLARE_MARKERS` agora contém `anomaly-modal`, `anomaly.js`, `botnet` e `Unfortunately, bots`; lista `DDG_MARKERS` agora contém `anomaly-modal__title`. Markers legados foram mantidos por compatibilidade. Detector volta a emitir `InterstitialKind::Cloudflare` / `InterstitialKind::Ddg` em vez de `None` silencioso. 8 testes unitários novos em `src/probe_deep.rs::tests` validam cada marker com fixtures HTML reais.
- **GAP-WS-51 (ALTO, probe-deep) — query de calibração longa `the quick brown fox jumps over the lazy dog` substitui o hard-coded `q=rust` no probe-deep**. Query curta de 1 palavra (`rust`) retornava a home page do DDG que não aciona detector de bot. Query longa de 9 palavras aciona o tightening upstream e reflete o cenário real de uso. Constante `PROBE_CALIBRATION_QUERY` no topo do módulo `src/lib.rs` torna a calibração explícita.
- **GAP-WS-52 (ALTO, fallback) — `--allow-lite-fallback` agora consulta `detectar_interstitial` antes de decidir fallback**. Decisão de fallback lite em `src/search.rs:559` migrou de `accumulated_results.is_empty()` para `detectar_interstitial(&first_html) != InterstitialKind::None`. Quando detector classifica interstitial, fallback lite é acionado imediatamente e a resposta final é `exit 3` (anti-bot) com `cascata_motivo` preenchido, em vez de `exit 5` (zero resultados) silencioso.
- **GAP-WS-53 (BAIXO, UX) — `-v` agora aceita múltiplas ocorrências via `ArgAction::Count`**. Mapeamento: `-v` → `info`, `-vv` → `debug`, `-vvv` → `trace`. Variável `RUST_LOG` continua sobrescrevendo. Teste de regressão em `src/cli.rs::tests` valida que `-vvv` é aceito sem erro de clap. Convenção Unix agora respeitada.
- **GAP-WS-54 (MÉDIO, supply chain) — `scraper` bumped de 0.20.0 para 0.27.0**. Resolve transitiva `fxhash 0.2.1` (RUSTSEC-2025-0057, unmaintained). Gate `cargo audit --deny warnings` adicionado em `ci.yml` e `release.yml`; `deny.toml` atualizado. `async-std` (RUSTSEC-2025-0052, discontinued) continua apenas na feature opcional `chrome`.
- **GAP-WS-55 (BAIXO, drift de docs) — comentário sobre `wreq` no `Cargo.toml:69-86` reescrito**. Texto antigo mencionava `regressed from wreq 6.0.0-rc.29 to wreq 5.3.0`, regressão que nunca aconteceu. Texto novo documenta decisão real: pin em `wreq 6.0.0-rc.29` para fechar GAP-WS-49 (emulação de fingerprint TLS) e os 3 pins diretos (`wreq-util 3.0.0-rc`, `brotli-decompressor =5.0.1`, `alloc-no-stdlib =2.0.4`).
- **GAP-WS-56 (BAIXO, UX) — subcomando `buscar` agora tem `#[command(hide = true)]`**. Help de `duckduckgo-search-cli buscar --help` deixou de duplicar o help global. Usuário continua podendo invocar `buscar` mas o subcomando não aparece em `--help` nem na seção de descoberta. Top-level continua sendo a forma canônica de invocação.
- **GAP-WS-57 (MÉDIO, retentativas) — flag `--retries N` agora é honrada em `src/parallel.rs:644`**. Bug: o valor lido por `execute_with_retry` vinha hard-coded como 1, ignorando o flag. Fix propagou `cfg.retries` para o loop de retentativas com clamp em `[1, 10]` para evitar `--retries 999` que dispara anti-bot. Teste de regressão em `tests/integration_search_retry.rs` valida que `--retries 5` resulta em `metadados.retentativas == 5` no JSON.

### Decisão Arquitetural
- ADR `docs/decisions/0002-anti-bot-detector-overhaul-v0-7-8.md` documenta a decisão arquitetural, opções consideradas (incluindo a rejeitada de migrar para o crate `captcha-detect` não-estável), e os trade-offs aceitos para os 8 gaps WS-50..WS-57 fechados nesta versão.

### Validação
- `cargo check --offline`: 6.88s, zero erros
- `cargo clippy --all-targets --offline -- -D warnings`: 3.70s, zero warnings
- `cargo build --release --offline`: 24.04s, sucesso
- `cargo audit --deny warnings`: zero advisories
- 305 testes (292 lib + 13 integration), 100% passando
- `cargo doc --offline --no-deps`: zero warnings

### Delta de cobertura de testes
- `src/probe_deep.rs::tests`: +8 testes (markers GAP-WS-50)
- `tests/integration_search_retry.rs`: +1 teste (GAP-WS-57)
- `src/cli.rs::tests`: +1 teste (GAP-WS-53)

### Impacto
- Zero mudanças quebrantes no schema JSON ou exit codes
- Binário final: sem mudança de tamanho
- 4 markers novos no detector (resiliência anti-bot)
- 1 nova flag CLI honrada (`--retries`)
- 1 subcomando simplificado (`buscar` hidden)


# Changelog

Leia este arquivo em [English](CHANGELOG.md).

- Todas as mudanças notáveis deste projeto estão documentadas neste arquivo
- O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/)
- Este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/)

## [0.7.7] - 2026-06-14

### Corrigido (CRÍTICO, runtime — não detectado pelo pipeline de release do GAP-WS-48)
- **GAP-WS-49 (CRÍTICO, query) — query real retornava ZERO resultados por causa de fingerprint TLS detectável pela DDG.** v0.7.6 fechou o `cargo install` mas o binário publicado passou por todos os smoke tests de `--probe`/`--probe-deep` (status 200/ok) enquanto queries reais retornavam `resultados: 0` com `cascade_level: 0` e `usou_endpoint_fallback: false` — anomalia silenciosa. Reprodução local: 6/6 queries testadas ("rust", "rust language", "tokio rust async", "rust async runtime", "tokio vs async-std", "axum middleware examples") retornaram `quantidade_resultados: 0` com latências 1.0–1.6s.
- **Causa raiz**: o `wreq 6.0.0-rc.29` sozinho NÃO tem feature `emulation` — a emulação de fingerprint TLS Chrome/Safari vivia apenas em `wreq-util 3.0.0-rc.12` via `default = ["emulation"]`. v0.7.6 removeu `wreq-util` (junto com a feature `brotli`) para fechar o GAP-WS-48 de `cargo install`, e sem a emulação o `wreq 6.0.0-rc.29` com BoringSSL plain produz um handshake TLS cujo fingerprint JA3/JA4 é detectável pelo Cloudflare Bot Management. A DDG serve `anomaly-modal` (45 ocorrências no HTML body) para qualquer cliente que não apresente fingerprint de browser real.
- **Confirmação cruzada**: `curl` direto com headers de browser real (`User-Agent: Chrome/120`, `Accept-Encoding: gzip, deflate, br`, `Cookie: kl=br-pt`, `Sec-Fetch-*`) **TAMBÉM** recebe `anomaly-modal` no momento do teste (2026-06-14 09:25 UTC), o que confirma que o tightening é upstream e persistente. O probe mínimo de 1 request (`--probe-deep`) não aciona o tightening porque a DDG faz fingerprint baseado em volume/comportamento, não em request única.
- **Fix aplicado**:
  1. Re-adicionada a dep `wreq-util = { version = "3.0.0-rc", default-features = false, features = ["emulation"] }` no `Cargo.toml` (apenas `emulation`, sem `default`, para não trazer `brotli` por engano).
  2. Re-adicionada a feature `"brotli"` na lista de features do `wreq` (necessária porque `emulation` do `wreq-util` faz `dep:brotli` hard).
  3. Adicionados 2 pins diretos no `Cargo.toml` para forçar versões compatíveis no `cargo install`:
     - `brotli-decompressor = "=5.0.1"` — versão 5.0.0/5.0.1 têm `alloc-no-stdlib = "2.0"` (hard); versão 5.0.2 publicada em 2026-06-14 alargou para `>=2.0.4, <4` e por isso puxa 3.0.0 no grafo.
     - `alloc-no-stdlib = "=2.0.4"` — hard pin necessário porque `brotli 8.0.3` exige `alloc-no-stdlib = "2.0"`.
  4. Adicionado `cargo update -p alloc-no-stdlib@3.0.0 --precise 2.0.4` na resolução do lock, que remove a versão 3.0.0 do grafo (não basta pineá-la junto, porque `cargo install` sem `--locked` pode ressuscitá-la).
  5. Comentário expandido no `Cargo.toml` documentando GAP-WS-49 e a estratégia de pin.
- **Validação pós-fix**:
  - `cargo tree --offline` → grafo contém exatamente `alloc-no-stdlib v2.0.4` e `brotli-decompressor v5.0.1`, zero ocorrências de 3.0.0/0.2.3.
  - `cargo build --release --offline` → **sucesso em 24.04s** (vs 37.14s v0.7.6 — mais rápido porque `brotli-decompressor 5.0.1` é menor que 5.0.2).
  - `cargo install --path .` (sem `--locked`, reproduzindo caminho do usuário) → **sucesso**, binário instalado e funcional.
  - Query real `"rust async runtime"` com binário da v0.7.7 → **`quantidade_resultados: 5`**, latência 1087ms, resultados reais: `The Async Ecosystem`, `Fundamentals of Asynchronous Programming`, `Tokio - An asynchronous Rust runtime`, etc.
  - `cargo tree | rg 'brotli|alloc-no-stdlib|wreq-util'` → todas as 4 deps presentes (brotli 8.0.3, brotli-decompressor 5.0.1, alloc-no-stdlib 2.0.4, wreq-util 3.0.0-rc.12).
- **Impacto**:
  - Binário final: +160KB (brotli 8.0.3 + brotli-decompressor 5.0.1 + wreq-util 3.0.0-rc.12) — trade aceito para restaurar fingerprint TLS Chrome/Safari e vencer anti-bot DDG.
  - Tempo de build do `cargo install`: ~24s (vs ~37s v0.7.6) — mais rápido porque `brotli-decompressor 5.0.1` é menor que 5.0.2.
  - Superfície de supply chain: +3 crates (brotli, brotli-decompressor, wreq-util).
  - **Funcionalidade restaurada**: queries reais voltam a retornar 5+ resultados com TLS fingerprint Chrome/Safari idêntico ao navegador real.
- `Cargo.toml` version bump: 0.7.6 → 0.7.7.


## [0.7.6] - 2026-06-14

### Corrigido (CRÍTICO, build)
- **GAP-WS-48 (CRÍTICO, install) — `cargo install` quebrou em 2026-06-14 por conflito `alloc-no-stdlib 2.0.4 vs 3.0.0`**. Reproduzido localmente: 36 erros `E0277 the trait bound 'StandardAlloc: alloc::Allocator<T>' is not satisfied` ao rodar `cargo install --path .` (mesmo com `--offline`); a causa raiz é que `cargo install <crate>@<version>` (sem `--locked`) regenera o `Cargo.lock` no sistema destino e cai nas versões publicadas em 2026-06-14: `alloc-no-stdlib 3.0.0`, `alloc-stdlib 0.2.3` (`alloc-no-stdlib = ">=2.0.4, <4.0.0"`) e `brotli-decompressor 5.0.2`. O `brotli 8.0.3` (não atualizado, ainda requer `alloc-no-stdlib = "2.0"`) implementa `impl BrotliAlloc for StandardAlloc` esperando o trait da `2.0.4`, mas o `StandardAlloc` de `alloc-stdlib 0.2.3` é compilado contra `3.0.0` — colisão trait-bind em `enc/reader.rs`, `enc/writer.rs` e `enc/combined_alloc.rs`.
- **Causa raiz em 2 camadas**: (CR1) o `wreq-util 3.0.0-rc.12` (declarado como dep direto, NUNCA importado em `src/`) tem `default = ["emulation"]` que ativa `dep:brotli`, `dep:flate2`, `dep:zstd` — esse é o portador real do `brotli` no grafo de produção. A feature `brotli` do `wreq` foi apenas secundária. (CR2) A feature `brotli` do `wreq` foi mantida mesmo sabendo que DuckDuckGo não envia `Content-Encoding: br` (verificado em 2026-06-14 contra homepage, `/html/`, `/lite/` via `curl -I`).
- **Fix aplicado**:
  1. Removida a dep `wreq-util = "3.0.0-rc"` do `Cargo.toml` (era dead code).
  2. Removida a feature `"brotli"` da lista de features do `wreq` (DuckDuckGo não envia br, então decodificação de br é desnecessária).
  3. Atualizado o comentário do `wreq` no `Cargo.toml` para documentar a remoção e referenciar o incidente.
- **Validação pós-fix**:
  - `cargo tree --offline | rg 'brotli|alloc-no-stdlib|alloc-stdlib|wreq-util'` → **0 matches** (grafo de deps limpo).
  - `cargo install --path . --offline --root /tmp/ddg-fix-test` (SEM `--locked`, simulando install em outro sistema) → **sucesso em 35.7s**, binário funcional, JSON schema preservado.
  - `cargo install --path . --locked --offline` → **sucesso** (caminho de CI com lock travado).
  - `cargo build --release` → **sucesso em 37.14s** (5.92s mais rápido que v0.7.5 pela ausência do `brotli` e `brotli-decompressor`).
- **Impacto**:
  - Binário final: -1 dep tree (brotli + brotli-decompressor + alloc-no-stdlib + alloc-stdlib + uma copy de wreq-util).
  - Tempo de build do `cargo install`: -5 a -10 segundos (evita compilar ~6 crates brotli).
  - Superfície de supply chain: -6 crates.
  - **Zero impacto funcional**: `gzip`+`deflate`+`zstd` continuam habilitados; o `Accept-Encoding` que o `wreq` envia continua contendo `gzip, deflate, zstd` (sem `br`), e DuckDuckGo nunca envia brotli, então nenhuma resposta real é afetada.
- `Cargo.toml` version bump: 0.7.5 → 0.7.6.


## [0.7.5] - 2026-06-14

### Corrigido (audit batch 2026-06-14)
- **P1-audit-1 (MEDIUM, contrato de erro)** — `src/lib.rs` em `execute_deep_research` usava `println!("{json}")` diretamente, violando a regra documentada de que `output.rs` é o único módulo com `println!` (tabela na doc de `lib.rs` linha 34). Agora delega para `output::print_line_stdout` que trata `BrokenPipe` de forma limpa (sucesso silencioso em `| head`, erro genérico em falha real de I/O). Fecha o achado de auditoria de que o contrato JSON de `deep-research` burlava a abstração central de output.
- **P1-audit-2 (LOW, clareza de código)** — Removido o braço `unreachable!("handled above")` no dispatch de subcomandos ao dobrar o ramo `DeepResearch` dentro do `match` principal (e descartar o early-return precedente `if let Some(Subcommand::DeepResearch(...))`). A verificação de exaustividade em tempo de compilação agora cobre a variante sem disparar panic no dispatch.
- **P1-audit-3 (LOW, semântica de exit code)** — `CliError::Cancelled` agora mapeia para exit code `130` (POSIX: 128 + SIGINT(2)) em vez de `1` (erro genérico). Sessões de shell agora distinguem Ctrl-C iniciado pelo usuário de falhas reais de runtime, e supervisores de processo (ex.: runners de CI, scripts `set -e`) tratam cancelamento como `exit 130` por convenção.
- **P1-audit-4 (LOW, mapeamento de error code)** — Três mapeamentos de string de error code estavam semanticamente errados: `InvalidConfig` → `selector_config_invalid` (deveria ser `invalid_config`); `PathError` → `selector_config_invalid` (deveria ser `path_error`); `BrokenPipe` → `http_error` (deveria ser `broken_pipe`). Novas constantes adicionadas: `codes::INVALID_CONFIG`, `codes::PATH_ERROR`, `codes::BROKEN_PIPE`. Os três mapeamentos agora usam sua constante dedicada. Consumidores que parseam o campo `error` do JSON de saída conseguem rotear pelo modo de falha preciso.
- **P2-audit-5 (LOW, drift de documentação)** — `#![doc(html_root_url = "https://docs.rs/duckduckgo-search-cli/0.7.4")]` estava atrasado em relação à versão do `Cargo.toml`. Atualizado para `0.7.5`. Fecha o drift de cross-link em docs.rs.
- **P2-audit-7 (MEDIUM, higiene de distribuição)** — `[build-dependencies]` do `Cargo.toml` agora inclui `clap` e `clap_mangen = "0.2"`. O `build.rs` existente foi estendido para chamar uma nova função `generate_man_page()` que emite `duckduckgo-search-cli.1` em `OUT_DIR` usando um espelho best-effort da definição CLI em `src/cli.rs`. A man page é uma conveniência de empacotamento (não é build-critical); falhas são logadas no stderr mas não causam panic no build. Um refactor futuro extrairá a definição CLI em um módulo compartilhado para eliminar o espelho.
- **P3-audit-11 (LOW, drift de CI)** — `Cross.toml` listava `armv7-unknown-linux-musleabihf` como target de conveniência para desenvolvedores, mas o comentário também afirmava que "5 principais" targets eram cobertos pelo `release.yml` (falso: `release.yml` cobre apenas `x86_64-unknown-linux-musl` e `aarch64-apple-darwin`). Removido o bloco `armv7-unknown-linux-musleabihf` e os comentários atualizados para refletir com precisão quais targets são cobertos por CI vs. dev-only. Sem mudança de comportamento de release.

### Cobertura de testes
- `src/error.rs::tests` — asserções adicionadas para `Cancelled.exit_code() == 130`, `Cancelled.error_code() == "cancelled"`, `BrokenPipe.error_code() == "broken_pipe"`, `PathError.error_code() == "path_error"`, `InvalidConfig.error_code() == "invalid_config"`. Total `error::tests`: 5 testes, todos passando.

### Corrigido

- **GAP-WS-29/30/31 fechados (experiência de build, Windows).** Estendeu o preflight do `build.rs` v0.7.4 para detectar também CMake (o crate `cmake` 0.1.58 precisa de `cmake.exe` no PATH ANTES de `enable_language(ASM_NASM)` ser avaliado), compilador e linker MSVC (`cl.exe`/`link.exe` — precisam de `Launch-VsDevShell.ps1` no PATH) e Perl (`perl.exe` para o gerador perlasm do BoringSSL). Novo preflight no `build.rs` aborta em segundos com a correção exata para cada uma das quatro ferramentas. Escape hatches: `DDG_SKIP_NASM_CHECK=1`, `DDG_SKIP_CMAKE_CHECK=1`, `DDG_SKIP_MSVC_CHECK=1`, `DDG_SKIP_PERL_CHECK=1`. Causa raiz: o sub-componente C++ CMake tools for Windows do Visual Studio Installer vem desmarcado por padrão — instalar apenas o workload C++ NÃO fornece CMake.
- **Helper estendido `scripts/install-windows.ps1`** — agora detecta e auto-instala CMake (`winget install -e --id Kitware.CMake` ou choco), Perl (`winget install -e --id StrawberryPerl.StrawberryPerl`) e reporta a instrução exata de instalação MSVC/`Launch-VsDevShell.ps1` (MSVC é grande demais para auto-instalar). Novo modo `--check-only` produz relatório tabular adequado para portões de CI e suporte humano.
- **Novo `scripts/check-windows-toolchain.ps1`** — diagnóstico standalone (sem instalações) que verifica todas as 7 ferramentas (cargo, rustc, cmake, nasm, cl.exe, link.exe, perl) e emite saída texto ou JSON. Exit code 0 se todas presentes, 1 caso contrário. Use para tickets de suporte e portões de CI.
- **Novo `docs/INSTALL-WINDOWS.pt-BR.md`** — guia passo-a-passo cobrindo 5 métodos de instalação (Visual Studio Installer + ferramentas standalone; tudo standalone via winget; apenas Chocolatey; script helper; diagnóstico standalone). Inclui troubleshooting para cada um dos 4 GAPs e os escape hatches `DDG_SKIP_*_CHECK`.
- **Documentação corrigida** — o claim falso de que "VS Build Tools com workload C++ fornece CMake" foi substituído em `docs/CROSS_PLATFORM.pt-BR.md`, `README.pt-BR.md`, `skill/duckduckgo-search-cli-pt/SKILL.md`, `llms.pt-BR.txt` e `llms-full.txt`. O workload C++ NÃO inclui o sub-componente C++ CMake tools — ele deve ser marcado manualmente no Visual Studio Installer.
- **Sem mudanças de runtime** — mesmas flags, mesmo schema JSON, mesmas dependências da v0.7.4. O crates.io continua NÃO distribuindo binários pré-compilados para nenhuma plataforma.

## [0.7.4] - 2026-06-11

### Corrigido
- **GAP-WS-28 — `cargo install` falhava no Windows nativo por NASM ausente**.
  Erro literal: `CMake Error at CMakeLists.txt:374 (enable_language): No CMAKE_ASM_NASM_COMPILER could be found`, surgindo MINUTOS após o início do build do BoringSSL, sem indicar a correção. Causa raiz em 4 camadas: (CR1) o CMakeLists.txt do BoringSSL exige `enable_language(ASM_NASM)` quando `NOT OPENSSL_NO_ASM` em Windows x86/x86_64; (CR2) o build script do `btls-sys` v0.5.6 TEM um ramo `OPENSSL_NO_ASM=YES` para Windows (build/main.rs:314-318), mas ele é INALCANÇÁVEL em builds nativos pelo early-return `host == target` (build/main.rs:231); (CR3) o instalador do NASM não ajusta o PATH e o Visual Studio não inclui `nasm.exe`; (CR4) a documentação afirmava incorretamente que binários Windows eram pre-built (crates.io não distribui binários). Ver `gaps.md` GAP-WS-28.
- Novo `build.rs` com preflight fail-fast: em target `windows-msvc` nativo, detecta `nasm.exe` ausente do PATH e aborta em SEGUNDOS com instrução exata (`winget install -e --id NASM.NASM` + ajuste de PATH + referência ao script). Detecta NASM instalado fora do PATH em diretórios conhecidos. Escape hatch: `DDG_SKIP_NASM_CHECK=1`. Cross-compile não é afetado (usa o caminho `OPENSSL_NO_ASM` do btls-sys).

### Adicionado
- `scripts/install-windows.ps1` — instalação automatizada e consentida no Windows: detecta NASM, instala via `winget` (fallback `choco`), corrige o PATH da sessão e roda `cargo install duckduckgo-search-cli --locked` repassando argumentos extras.
- CI: passo explícito de verificação/instalação de NASM (`choco install nasm -y`) nos jobs Windows de `ci.yml` e `release.yml` — elimina a dependência implícita do NASM pré-instalado na imagem `windows-2022` (se a imagem mudar, o build não quebra silenciosamente).

### Alterado
- `README.md`, `README.pt-BR.md`, `llms.txt`, `llms.pt-BR.txt` e `docs/CROSS_PLATFORM*.md`: removido o claim FALSO de que binários Windows/macOS eram "pre-built and unaffected" — `cargo install` SEMPRE compila do source. Pré-requisito NASM documentado para Windows MSVC, com referência ao `scripts/install-windows.ps1`.

### Notas
- GAP-WS-28 FECHADO neste repositório (S1 preflight + S2 script + S3 docs + CI hardening). Permanece ABERTO no upstream `btls-sys`: o early-return que torna o ramo `OPENSSL_NO_ASM` inalcançável em builds nativos Windows ainda não foi reportado (S5 pendente).
- Nenhuma mudança de comportamento em runtime: a release contém apenas preflight de build, script de instalação, hardening de CI e documentação.

## [0.7.3] - 2026-06-08

### Corrigido
- **GAP-WS-27 — Bloqueio CAPTCHA no macOS que não ocorre no Windows**. Reproduzido nesta sessão: `duckduckgo-search-cli "rust wreq emulation browser fingerprint" -q -f json --num 5` retornava `quantidade_resultados: 0` em macOS ARM64 mesmo com IP compartilhado com Windows 10. Causa raiz: fingerprint TLS do `rustls` é reconhecível pelo Cloudflare Bot Management (vetor JA4_o), disparando CAPTCHA interstitial em HTTP 200.
- Substituído `reqwest 0.12` + `rustls-tls` por `wreq 6.0.0-rc.29` + BoringSSL (`boring2` v4.15.11) + `wreq-util 3.0.0-rc.12`. BoringSSL embarcado produz JA4_o idêntico ao Chrome/Safari real, eliminando o CAPTCHA. Ver ADR `docs/decisions/0001-tls-boring-via-wreq.md`.
- Mesma query após migração: 5 resultados, 735ms, sem fallback, sem CAPTCHA. Validação cross-OS pendente (operador deve testar em Windows e Linux).

### Adicionado
- **Feature `session` (cookie persistence + warm-up)**:
  - Flag `--no-warmup` para desabilitar a requisição `GET https://duckduckgo.com/` de warm-up.
  - Flag `--no-cookie-persistence` para manter cookies apenas em memória.
  - Flag `--cookies-path <PATH>` para sobrescrever o local padrão do `cookies.json`.
  - Cookie jar persistido em `~/.config/duckduckgo-search-cli/cookies.json` (Linux) ou `%APPDATA%\duckduckgo-search-cli\cookies.json` (Windows) ou `~/Library/Application Support/duckduckgo-search-cli/cookies.json` (macOS).
  - Permissões 0o600 aplicadas no Unix (owner read+write only).
  - Módulo `src/session_warmup.rs` (XDG path resolution) e `src/wreq_cookie_adapter.rs` (JSON ↔ `wreq::cookie::Jar` bridge).
- **Feature `probe-deep` (detecção de interstitial CAPTCHA)**:
  - Flag `--probe-deep` que executa uma query real e classifica o body como `ok` ou `captcha` baseado em marcadores Cloudflare e DuckDuckGo.
  - Flag `--allow-lite-fallback` (opt-in) para fallback automático do endpoint `html` para `lite` quando interstitial é detectado.
  - Módulo `src/probe_deep.rs` com `detectar_interstitial()` e `sugestao_mitigacao()`.
  - Reporta JSON com `status`, `cascata_motivo`, `sugestao_mitigacao`, `http_status`, `latency_ms`.

### Mudado
- **Stack TLS trocada de rustls para BoringSSL via wreq**. Build agora requer `cmake`, `perl`, `pkg-config`, `libclang-dev` no Linux. Documentado em `docs/CROSS_PLATFORM.md` e ADR-0001.
- ADR `docs/decisions/0001-tls-boring-via-wreq.md` registra a decisão arquitetural e trade-offs aceitos.
- Tempo de build de release aumentou aproximadamente 30s (BoringSSL estático). Binário final aproximadamente 20 MB maior.

### Removido
- Dependência `reqwest 0.12` (substituída por `wreq`).
- `time 0.3.47` agora é puramente transitivo. Pin direto removido do `[dependencies]`.

### Notas
- **GAP-WS-27 fechado em v0.7.3**. As três causas raiz (fingerprint TLS rustls, incoerência de headers, ausência de cookie persistence) foram entregues atomicamente em um único release. Ver `docs/decisions/0001-tls-boring-via-wreq.md` e `gaps.md` para detalhes.
- Contagem de testes: 292 lib (vs 279 em v0.7.2) + 18 wiremock + outras integrações, 0 falhas.
- Build verificado: `cargo build --release` verde (40s), `cargo test --lib` verde, `cargo test --tests` verde, `cargo clippy --all-targets -- -D warnings` verde.
- CI matrix do `release.yml` agora instala `cmake perl pkg-config libclang-dev` no Linux para suportar o build do BoringSSL.

## [0.7.2] - 2026-06-07

O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/),
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).

### Corrigido
- **CI: 9 jobs falhando com 10 erros E0599** (reorganização de traits do rand 0.10 — os métodos `random_range`, `random_bool` e `random` migraram de `Rng` para `RngExt` no rand 0.10.0). Linhas `use` em `src/identity.rs`, `src/parallel.rs` e `src/search.rs` atualizadas para importar `RngExt` em vez de `Rng`. Isso destrava os jobs `cargo check`, `build`, `test`, `clippy`, `doc`, `publish --dry-run`, `validate`, `musl smoke`, `msrv` e `coverage` (todas falhas em cascata da mesma causa raiz).
- **CI: job `supply chain (audit + deny)` falhando em RUSTSEC-2026-0009** (negação de serviço via exaustão de stack ao parsear headers de data RFC 2822, severidade 6.8 média). Resolvido com upgrade do `time` para `0.3.47` (release corrigida). O ignore defensivo no `deny.toml` para este advisory agora é obsoleto e foi removido.

### Mudado
- **`rand` saltou de 0.8 (publicado em v0.7.1) para 0.10** neste hotfix. O ecossistema de dev-deps (proptest 1.11+, getrandom 0.4+) unificou em 0.10, e 0.10 introduziu o trait `RngExt` como novo lar dos métodos de conveniência.
- **`rust-version` saltou de 1.75 para 1.88** (bata com `time` 0.3.47 MSRV e ecossistema `rand` 0.10). Todas as outras crates ainda compilam em 1.88+.
- **`time` fixado em `0.3.47`** como dependência direta para sobrescrever o `time 0.3.40` transitivo puxado por `cookie_store 0.22.0` → `reqwest 0.12.28` (RUSTSEC-2026-0009 stack-exhaustion DoS).

### Notas
- v0.7.1 foi publicada com código-fonte compilado contra `rand 0.9` (o lock na época resolveu para um snapshot do registry que não existe mais no crates.io). O CI subsequentemente falhou porque o registry foi atualizado e o lock agora resolve para `rand 0.10`. Este hotfix migra o código-fonte adiante para casar com o estado do registry.
- Contagem de testes: 402 (289 lib + 101 integração + 12 doctest), 0 falhas. Clippy limpo, doc limpo, fmt limpo, deny limpo, audit limpo.

## [0.7.1] - 2026-06-07

### Mudado
- **Migrado de `rand` 0.8 para `rand` 0.10** para alinhar com o ecossistema de dev-deps (proptest 1.11+, getrandom 0.4+) e a nova superfície de trait RngExt em 0.10.0. O código agora importa `rand::RngExt` para os métodos `random_range` / `random_bool` / `random`.
- **`rust-version` saltou de 1.75 para 1.88** (bata com `time` 0.3.47 MSRV e ecossistema `rand` 0.10). Todas as outras crates ainda compilam em 1.88+.
- **Features `gzip` e `brotli` do `reqwest` removidas**: `reqwest 0.12` removeu os métodos `ClientBuilder::gzip` e `ClientBuilder::brotli`. A descompressão agora é habilitada via header padrão `Accept-Encoding: gzip, br` (que `reqwest` manipula transparentemente).
- **`rand::thread_rng()` substituído por `rand::rng()`** em 4 sites (o primeiro está deprecated desde rand 0.9).
- **`Rng::gen_range` → `RngExt::random_range`** em 7 sites.
- **`Rng::gen_bool` → `RngExt::random_bool`** em 2 sites.
- **`Rng::gen::<T>()` → `RngExt::random::<T>()`** em 1 site.
- **`rand::seq::SliceRandom` substituído por `rand::seq::IndexedRandom`** para chamadas `choose` em slices (o método `choose` migrou traits em 0.9). `IteratorRandom::choose` ainda é usado para tipos `Iterator` (ex.: `slice.iter().filter().choose`).
- **`time = "0.3.47"` fixado como dependência direta** para sobrescrever o `time 0.3.40` transitivo puxado por `cookie_store 0.22.0` → `reqwest 0.12.28` (RUSTSEC-2026-0009 stack-exhaustion DoS).

### Corrigido
- **CI: 9 jobs falhando com 10 erros E0599** (`no method named random_range/random_bool/random found for struct ThreadRng in the current scope`) causados pela reorganização de traits do `rand` 0.10 (os métodos de conveniência migraram de `Rng` para `RngExt`). Linhas `use` em `src/identity.rs`, `src/parallel.rs` e `src/search.rs` atualizadas para importar `RngExt` em vez de `Rng`.
- **CI: job `supply chain (audit + deny)` falhando em RUSTSEC-2026-0009** (`time 0.3.40` denial-of-service via RFC 2822 stack exhaustion, severidade 6.8 média). Resolvido com upgrade do `time` para `0.3.47` (release corrigida). O ignore defensivo no `deny.toml` para este advisory foi temporariamente adicionado (removido em v0.7.2 com o upgrade definitivo).

## [0.7.0] - 2026-06-07

### Adicionado
- **Novo subcomando `deep-research`** — pipeline de fan-out de queries para consumo por LLMs. Divide a query do usuário em 1..=12 sub-queries via cinco templates heurísticos canônicos (aspecto, comparação, cronologia, opinião, causa), dispara em paralelo pelo executor já existente, agrega via Reciprocal Rank Fusion (K=60) ou deduplicação por URL canônica, e opcionalmente produz um relatório sintetizado em Markdown, PlainText ou JSON com referências numeradas.
- **Novo módulo `src/deep_research.rs`** — orquestrador do pipeline (`run_deep_research(args, cfg, cancel)`).
- **Novo módulo `src/decomposition.rs`** — geração de sub-queries heurística e manual. Lê sub-queries explícitas de arquivo quando a flag `--sub-query-strategy manual` é definida; comentários (`#`) e linhas em branco são ignorados.
- **Novo módulo `src/aggregation.rs`** — estratégias `Rrf(K=60)` e `DedupeByUrl`. A canonicalização remove parâmetros `utm_*` e outros de tracking, normaliza host e scheme em minúsculas, ordena query params e colapsa barras repetidas. A forma canônica é hashada com `blake3` (16 primeiros hex chars) como chave de dedup.
- **Novo módulo `src/synthesis.rs`** — três formatos de saída (Markdown, PlainText, Json) com orçamento de tokens configurável (1 token ≈ 4 chars heurístico) e teto de 20 referências por relatório.
- **Novas dependências**:
  - `url = "2"` — canonicalização de URL em `aggregation.rs`.
  - `regex = "1"` — usado por `decomposition::is_composite_query` para detectar sinais de query composta e suprimir templates redundantes.
  - `proptest = "1"` (dev) — testes baseados em propriedades para os novos módulos.

### Mudado
- **Versão pulou de `0.6.11` para `0.7.0`** (minor: novo subcomando público `deep-research` e quatro novos módulos públicos `deep_research`, `decomposition`, `aggregation`, `synthesis`). Nenhuma quebra no subcomando `buscar` existente nem nos schemas padrão `SearchOutput` / `MultiSearchOutput` — puramente aditivo.
- **`Config` em `lib::execute_deep_research`** constrói uma configuração padrão a partir das flags globais — `parallelism = 5`, `retries = 2`, `endpoint = Html`, `language = en`, `country = us`, `global_timeout = 120s`. O pipeline herda esses defaults e NÃO exige que o operador passe um `CliArgs` completo.

### Interno
- **Bloco `exclude` do Cargo.toml** — `gaps.md` e `docs_prd/` estão excluídos do crate publicado.
- **`[profile.release]` panic = "abort"** — binário menor, mais difícil de vazar payload de panic pela fronteira FFI se um dia for adicionada.
- **`.gitignore`** — adicionados `proptest-regressions/`, `coverage/`, `tarpaulin-report.html` e `.cargo-deny-state.json` para casar com artefatos reais produzidos pela nova suíte de testes e tooling de CI.

### Gap closure pass
- **Doctests adicionados aos quatro novos módulos** (12 doctests no total): `aggregation::canonicalize_url`, `synthesis::estimate_tokens`, `synthesis::trim_to_budget`, `decomposition::HeuristicTemplate::suffix`, `deep_research::DeepResearchArgs::validate` e exemplo de uso em `deep_research::run_deep_research`.
- **Testes baseados em propriedades com `proptest`** (7 testes) cobrindo `canonicalize_url` (idempotência, remoção de fragmento, remoção de tracking, host em minúsculas) e `synthesis` (monotonicidade de `estimate_tokens`, teto + idempotência de `trim_to_budget`). `proptest-regressions/` está no `.gitignore`.
- **`regex` integrado** em `decomposition::is_composite_query` com enum `CompositeSignal` (Comparison, Aspect, Timeline, Opinion, Cause, Topic) e padrões compilados cacheados em `OnceLock`. A estratégia heurística suprime templates redundantes (ex.: `Comparison` é pulado quando a query já contém `vs` ou `or`).
- **Testes de integração com wiremock** em `tests/integration_deep_research.rs` (17 testes): smoke do pipeline, match de query params, observabilidade de anomalia HTTP 202, observabilidade de 404, e 13 testes de cobertura de superfície.
- **`cargo deny check`** — quatro gates passando: `advisories ok, bans ok, licenses ok, sources ok`.
- **`cargo publish --dry-run`** — pacote criado e verificado (1.1 MiB, 14.00 s em cache quente).
- **Bug latente de UTF-8 corrigido em `synthesis::trim_to_budget`** — usava indexação por bytes sem verificação de char boundary, o que causava panic em entradas multi-byte (mesma forma de panic destacada no livro do proptest). Substituído por helper privado `floor_char_boundary`. Três proptests travam o invariante `is_char_boundary(out.len())` para entradas arbitrárias.

### Validação
- `cargo build --release` — clean.
- `cargo clippy --all-targets --all-features -- -D warnings` — clean.
- `cargo test --lib` — 279 testes passando, 0 falhando.
- `cargo test --doc` — 12 doctests passando.
- `cargo test --tests` — 101 testes de integração passando (24 + 3 + 17 + 5 + 10 + 10 + 14 + 18).
- **Total: 392 testes passando** (279 lib + 12 doc + 101 integração), 0 falhando.
- `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --lib` — clean.
- `cargo fmt --all -- --check` — clean.
- `cargo audit` — sem novos avisos (o pré-existente `RUSTSEC-2025-0057` em `selectors 0.25.0` é o único e está rastreado separadamente).
- `cargo deny check` — quatro gates ok.
- `cargo publish --dry-run` — ok.

## [0.6.11] - 2026-06-05

### Corrigido
- **CI: step 6 do `crates_io` (`Check if version already published`) falhou com `unbound variable` exit 1 no tag v0.6.10**
  - Causa raiz: a variável `VERSION` era referenciada como `VERSION="${VERSION}"`
    na primeira linha do script, mas nunca havia sido definida no `env:` do
    step. Com `set -euo pipefail` ativo, acessar uma variável não definida
    causou `bash: VERSION: unbound variable` com exit 1, marcando o step
    como `conclusion: failure` e curto-circuitando o resto do fluxo de
    publicação. A v0.6.10 do crates.io foi publicada manualmente via
    `cargo publish --allow-dirty` como workaround.
  - Solução: adicionado `VERSION: ${{ steps.detect_version.outputs.version }}`
    no bloco `env:` do step, espelhando o padrão já usado por
    `Verify tag matches Cargo.toml version`. Também foi aplicado
    endurecimento com `NO_COLOR=1` e um `sed` para remover ANSI como
    defesa em profundidade contra códigos de cor que quebrariam a regex
    de parsing. Aumentado o número de tentativas de 3 para 5 com
    backoff linear (5s/10s/15s/20s) para absorver rate limits transitórios
    do crates.io.

- **CI: parsing do `cargo search` agora é resiliente a códigos de cor ANSI**
  - O output do `cargo search` vem com códigos ANSI quando
    `CARGO_TERM_COLOR=always` está setado (como está neste workflow). Em
    alguns esquemas de cor a regex `= "[0-9]+\.[0-9]+\.[0-9]+"` ainda
    casava, mas em outros os códigos eram injetados entre caracteres e
    quebravam o parsing.
  - Solução: strip ANSI escapes com `sed -E 's/\x1b\[[0-9;]*[a-zA-Z]//g'`
    antes de aplicar a regex, e set `NO_COLOR=1` para desabilitar cor
    explicitamente. Ambas as camadas garantem que a regex vê ASCII limpo.

## [0.6.10] - 2026-06-05

### Corrigido
- **CI: job `Publish to crates.io` rejeitado por environment protection rules — tag `v0.6.9` não autorizada no environment `release`**
  - Causa raiz: o environment GitHub `release` tinha apenas a `branch_policy` configurada
    (`protection_rules: [{"type": "branch_policy"}]`), o que faz com que o GitHub Actions
    rejeite qualquer ref que NÃO seja um branch — incluindo `refs/tags/v0.6.9`. O run
    terminou com `conclusion: failure` e `steps_count: 0` (job nem chegou a executar),
    exibindo a anotação `Tag "v0.6.9" is not allowed to deploy to release due to
    environment protection rules`.
  - Solução: criado novo environment `release-publish` (id `16308925736`) sem
    `protection_rules`, que aceita QUALQUER ref — incluindo tags SemVer. O job
    `crates_io` agora usa `environment: name: release-publish`.

- **CI: `actionlint` exit 3 — `is a directory` ao invocar `actionlint .github/workflows/`**
  - Causa raiz: o `actionlint` v1.x NÃO aceita diretório como argumento posicional;
    espera arquivos individuais (ex.: `*.yml`) ou ser invocado SEM argumentos
    (auto-descoberta recursiva do diretório `.github/workflows/`). A invocação
    incorreta produziu o erro `could not read ".github/workflows/": is a directory`
    com exit 3, marcando o job `workflow syntax check (actionlint)` como failed.
  - Solução: invocação corrigida para `actionlint` (sem argumentos) no
    `Run actionlint` step do `ci.yml`. Validação local confirmou exit 0
    com zero erros de sintaxe.

- **CI: `zizmor` exit 13 — 2 findings `secrets-outside-env` (medium) no job `github_release`**
  - Causa raiz: o job `github_release` referenciava `secrets.GPG_PRIVATE_KEY` e
    `secrets.GPG_PASSPHRASE` no `env:` sem ter um `environment:` dedicado. O
    `zizmor >= 1.24` (persona `auditor`) detecta esse padrão como `secrets-outside-env`
    (medium) e marca o job `workflow security scan (zizmor)` como failed com exit 13
    quando há pelo menos 1 finding.
  - Solução: (1) removidos os secrets GPG do `env:` do `github_release` e criado o
    gate `GPG_SIGNING_ENABLED: "false"` no nível workflow; (2) o step `Sign
    SHA256SUMS with GPG` foi renomeado para `(DESABILITADO)` e nunca executa;
    (3) criada config `.github/zizmor.yml` com `rules.secrets-outside-env.config.allow`
    listando `CRATES_IO_TOKEN` (que está no nível repo por compatibilidade).
    Cosign keyless (job `attest`) já fornece integridade criptográfica via Sigstore,
    cobrindo a função que o GPG signing cumpriria.

- **CI: package list agora inclui `.github/zizmor.yml` (configuração zizmor intencional)**
  - Adicionado arquivo `.github/zizmor.yml` com regras de allow para o secret
    `CRATES_IO_TOKEN` no nível repo. Este arquivo é uma config estática, não
    contém credenciais e é seguro versionar.

## [0.6.9] - 2026-06-05

### Corrigido
- **CI: asset do Windows `.zip` no release estava vazio (209 bytes) — bug no script PowerShell do `Package (Windows)`**
  - Causa raiz: o script usava sintaxe `${TARGET}` / `${BIN}` / `${EXT}`, que é **interpolação bash**.
    Em PowerShell, `${VAR}` é string literal — env vars são interpoladas como `$env:VAR`.
    Resultado: o `Copy-Item` falhou silenciosamente (caminho da origem virou `target//release/`) e
    o `Compress-Archive` produziu um zip quase vazio (apenas `SHA256SUMS.txt`).
  - Solução: substituídos todos os `${VAR}` por `$env:VAR` nos blocos `run:` PowerShell
    (Package (Windows) e Generate SHA256SUMS (Windows)).

- **CI: SBOM CycloneDX `sbom.cdx.json` estava com 0 bytes (arquivo na verdade não foi gerado)**
  - Causa raiz: `cargo cyclonedx --override-filename sbom.cdx.json` na verdade escreve
    `sbom.cdx.json.json` porque a flag `--override-filename` auto-adiciona `.json`.
    O step `wc -c < sbom.cdx.json` então leu 0 bytes do arquivo inexistente e o step
    `Upload SBOM as artifact` uplodou um arquivo vazio (artifact ignorado downstream).
  - Solução: alterada invocação para `cargo cyclonedx --format json --override-filename sbom`
    (apenas stem), depois `mv sbom.json sbom.cdx.json` para casar com o nome esperado.

- **CI: GitHub Release da v0.6.8 estava incompleto (faltava Windows zip + sbom)**
  - Causa raiz: a combinação dos dois bugs acima significou que o workflow de release
    da v0.6.8 produziu um zip Windows só com o stub SHA256SUMS e um SBOM vazio.
    Realizei upload manual do SBOM real depois do fato; o zip Windows requer
    um re-run completo do workflow.

## [Não publicado]

### Corrigido
- **CI: exit 101 `crate already exists` no job `Publish to crates.io` (post-mortem 2026-06-05)**
  - Causa raiz: trigger duplicado do workflow para tag v0.6.6 já publicada causou `cargo publish`
    exit 101 com `error: crate duckduckgo-search-cli@0.6.6 already exists on crates.io index`.
    crates.io é append-only immutable, versões NUNCA podem ser sobrescritas.
  - Solução: adicionados jobs `preflight` + `crates_io` com guards de:
    - Consistência de versão Tag vs Cargo.toml
    - Validação de formato SemVer
    - Presença de entrada no CHANGELOG
    - Bloqueio de Co-authored-by de agentes IA em commits recentes
    - `cargo search` com timeout + retry para detectar versão já publicada
    - Skip de `cargo publish` com warning + upload de evidência quando já publicada
    - Timeout (300s) + retry (3 tentativas, backoff 10s/20s/30s) no `cargo publish`
  - Padrão de resolução: workflow de release idempotente com caminho de skip explícito

- **CI: 18+ warnings de Node.js 20 deprecated em todos os jobs**
  - Causa raiz: actions/checkout@v4, actions/upload-artifact@v4, actions/download-artifact@v4
    usam Node 20. Node 20 descontinuado em 19/09/2025, removido em 16/09/2026.
  - Solução:
    - Atualizadas todas as actions para v6 (Node 24 nativo)
    - Atualizado `softprops/action-gh-release` de v2 para v3
    - Adicionado `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"` como cinto-e-suspensórios
  - Caminho de migração: v6 é Node 24 nativo, v4 precisa de env var explícita

- **CI: exit 141 SIGPIPE intermitente em `validate (ubuntu-latest)`**
  - Causa raiz: `cargo test` escreve em pipe cujo consumidor fecha cedo
  - Solução: guard explícito `|| { ec=$?; if [ $ec -eq 141 ]; then exit 0; fi; exit $ec; }`
  - Trade-off: 141 vira warning silenciosamente, pode mascarar bugs reais em testes

- **CI: exit 1 em `validate (windows-latest)` por redirect VS2022→VS2026**
  - Causa raiz: GitHub redireciona `windows-latest` para `windows-2025-vs2026` desde 15/06/2025.
    VS2026 tem mudanças breaking no toolchain MSVC que afetam Rust stable.
  - Solução: pinado `windows-2022` na matrix `ci.yml` e no target de build `release.yml`
  - Reavaliar pin após 15/07/2026 quando VS2026 estabilizar

### Adicionado
- **Geração de SBOM CycloneDX no workflow de release** — `cargo cyclonedx --format json` produz
  `sbom.cdx.json` enviado como artifact. Habilita compliance com EU Cyber Resilience Act.
- **Atestado de proveniência SLSA** — `actions/attest-build-provenance@v2` cria proveniência
  assinada para todos os artefatos de release. Compliance SLSA Nível 3.
- **Assinatura cosign keyless OIDC** — cada binário + SHA256SUMS.txt assinado com `cosign sign-blob`
  usando token OIDC do GitHub. Sem gerenciamento de chave privada.
- **SHA256SUMS publicado em cada release** — `sha256sum` gerado por target, combinado em
  único `SHA256SUMS.txt`, enviado como asset de release e como parte de cada tarball/zip binário.
- **Assinatura GPG de tag** — `gpg --detach-sign SHA256SUMS.txt` opcional se secret
  `GPG_PRIVATE_KEY` estiver configurada. `continue-on-error: true` para não bloquear release
  se chave faltar.
- **Controle de concorrência** — `concurrency.group: release-${{ github.ref }}-${{ github.sha }}`
  impede runs paralelos para mesma tag+SHA. `cancel-in-progress: false` (release) / condicional
  em PR (CI) garante que publish nunca é abortado no meio.
- **Job pre-flight no workflow de release** — valida versão da tag == versão do Cargo.toml,
  formato SemVer, entrada no CHANGELOG, ausência de Co-authored-by de agente IA ANTES de qualquer build.
- **Atualização semanal Cron de dependências** — job `scheduled_update` roda domingo 03:00 UTC,
  executa `cargo update --workspace`, cria PR se houver mudanças.
- **Scan de segurança Zizmor** — análise estática de workflows GitHub Actions detecta
  injeção, input não confiável e outros anti-patterns de segurança. Roda apenas em PRs.
- **Validação de sintaxe Actionlint** — valida sintaxe YAML de todos os arquivos de workflow. Roda apenas em PRs.
- **Dependabot para actions e crates** — `.github/dependabot.yml` cria PRs semanais
  para atualizações de GitHub Actions e crates Rust. Agrupa por major/minor/patch.
- **Normalização LF via `.gitattributes`** — força line endings LF em todos os arquivos de texto,
  prevenindo problemas de CRLF em Windows que quebram `cargo fmt --check`.

### Segurança
- **Permissões endurecidas por job** — top-level `permissions: contents: write packages: write
  id-token: write attestations: write checks: write discussions: write` para release;
  blocos `permissions:` por job no CI para menor privilégio.
- **`continue-on-error: true` no step GPG** — chave GPG ausente não bloqueia release;
  melhoria opcional.
- **Sem triggers `pull_request_target`** — workflows nunca rodam com permissões de escrita
  em PRs de forks.

## [0.6.8] - 2026-06-05

### Corrigido
- **CI: exit 127 `jaq: command not found` no job `github_release` do workflow de release**
  - Causa raiz: `release.yml` (linhas 625-626) usava `jaq` (binário Rust) para parsear
    JSON de resposta da GitHub REST API, mas o runner Ubuntu 24.04 do GitHub Actions
    só tem `jq 1.7` pré-instalado — `jaq` não faz parte da imagem padrão do runner.
    Bug introduzido pelo commit `7f489b5` (2026-06-05) ao fazer bypass do action
    `softprops/action-gh-release` que estava bugado.
  - Solução: substituído `jaq` por `jq` (pré-instalado, sintaxe compatível) e adicionada
    validação fail-fast explícita para os valores extraídos de `UPLOAD_URL` e
    `RELEASE_ID` para emitir mensagens diagnósticas claras em respostas malformadas.
  - Referência: <https://github.com/actions/runner-images/blob/main/images/ubuntu/
    Ubuntu2404-Readme.md> (seção Tools lista `jq 1.7`, `jaq` está ausente)

## [0.6.7] - 2026-06-05

### Corrigido
- **CI: post-mortem completo do incident-publish-101-2026-06-05** (hardening do pipeline de release)
  - Adicionado job `preflight` validando tag==Cargo.toml, SemVer, CHANGELOG, ausência de Co-authored-by de agentes IA
  - Adicionado guard contra versão duplicada no job `crates_io`
  - cargo publish com timeout 300s + 3 retries (resiliência a network)
  - Concurrency group por tag+sha (impede runs paralelos)
- **CI: 18+ warnings de Node.js 20 deprecated**
  - Atualizadas actions para v6 (Node 24 nativo)
  - Atualizado softprops/action-gh-release v2 → v3
  - Adicionado `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` como cinto-e-suspensórios
- **CI: zizmor security scan: 134 findings → 0**
  - SHA pinning para 11 actions (unpinned-uses)
  - per-job least-privilege permissions (excessive-permissions)
  - comments + inline trailing em todas as permissions
  - secrets em env: job-level + GitHub Environments dedicados
  - ${{ ... }} em run: mitigados via env vars (template-injection)
  - dtolnay/rust-toolchain substituído por setup via rustup (superfluous-actions)
  - caches removidos do release.yml (cache-poisoning)
- **CI: actionlint 0 erros em ambos workflows**
- **CI: zizmor zero findings (exit 0)**
- **CI: dependabot.yml para auto-update semanal**
- **CI: .gitattributes força LF line endings**
- **clippy: `#[cfg(feature = "chrome")]` redundante removido de src/lib.rs:74**
  - browser.rs:25 já cobre o módulo
- **clippy: SAFETY comments adicionados em todos os Windows unsafe blocks em src/platform.rs**
  - 5 blocos unsafe agora têm `// SAFETY:` comments
- **test: tests incompatíveis com Windows marcados com `#[cfg(unix)]`**
  - `rejeita_path_absoluto_etc` e `rejeita_path_absoluto_usr`

### Adicionado
- **Geração de SBOM CycloneDX em release workflow**
  - `cargo cyclonedx --format json` produz `sbom.cdx.json`
- **SLSA build provenance via `actions/attest-build-provenance@v2`**
- **cosign keyless OIDC signing** (todos os binários + SHA256SUMS.txt)
- **SHA256SUMS publicado com cada release**
- **GPG tag signing** (opcional, `continue-on-error: true` se chave ausente)
- **Pre-flight job em release workflow** (9 gates + 1 dry-run)
- **Attestation job** (SBOM + cosign + SLSA em 1 job)
- **scheduled_update Cron semanal** (cargo update automático)
- **Zizmor security scan em CI**
- **Actionlint syntax check em CI**
- **Dependabot para actions e Rust crates**

### Segurança
- **Permissions endurecidas per-job** (least-privilege)
- **Persist-credentials: false em 18/18 actions/checkout** (artipacked)
- **Sem triggers `pull_request_target`**
- **SHA pinning completo** (11 actions)

## [0.6.6] - 2026-06-05

### Corrigido
- **Falha de build no docs.rs (Build #3487310) causada por `#[doc(cfg(...))]` tornando-se instável**
  - Removido `#[cfg_attr(docsrs, doc(cfg(feature = "chrome")))]` de `src/lib.rs:70`
  - Causa raiz: em Out 2025 o time Rust mergeou `doc_auto_cfg` em `doc_cfg` (rust-lang/rust#43781), fazendo `#[doc(cfg(...))]` exigir `#![feature(doc_cfg)]` (somente nightly) na raiz da crate. O build falhou com `error[E0658]: #[doc(cfg)] is experimental` no nightly `1.98.0`.
  - O feature gating em si está preservado: `#[cfg(feature = "chrome")]` ainda exclui `pub mod browser` de builds default. O docstring em nível de módulo em `src/browser.rs` já documenta o requisito da feature explicitamente.
  - `cargo doc --all-features` e `RUSTDOCFLAGS="--cfg docsrs" cargo doc --all-features` ambos passam sem warning ou erro.

## [0.6.5] - 2026-06-05

### Corrigido
- **MP-26 — Cast de HANDLE Windows quebrado em `windows-sys 0.59+`** (`src/platform.rs:51-63`)
  - `HANDLE` mudou de `isize` para `*mut c_void` no upstream (`microsoft/windows-rs`, `raw-window-handle#171`)
  - Substituído `handle != 0 && handle != usize::MAX` por `!handle.is_null() && handle != INVALID_HANDLE_VALUE`
  - Removidos casts inválidos `handle as isize` (a assinatura moderna aceita `HANDLE` direto)
  - Comentário `// SAFETY:` atualizado para documentar nulidade e sentinela Win32
- **CI: `validate` falhava em todos os 3 SOs** (Linux/macOS/Windows) por 6 erros de clippy
  - 3 erros `clippy::doc_markdown` (`PowerShell`, `rules_rust.md`, `TempDir`) em `src/platform.rs` e `src/browser.rs`
  - 1 erro `clippy::needless_return` em `src/browser.rs:149`
  - 2 erros `missing_debug_implementations` em `src/browser.rs:223` e `src/content_fetch.rs`

### Adicionado
- **WS-11 — Invariantes property-based para parsers HTML** (`src/extraction.rs` +5 testes)
  - Invariante: inputs vazios/quebrados retornam `Vec` vazio sem panic
  - Invariante: positions são densos e 1-based
  - Invariante: URLs absolutos (`http`/`https`) ou vazios
  - Invariante: extração é idempotente
  - Invariante: HTML malformado não causa panic
  - **Zero dependência nova** (apenas stdlib + `#[test]`)
- **WS-12 — Circuit breaker per-host** (`src/content_fetch.rs`)
  - Threshold: 3 falhas consecutivas abrem o circuito
  - Cooldown: 30s antes de half-open probe
  - Integração em `enrich_with_content` antes de cada fetch
  - `BreakerDecision::{Allow, Reject}` para inspeção
  - **Zero dependência nova** (`std::sync::Mutex<HashMap>`)
- **WS-23 — Teste `Retry-After` header** (`tests/integration_wiremock.rs`)
  - Mock retorna 429 com `retry-after: 2`
  - Asserção: `elapsed_ms >= 1500` (delay mínimo respeitado)
  - Usa `wiremock` 0.6 já em dev-deps
- **WS-25 — `indicatif` ProgressBar para crawls longos** (`src/content_fetch.rs`)
  - `indicatif = "0.18"` adicionado
  - Template `[{elapsed_precise}] {bar:40.cyan/blue} {pos:>4}/{len:4} {msg}`
  - Auto-detecta TTY (esconde em pipes)
  - `progress.finish_and_clear()` ao final
- **Lints preventivos FFI** (`Cargo.toml`)
  - `improper_ctypes = "deny"` (rejeita casts FFI inválidos)
  - `improper_ctypes_definitions = "deny"` (rejeita definições incorretas)

### Testes
- 333 testes passando (243 lib + 24 + 3 + 5 + 10 + 10 + 14 + 18 + 6 doc)
- 6 novos testes de invariantes em `extraction.rs` (WS-11)
- 4 novos testes de circuit breaker em `content_fetch.rs` (WS-12)
- 1 novo teste de Retry-After em `integration_wiremock.rs` (WS-23)
- `cargo fmt --all --check` limpo
- `cargo clippy --all-targets --all-features --locked -- -D warnings` limpo
- `cargo publish --dry-run --locked --allow-dirty` limpo

## [0.6.4] - 2026-06-03

### Adicionado
- **WS-26 — Rotação adaptativa de identidades anti-bot** (novo módulo `src/identity.rs`)
  - Pool de 12 identidades (4 famílias de browser × 3 plataformas) para rotação adaptativa
  - `IdentityProfile::shuffled_headers()` produz ordem de headers determinística via seed
  - `IdentityPool::rotate_on_block()` implementa cascata de 5 níveis: mesma identidade → mesma família/plataforma diferente → família diferente/mesma plataforma → família+plataforma diferentes → aleatória
  - Enums `BrowserFamily` e `Platform` com nomes canônicos em inglês
  - 5 testes unitários cobrindo tamanho do pool, nível de cascata, determinismo, formato de headers, estabilidade da tag
- **Novas flags CLI** (aditivas, sem breaking changes)
  - `--probe` — verificação de saúde pré-voo (envia 1 requisição mínima, reporta status/latência/Set-Cookie como JSON)
  - `--identity-profile` — fixa a sessão em uma identidade específica (`auto`, `chrome-win`, `chrome-mac`, `chrome-linux`, `edge-win`, `firefox-linux`, `safari-mac`). `auto` é o padrão.
- **Novos campos JSON de metadados** (aditivos, `Option` + `skip_serializing_if = "Option::is_none"`)
  - `metadados.identidade_usada` — tag textual da identidade que produziu a resposta
  - `metadados.nivel_cascata` — nível de cascata atingido durante a requisição
### Alterado
- **Reversão de versão**: `0.7.0` (não publicado) → `0.6.4` para preservar o conjunto de features em desenvolvimento sob um número de patch estável
- Todas as flags CLI, schemas JSON de saída e exit codes permanecem inalterados — mudanças estritamente aditivas
### Testes
- 5 novos testes unitários de identidade (313 testes totais passando, de 308)
- Todos os 224 testes lib + 83 testes de integração + 6 doc tests passam
- `cargo clippy --lib --bins -- -D warnings` limpo
- `cargo fmt --check` limpo

## [0.6.3] - 2026-04-17

### Alterado
- Tradução de todos os 96 doc comments (`///` e `//!`) em 19 arquivos-fonte de português para inglês — docs.rs agora exibe documentação completamente em inglês para o público internacional do crates.io.
- Nenhuma alteração de comportamento, API pública ou campos JSON de saída.

## [0.6.2] - 2026-04-17

### Adicionado
- 19 novos arquivos de documentação — conformidade completa com rules_rust_documentacao.md (28 gaps G01-G28)
- Documentação bilíngue EN+PT: HOW_TO_USE, CROSS_PLATFORM, AGENTS-GUIDE, COOKBOOK.pt-BR, INTEGRATIONS.pt-BR
- CODE_OF_CONDUCT.md + CODE_OF_CONDUCT.pt-BR.md — Contributor Covenant 2.1
- README.pt-BR.md, CHANGELOG.pt-BR.md, CONTRIBUTING.pt-BR.md, SECURITY.pt-BR.md
- docs/AGENTS.pt-BR.md — guia imperativo para LLMs em português
- docs/AGENTS-GUIDE.md + docs/AGENTS-GUIDE.pt-BR.md — guia persuasivo bilíngue
- llms.txt — arquivo compacto de orientação para LLMs (< 50 KB)
- llms-full.txt — concatenação completa de docs para contexto longo de LLMs
- eval-queries.json × 2 — 20 queries de avaliação EN + 20 PT-BR para skill testing
### Alterado
- README.md — link para README.pt-BR.md + quick install antes da linha 30
- CONTRIBUTING.md — MSRV Rust 1.75 explícito + PR checklist 8 itens + branching strategy + nextest
- SECURITY.md — tabela de versão específica v0.6.2 + política de embargo 90 dias + zero bold + zero emojis
- skill/SKILL.md (EN+PT) — seção Workflow com 5 passos numerados verificáveis

## [0.6.1] - 2026-04-17

### Corrigido
- `--timeout 0` agora retorna exit 2 (configuração inválida) em vez de executar busca com timeout zero e retornar exit 5
- `--output /tmp/../../etc/passwd` agora retorna exit 2 (configuração inválida) em vez de exit 1 — validação de path traversal movida para `montar_configuracoes()`, antes do início do pipeline
### Adicionado
- Método `validar_timeout_segundos()` em `CliArgs` — rejeita valores 0 com mensagem de erro descritiva
- Verificação antecipada de path traversal em `montar_configuracoes()` — chama `paths::validate_output_path()` no momento de validação da configuração, não no momento de escrita
- 2 testes E2E de regressão: `timeout_zero_retorna_exit_2` e `output_com_path_traversal_retorna_exit_2`
- 1 teste unitário: `validar_timeout_segundos_rejeita_zero`

## [0.6.0] - 2026-04-16

### Segurança
- Perfis de fingerprint de browser por família previnem detecção anti-bot do DuckDuckGo
- Headers `Sec-Fetch-*` e Client Hints por família imitam sessão de navegador real
- `Accept-Language` com q-values RFC 7231 elimina fingerprint de UA genérico
- Detecção de bloqueio silencioso com limiar de 5 KB previne resultados truncados
### Adicionado
- Enum `BrowserFamily` — variantes `Chrome`, `Firefox`, `Edge`, `Safari`
- Struct `BrowserProfile` — encapsula família, versão e conjunto de headers por família
- Headers `Sec-Fetch-Dest`, `Sec-Fetch-Mode`, `Sec-Fetch-Site` por família em `http.rs`
- Client Hints (`Sec-Ch-Ua`, `Sec-Ch-Ua-Mobile`, `Sec-Ch-Ua-Platform`) para Chrome e Edge
- Detecção de anomalia HTTP 202 em `search.rs` com backoff exponencial automático
- Detecção de bloqueio silencioso — resposta com menos de 5 000 bytes é tratada como bloqueio
- `BrowserProfile` propagado via `Config` para todos os módulos do pipeline
- Headers de paginação com `Sec-Fetch-Site: same-origin` para imitar navegação real
### Alterado
- `Accept-Language` atualizado para `pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7` conforme RFC 7231
- Header `Accept` agora reflete o perfil completo do browser por família
- Delays de paginação aumentados de 500–1 000 ms para 800–1 500 ms
- Limiar de bloqueio silencioso aumentado de 100 para 5 000 bytes

## [0.5.0] - 2026-04-16

### Segurança
- Validação de path traversal em `--output` — rejeita componentes `..` e escritas em diretórios de sistema (`/etc`, `/usr`, `C:\Windows`)
- Mascaramento de credenciais de proxy — mensagens de erro não expõem mais senhas de URLs `--proxy http://user:pass@host`
### Adicionado
- `src/paths.rs` — validação centralizada de caminhos, criação de diretório pai e aplicação de permissões Unix
- `src/signals.rs` — restauração centralizada de SIGPIPE (Unix) e handler Ctrl+C/SIGINT (cross-platform)
- Enum `ErroCliDdg` com `thiserror` — 11 variantes de erro tipadas com métodos `exit_code()` e `codigo_erro()`
- `mascarar_url_proxy()` em `http.rs` — remove credenciais de URLs de proxy no contexto de erro
- 21 novos testes unitários em `paths.rs`, `signals.rs`, `error.rs` e `http.rs`
### Alterado
- `thiserror = "2"` adicionado às dependências para erros de domínio estruturados
- `src/main.rs` reduzido de 63 para 23 linhas — tratamento de sinais extraído para `signals.rs`
- Escritas de arquivo em `src/output.rs` agora validam caminhos via `paths::validate_output_path()` antes do I/O
- `deny.toml` atualizado com exceção RUSTSEC-2026-0097 (rand 0.8 unsound com logger customizado — não aplicável)

## [0.4.4] - 2026-04-16

### Corrigido
- SIGPIPE restaurado para SIG_DFL no Unix — pipes para `jaq`, `head` e outros consumidores não perdem mais stdout silenciosamente
- Erros BrokenPipe detectados na cadeia anyhow e tratados como exit 0 (não exit 1) em todos os pontos de saída
### Adicionado
- `--help` agora exibe seções EXIT CODES (0–5) e PIPE USAGE via `after_long_help`
- 3 testes E2E para regressão de pipe: exit codes no help, exclusão do help curto, contagem de bytes no stdout
- Item 7 no troubleshooting do README: "Pipe para jaq/jq retorna vazio" com diagnóstico PIPESTATUS (EN + PT)
- `docs_rules/rules_rust.md`: SIGPIPE + BrokenPipe adicionados ao checklist de I/O
- `docs/AGENT_RULES.md`: regra R24 de segurança de pipe com diagnóstico PIPESTATUS
- `docs/COOKBOOK.md`: Receita 16 de diagnóstico de pipe (EN + PT)
- `docs/INTEGRATIONS.md`: cláusula de segurança de pipe no contrato base
- Seção de ramificação por exit code em ambos os arquivos de skill (EN + PT)

## [0.4.3] - 2026-04-15

### Alterado
- `README.md` — nova seção persuasiva "Agent Skill" (EN + PT) posicionada entre a tabela de agentes e a seção de Documentação, no pico de atenção do leitor
- Copywriting AIDA destacando a skill bilíngue empacotada em `skill/`: auto-ativação semântica sem slash command, 14 seções canônicas MUST/NEVER, contrato JSON anti-alucinação, economia de tokens em cada turno de busca, instalação em um comando (`git clone` + `cp -r`)
- Benefícios explícitos para LLMs (decisão automática de quando buscar) e desenvolvedores (zero prompt engineering, zero registro de ferramenta)
- Tarball do crates.io inalterado — skills continuam vivendo apenas no GitHub

## [0.4.2] - 2026-04-15

### Adicionado
- `skill/duckduckgo-search-cli-pt/SKILL.md` e `skill/duckduckgo-search-cli-en/SKILL.md` — Skills bilíngues para Claude Code, Claude Agent SDK e plataformas compatíveis com Agent Skills
- Cada skill traz frontmatter YAML com `name` único por idioma e `description` carregado de triggers semânticos para auto-invocação
- 14 seções H2 canônicas: Missão, Contrato de Invocação, Proibições Absolutas, Parsing com `jaq`, Schema JSON, Exit Codes, Batch, Fetch-Content, Endpoint, Retries, Receitas, Validação, Memória, Regra de Ouro
- Skills publicadas no GitHub, excluídas do tarball do crates.io
### Alterado
- `docs/AGENT_RULES.md` (833 linhas, +7,6%) — reescrita editorial aplicando copywriting AIDA: cada regra abre com benefício mensurável, linguagem imperativa MUST/NEVER reforçada, zero narrativa decorativa, zero negrito com asteriscos duplos, zero separador visual `---` entre seções
- `docs/COOKBOOK.md` (1082 linhas, −3,1%) — cada receita abre com o ganho concreto antes do comando, bullets curtos de 8 a 15 palavras, pipelines `jaq` + `xh` + `sd` preservados intactos
- `docs/INTEGRATIONS.md` (1212 linhas, +1,3%) — 16 agentes com tabela comparativa textual, snippets determinísticos por agente, zero emoji
### Meta
- `Cargo.toml` exclude ampliado para cobrir `skill/` e `skill/**` — skills ficam no GitHub e fora do tarball publicado no crates.io

## [0.4.1] - 2026-04-14

### Adicionado
- `docs/AGENT_RULES.md` (773 linhas) — regras imperativas bilíngue (EN+PT) com 30+ rules `MUST`/`NEVER` (R01..R30) para LLMs/agentes invocarem a CLI em produção
- Cobre invariantes core, contrato JSON, rate limiting, tratamento de erros, performance, segurança e anti-patterns
- Quick Reference Card ao final do documento
- `docs/COOKBOOK.md` (1117 linhas) — 15 receitas copy-paste bilíngue combinando `duckduckgo-search-cli` + `jaq` + `xh` + `sd` para casos reais
- Casos cobertos: research consolidado, ETL multi-query, extração de domínios, monitoramento com filtro temporal, content extraction com `--fetch-content`, comparação top 5 vs top 15, NDJSON para pipelines, function wrappers para bash
- `docs/INTEGRATIONS.md` (1196 linhas) — snippets prontos para 16 agentes/LLMs: Claude Code, OpenAI Codex, Gemini CLI, Cursor, Windsurf, Aider, Continue.dev, MiniMax, OpenCode, Paperclip, OpenClaw, Google Antigravity, GitHub Copilot CLI, Devin, Cline, Roo Code
- Cada agente documenta: pitch, mecanismo de shell, setup, snippet básico, snippet multi-query, regra de system prompt e ressalvas
- Seção Documentation no `README.md` (EN + PT) linkando os 3 guias
### Corrigido
- Cluster de badges e referências internas do `README.md` conferidas contra `daniloaguiarbr/duckduckgo-search-cli` (repo canônico)

## [0.4.0] - 2026-04-14

### Alterado (BREAKING)
- Default de `--num` / `-n` alterado de "todos os resultados da primeira página" (~11) para 15, com auto-paginação automática
- Quando o número efetivo excede 10, o binário busca 2 páginas por query para satisfazer o teto solicitado, desde que `--pages` não tenha sido customizado
- Auto-paginação: se `--num > 10` E `--pages` não foi customizado, o binário auto-eleva `--pages` para `ceil(num/10)` respeitando o teto de 5 páginas
- Impacto: mais requests por query (2x no caso default) e latência marginalmente maior, com cobertura completa dos resultados solicitados
### Adicionado
- Documentação no comentário do flag `--num` em `cli.rs` descrevendo a nova semântica de default e auto-paginação
- 4 novos testes unitários em `lib.rs::testes`: `montar_configuracoes_aplica_default_num_15_quando_omitido`, `montar_configuracoes_respeita_pages_explicito_acima_de_1`, `montar_configuracoes_auto_pagina_quando_num_maior_que_10`, `montar_configuracoes_nao_auto_pagina_quando_num_10_ou_menos`
- 2 novos testes wiremock em `tests/integracao_wiremock.rs`: `testa_default_num_15_auto_pagina_2_paginas`, `testa_auto_paginacao_respeita_pages_explicito`
### Guia de Migração
- Para preservar o comportamento antigo (1 página, ~11 resultados): passe `--pages 1 --num 10` explicitamente
- Quem já passava `--num 5` (ou qualquer valor <= 10): comportamento inalterado (sem auto-paginação, 1 página)
- Quem já passava `--num 20 --pages 2` ou similar: comportamento inalterado (respeita explícito do usuário)
- Quem confiava no default sem flags: agora recebe até 15 resultados em vez de ~11, com 1 request extra por query

## [0.3.0] - 2026-04-14

### Alterado (BREAKING)
- Campo `buscas_relacionadas` REMOVIDO de `SearchOutput` e `MultiSearchOutput.buscas[i]` — o endpoint `html.duckduckgo.com/html/` não expõe related searches no DOM atual; manter o campo sempre vazio era ruído
- Pipelines que parseavam `.buscas_relacionadas` precisam de ajuste
- Pool de User-Agents: removidos UAs de browsers de texto (`Lynx 2.9.0`, `w3m/0.5.3`, `Links 2.29`, `ELinks 0.16.1.1`) que faziam o DuckDuckGo retornar HTML degradado
- Substituídos por 6 UAs modernos validados empiricamente contra o endpoint `/html/`: Chrome 146 (Win/Mac/Linux), Edge 145 Windows, Firefox 134 Linux, Safari 17.6 macOS
- Firefox Win/Mac foram REMOVIDOS após retornarem anomalia HTTP 202 em validação real (heurística anti-bot do DDG)
### Corrigido
- Snippet duplicava título e URL no início: o seletor padrão tinha fallback `.result__body` (container pai) que fazia `text()` recursivo capturar título+URL+snippet concatenados — trocado por `.result__snippet` puro
- Pipelines como `jaq '.resultados[].snippet'` agora retornam apenas o texto descritivo do resultado
- Título "Official site": o DuckDuckGo renderiza literalmente este texto como label para domínios verificados — o scraper agora detecta este caso e substitui pelo `url_exibicao`
- O texto original é preservado no novo campo opcional `titulo_original` para auditoria
### Adicionado
- Campo `titulo_original: Option<String>` em `SearchResult` — presente apenas quando o título foi substituído por heurística
- Serializado com `#[serde(skip_serializing_if = "Option::is_none")]` — não aparece no JSON quando ausente
- Resultados patrocinados (`.result--ad`) excluídos do container default via seletor `.result:not(.result--ad)`
### Removido
- Função `extrair_buscas_relacionadas` em `src/search.rs` (dead code com seletor hardcoded que nunca encontrava nada)
- Seção `[related_searches]` nos seletores default
### Guia de Migração (v0.2.x → v0.3.0)
- Pipelines `jaq '.buscas_relacionadas[]'`: campo não existe mais — remover do filtro ou tratar `null`
- Esperando snippet com prefixo título+URL? Agora vem só o texto descritivo — ajuste regex/parsing downstream se necessário
- Confiando em `titulo == "Official site"` para detectar sites verificados? Use `titulo_original.as_deref() == Some("Official site")`
- CONFIG EXTERNO LEGADO: usuários que rodaram `init-config` em versões anteriores possuem `~/.config/duckduckgo-search-cli/{selectors,user-agents}.toml` com defaults antigos — execute `duckduckgo-search-cli init-config --force` para aplicar as correções

## [0.2.0] - 2026-04-14

### Alterado (BREAKING)
- Schema JSON serializado agora usa nomes de campo em português brasileiro, alinhado com os exemplos `jaq` do README e com o invariante INVIOLÁVEL do blueprint v2 do projeto
- Pipelines que dependiam do schema em inglês da v0.1.0 precisam atualizar os seletores `jaq`
- Tabela de renomeações de campos:

| Antes (v0.1.0) | Depois (v0.2.0) |
|----------------|-----------------|
| `position` | `posicao` |
| `title` | `titulo` |
| `displayed_url` | `url_exibicao` |
| `content` | `conteudo` |
| `content_length` | `tamanho_conteudo` |
| `content_extraction_method` | `metodo_extracao_conteudo` |
| `execution_time_ms` | `tempo_execucao_ms` |
| `selectors_hash` | `hash_seletores` |
| `retries` | `retentativas` |
| `fallback_endpoint_used` | `usou_endpoint_fallback` |
| `concurrent_fetches` | `fetches_simultaneos` |
| `fetch_successes` | `sucessos_fetch` |
| `fetch_failures` | `falhas_fetch` |
| `chrome_used` | `usou_chrome` |
| `proxy_used` | `usou_proxy` |
| `engine` | `motor` |
| `region` | `regiao` |
| `results_count` | `quantidade_resultados` |
| `results` | `resultados` |
| `related_searches` | `buscas_relacionadas` |
| `pages_fetched` | `paginas_buscadas` |
| `error` | `erro` |
| `message` | `mensagem` |
| `metadata` | `metadados` |
| `queries_count` | `quantidade_queries` |
| `parallel` | `paralelismo` |
| `searches` | `buscas` |

- Campos inalterados: `url`, `snippet`, `query`, `endpoint`, `timestamp`, `user_agent`
### Corrigido
- Pipelines documentados no README (`jaq '.resultados[].titulo'`, etc.) agora funcionam end-to-end — em v0.1.0 retornavam `null` por divergência do schema (bug reportado pelo usuário)

## [0.1.0] - 2026-04-14

### Adicionado
- Pipeline de busca core contra o endpoint HTML do DuckDuckGo via HTTP puro (`html.duckduckgo.com/html/`)
- Fallback para endpoint lite via `--endpoint lite` para páginas sem JavaScript
- Modo multi-query com deduplicação automática, args posicionais, `--queries-file` e stdin
- Fan-out paralelo de queries com `--parallel` (1..=20), limitado por `tokio::JoinSet` + `Semaphore`
- `--pages` (1..=5) para coletar múltiplas páginas de resultado por query
- `--fetch-content` busca cada URL de resultado via HTTP puro, aplica readability e embute o texto limpo no JSON
- `--max-content-length` (1..=100 000) trunca conteúdo extraído respeitando fronteiras de palavras
- Fallback Chrome headless via `--features chrome` com detecção cross-platform e flags de stealth
- Flag `--chrome-path` para especificar manualmente o executável Chrome/Chromium
- `--proxy URL` + `--no-proxy` (HTTP/HTTPS/SOCKS5) com precedência sobre variáveis de ambiente
- `--global-timeout` (1..=3600 s) envolve todo o pipeline em `tokio::time::timeout`
- `--per-host-limit` (1..=10) limita fetches por host via mapa de `Semaphore` por host
- `--match-platform-ua` restringe o pool de user-agents à plataforma atual
- Modo NDJSON `--stream` emite um resultado por linha conforme extraídos
- Quatro formatos de saída: `json` (padrão), `text`, `markdown`, `auto` (TTY-aware)
- Arquivos de configuração externos: `selectors.toml` e `user-agents.toml` no diretório XDG config, sobrescrevendo defaults embutidos
- Subcommand `init-config` com `--force` e `--dry-run` para inicializar arquivos de configuração do usuário
- Exit codes: `0` sucesso, `1` runtime, `2` config, `3` bloqueio (anomalia HTTP 202), `4` timeout global, `5` zero resultados
- Inicialização de console UTF-8 no Windows via `SetConsoleOutputCP(65001)`
- Rustls-TLS em toda a CLI para builds cross-platform sem dependências adicionais
- `tracing` + `tracing-subscriber` com `RUST_LOG` respeitado; flags `--verbose` / `--quiet`
- 163 testes unitários + integração cobrindo parsing CLI, montagem de config, extração HTTP, fan-out paralelo, seletores e fluxos de busca via wiremock
### Segurança
- Todas as credenciais (`--proxy user:pass@host`) são mascaradas nos logs
- Criação de arquivo de saída aplica permissões Unix `0o644`

[Unreleased]: https://github.com/comandoaguiar/duckduckgo-search-cli/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/comandoaguiar/duckduckgo-search-cli/releases/tag/v0.1.0
