sqlite-graphrag 1.1.6

Persistent GraphRAG memory for Claude Code, Codex, Cursor, and 27 AI agents — one self-contained ~19 MiB Rust binary, zero daemon. Never re-explain your codebase again. Hybrid retrieval (FTS5 BM25 + cosine similarity + multi-hop graph traversal) surfaces the right memory in milliseconds. Embedding and entity enrichment run as parallel REST calls against your cloud LLM — no fragile headless subprocesses, no ONNX runtime, no model downloads. Soft-delete with full version history, transactional atomic writes, BLAKE3-tracked mutations. OAuth-only: raw API keys ABORT the spawn.
Documentation
# Plano de Testes


- Leia a versão em inglês em [TEST_PLAN.md]TEST_PLAN.md
- Guia complementar: [TESTING.pt-BR.md]TESTING.pt-BR.md documenta a infraestrutura de cada camada
- Criado durante a auditoria pós-publicação da v1.0.79 em 2026-06-11 (gaps G46-G54)


## Objetivos e Escopo
### Por Que Este Plano Existe
- O G43 provou que suítes fora do caminho default do CI escondem quebras por ciclos inteiros de release
- O G50 provou que doctests rodam SOMENTE no CI, e um exemplo rustdoc quebrado foi publicado em 10 releases
- O artefato publicado no crates.io nunca era exercitado diretamente antes deste plano
- Este plano torna cada camada explícita: o que roda, quando, com qual comando e o que significa passar
### Escopo
- Cobre o crate sqlite-graphrag: testes unitários da lib, integração da CLI, contratos, concorrência, benchmarks e auditoria pós-publicação
- Exclui teste exploratório manual e projetos consumidores downstream


## Gate de regressão v1.1.06 (scan O(k) do entity-connect)
- Comando: `/usr/bin/timeout 300 cargo test --test v1106_entity_connect_scan_regression`
- Escopo: cobertura na fronteira da CLI + unitários para GAP-ENTITY-CONNECT-SCAN-CARTESIAN fechado na v1.1.06
- **Sem migração de schema** nesta release: `CURRENT_SCHEMA_VERSION` permanece **16**
- Scan O(k) de pares: coocorrência + hub×ilha (nunca cartesiano `entities × entities` com ORDER BY global)
- Chaves de fila `pair:{id1}:{id2}`, `item_type=entity_pair`; drain por PK sem re-scan
- Deadline no primeiro scan: InterruptHandle / `--max-runtime` / teto soft 120s → Timeout exit 1 (não 75)
- NDJSON: `scan_start` antes do SQL (`operation`, `entities_in_namespace`, `backlog_degree0_proxy`); `scan_meta` (`pairs_enqueued_this_scan`)
- `cross-domain-bridges` compartilha o mesmo path de scan; GAP-002 `entity_connect_seen` preservado
- Critério: ZERO falhas
- Docs complementares: [TESTING.pt-BR.md]TESTING.pt-BR.md, [ADR-0066]decisions/adr-0066-v1-1-06-entity-connect-scan.pt-BR.md, arquivo da suite [`tests/v1106_entity_connect_scan_regression.rs`]../tests/v1106_entity_connect_scan_regression.rs
- Também rode unitários do enrich: `/usr/bin/timeout 300 cargo test --lib commands::enrich`


## Gate de regressão v1.1.05 (incidente danilo)
- Comando: `/usr/bin/timeout 300 cargo test --test v1105_danilo_bugs_regression`
- Escopo: cobertura na fronteira da CLI dos cinco bugs de operador fechados na v1.1.05
- Bug 1: `deep-research` de token único emite fan-out com `source: "aspect"`; caminho manual opcional `--sub-query-strategy manual --sub-queries-file PATH` (smoke de operador, não é caso separado da suite)
- Bug 2: `deep-research --output` grava JSON atômico via **atomwrite** e devolve ack no stdout (`written`, `bytes`, `blake3`, …); **`--quiet`** global acompanha o contrato de stderr
- Bug 3: `graph traverse` com nome curto sugere ou resolve com `--fuzzy`
- Bug 4: `merge-entities` rejeita `--ids`/`--into-id` auto-referenciais antes do DB
- Bug 5: `link` rejeita nomes puramente numéricos e aceita `--from-id`/`--to-id`
- Critério: ZERO falhas (5 testes)
- Docs complementares: [TESTING.pt-BR.md]TESTING.pt-BR.md, [ADR-0065]decisions/adr-0065-v1-1-05-danilo-bugs.pt-BR.md, arquivo da suite [`tests/v1105_danilo_bugs_regression.rs`]../tests/v1105_danilo_bugs_regression.rs


## Matriz de Camadas de Teste
### Camada 1 — Testes Unitários (por commit)
- Comando: `/usr/bin/timeout 300 cargo nextest run --profile default`
- Escopo: funções puras, parsing, validação e variantes de erro dentro de `src/`
- Critério: ZERO falhas
- Nota: testes que leem a dim global de embedding DEVEM ser `#[serial_test::serial(env)]` (G50 causa E)
### Camada 2 — Testes de Integração (por commit)
- Comando: a mesma invocação do nextest; arquivos vivem em `tests/`
- Pré-requisito: `export PATH="$PWD/tests/mock-llm:$PATH"` (mocks dim-aware desde o G51)
- Critério: ZERO falhas
### Camada 3 — Doctests (por commit, OBRIGATÓRIA localmente)
- Comando: `/usr/bin/timeout 300 cargo test --doc`
- O nextest NÃO executa doctests; pular esta camada localmente foi como a causa A do G50 ficou quebrada por 10 releases
- Critério: ZERO falhas
### Camada 4 — Suítes Lentas de Contrato (por release)
- Comando: `/usr/bin/timeout 1800 cargo nextest run --profile heavy --features slow-tests`
- Comando: `/usr/bin/timeout 1200 cargo test --features slow-tests --test doc_contract_integration -- --nocapture`
- Comando: `/usr/bin/timeout 1200 cargo test --features slow-tests --test prd_compliance -- --nocapture`
- Critério: ZERO falhas nos ~1881 testes
### Camada 5 — Concorrência Loom (somente opt-in explícito)
- Comando: `/usr/bin/timeout 3900 bash scripts/test-loom.sh`
- RISCO TÉRMICO: nunca rodar fora do script dedicado (incidente de 2026-04-19)
- Critério: todos os modelos gated completam dentro dos limites de preempção
### Camada 6 — Benchmarks (por release, informativa)
- Comando: `/usr/bin/timeout 1800 cargo bench --bench regression_baseline -- --quick`
- Pré-requisito: mock LLM no PATH (G50 causa C)
- Critério: sem regressão acima de 10 por cento contra o baseline armazenado
### Camada 7 — Black-Box Pós-Publicação (por release, OBRIGATÓRIA)
- Alvo: o binário instalado do crates.io (`cargo install sqlite-graphrag`), nunca o `target/`
- Setup: banco temporário via `SQLITE_GRAPHRAG_DB_PATH`, namespace isolado, mocks dim-aware no PATH
- Matriz: bootstrap (init/health/migrate/stats), ciclo de vida CRUD, comandos de busca, comandos de grafo, manutenção (fts/optimize/backup/vec/export), contratos de exit code (1, 2, 3, 4, 9) e contratos JSON contra `docs/schemas/`
- Robustez: abort OAuth-only com `ANTHROPIC_API_KEY` definida, SIGPIPE exit 141 em output grande, `--tz` inválido exit 2, `SQLITE_GRAPHRAG_EMBEDDING_DIM` inválida emite warning (G49)
- Dimensionalidade: banco virgem adota 64; banco 384 pré-existente é adotado (G43) e os lotes encolhem (G44)
- Tarball: baixar o `.crate`, verificar ausência de arquivos proibidos (scripts/legacy, configs de agente) e READMEs corretos
- Critério: cada comando bate o exit code e o schema esperados; esta camada teria pego G46-G49 antes dos usuários
### Camada 8 — Smoke com LLM Real (por release, custo OAuth)
- Comandos: uma criação pequena com grafo curado, um round-trip de `recall`, um `edit --force-reembed`
- Orçamento: 3 chamadas LLM, menos de 5 minutos no total; latência esperada da criação abaixo de 90 segundos (critério G42)
- Registrar o score do top hit para o baseline de qualidade de retrieval (G54)
- Rate limits são registrados como evidência, nunca retentados em loop


## Gates de Release (executar em ordem, parar na primeira falha)
### Os 8 Gates Obrigatórios
- Gate 1: `cargo fmt --all --check`
- Gate 2: `/usr/bin/timeout 600 cargo clippy --all-targets --all-features -- -D warnings`
- Gate 3: camadas 1-4 verdes, INCLUINDO `cargo test --doc`
- Gate 4: `RUSTDOCFLAGS="-D warnings" /usr/bin/timeout 300 cargo doc --no-deps --all-features`
- Gate 5: `/usr/bin/timeout 120 cargo audit`
- Gate 6: `/usr/bin/timeout 180 cargo deny check advisories licenses bans sources`
- Gate 7: `/usr/bin/timeout 120 cargo publish --dry-run --allow-dirty` mais revisão de `cargo package --list`
- Gate 8: workflow CI do GitHub Actions VERDE no commit do release — publicar com CI vermelho é a falha raiz documentada no G50
### Gates Informativos (registrar, decidir, nunca pular em silêncio)
- `cargo +stable semver-checks --baseline-version <anterior>` — exige rustc >= 1.91; 9 quebras major saíram em silêncio na v1.0.79 (G53)
- `cargo llvm-cov --lib --summary-only` — meta de cobertura de 80 por cento para código novo


## Gatilhos
### Por Commit
- Camadas 1-3 mais Gates 1-2
### Por Release (antes do `cargo publish`)
- Camadas 1-6 mais os 8 gates mais os gates informativos
### Pós-Publicação (depois do crates.io aceitar a versão)
- Camadas 7-8 contra o binário instalado do registry
- Registrar gaps novos em `gaps.md` no formato de numeração G para qualquer achado


## Riscos e Restrições
- Loom fora do script pode congelar termicamente máquinas de muitos cores (hard reset em 2026-04-19)
- O smoke com LLM real depende de OAuth ativo; uma chamada custa 10-90 segundos
- Jobs em background acima de ~80 minutos podem ser mortos por harnesses de agente (G42/C1); manter jobs de teste curtos
- `cargo-nextest` e `cargo-llvm-cov` NÃO são assumidos instalados; instalar via binários pré-compilados antes da Camada 1


## Planos Mais Recentes — v1.0.84 e v1.0.85

O plano de teste do Split do Backend Claude (ADR-0042) e o plano de teste da Remediação de Cinco Gaps (ADR-0043) estão consolidados neste documento; seus arquivos de snapshot independentes foram aposentados na v1.0.96.

## Plano de Teste v1.0.99 — Remoção do Degree-Cap + Correções de Doc/Convergência (ADR-0059, GAP-SG-67/68/69)

### Mudanças na Camada 1 (unit)
- GAP-SG-67: os 5 testes unitários de `enforce_degree_cap` e o helper `setup_cap_db` foram removidos junto com a função; sem teste de regressão substituto — a propriedade aditiva é garantida por construção (não resta `DELETE FROM relationships` no path de escrita de `remember`/`link`).
- GAP-SG-68: os 6 testes `build_order_by_*` fixam o default ascendente e a ordenação `--order desc` que o doc-comment realinhado promete.
- GAP-SG-69: `skipped_item_keys_excludes_only_skipped_for_operation` fixa que apenas linhas `status='skipped'` da operação são retornadas, então o rescan do body-enrich converge.

### Validação Manual / E2E
- GAP-SG-67: `remember`/`link` referenciando um hub de grau alto (grau > 50) — confirme via `graph stats` que a contagem total de relações NÃO decresce e o grau do hub permanece intacto; passar `--max-entity-degree` agora deve falhar com clap exit 2.
- GAP-SG-68: `graph entities --sort-by degree --json` retorna ascendente por default; `--order desc` retorna os mais-conectados-primeiro.
- GAP-SG-69: rode `enrich --operation body-enrich --mode openrouter ... --until-empty` contra um banco com corpos curtos não-expansíveis — o backlog converge (empiricamente 55→3) e o sidecar `.enrich-queue.sqlite` é mantido enquanto houver vereditos `skipped`.

### Gate
- Sem migração; schema permanece v15; `Cargo.toml` é 1.0.99.

## Plano de Teste v1.1.04 — Correção de Runtime Aninhado do deep-research + Convergência do entity-connect + Migração V016 (ADR-0064)

- Nome oficial de release v1.1.04; `Cargo.toml` é `1.1.4`; binário ~19 MiB; User-Agent `sqlite-graphrag/1.1.4`. Migração de banco OBRIGATÓRIA (V016).
- Schema avança v15 → v16 via migração V016 (tabela `entity_connect_seen`).

### GAP-001 — pânico de runtime Tokio aninhado no deep-research

- Reprodução (pré-correção entrava em pânico): `SQLITE_GRAPHRAG_SKIP_PREFLIGHT=1 CLAUDE_CONFIG_DIR=/tmp/graphrag-empty-config sqlite-graphrag --embedding-backend openrouter --embedding-model qwen/qwen3-embedding-8b --embedding-dim 384 deep-research "<query>" --k 5 --max-hops 2 --json` deve emitir um envelope JSON estruturado (sucesso OU erro estruturado), NUNCA um pânico.
- Teste de regressão `tests/deep_research_nested_runtime_regression.rs`: invoca `deep_research::run` dentro de um runtime Tokio ativo; afirma `Ok`/`Err` estruturado, não pânico.
- Valida o helper `compute_sub_embeddings` (embeddings computados ANTES da construção do T1) e o padrão `Handle::try_current()` + `block_in_place(|| handle.block_on(fut))` nos três caminhos OpenRouter de `embedder.rs`.

### GAP-002 — convergência do entity-connect

- `enrich --operation entity-connect --status --mode openrouter --openrouter-model deepseek/deepseek-v4-flash:nitro --json` deve reportar `scan_backlog > 0` (antes era sempre 0 antes da v1.1.04).
- `enrich --operation entity-connect --until-empty --max-runtime 600 --mode openrouter --openrouter-model deepseek/deepseek-v4-flash:nitro --json` deve convergir: `eligible_remaining` decai para 0; pares aparecem em `entity_connect_seen` com veredito `related` ou `none`; re-scans pulam pares avaliados.
- `graph stats --json` mostra novas arestas após a convergência.
- Testes de regressão: `count_operation_backlog_entity_connect_counts_isolated` (entidade de grau 0 COM binding NER conta; sem binding não conta); `scan_isolated_entity_pairs_excludes_seen` (par em `entity_connect_seen` não é retornado).
- O teste `count_operation_backlog_advisory_ops_report_zero` exclui `EntityConnect` (agora tem predicado real de backlog).
- **v1.1.06 (GAP-ENTITY-CONNECT-SCAN-CARTESIAN)**: o scan de pares usa coocorrência + hub×ilha (nunca `entities × entities` com ORDER BY global); chaves da fila são `pair:{id1}:{id2}`; dry-run emite `scan_start` e depois `scan` em tempo de parede limitado. Suite: `tests/v1106_entity_connect_scan_regression.rs` e testes unitários em `scan.rs` / `queue.rs`.

### Migração V016

- `migrate --dry-run --json` lista V016; `migrate --json` aplica; `health --json` reporta `schema_version >= 16` e `integrity_ok: true`.
- `schema_version_matches_migrations_count` passa com V016 presente e `CURRENT_SCHEMA_VERSION = 16`.

### Totais da Suíte

- ~1072 testes lib passando (ante ~1070 na v1.1.03 — dois novos testes de regressão). `cargo nextest -P ci` para a contagem ao vivo.

## Plano de Teste v1.1.03 — Enqueue Atômico do Enrich + Migração Literal de Relação + Merge Cross-Namespace + Recuperação de Stale Claim + Re-Embed de Chunk Órfão + split-body

### Camada 1 (unit) — adições
- `commands::enrich::queue::tests::enqueue_batch_is_atomic`: prova que o caminho de enqueue em lote envolve todo insert por item em uma única transação SQL — uma falha injetada no meio do lote faz rollback de TODOS os inserts, deixando a fila no estado pré-lote.
- `commands::reclassify_relation::tests::literal_to_writes_verbatim`: prova que `--literal-to <RELATION>` armazena o destino VERBATIM (sem normalização do clap), simétrico a `--literal-from`.
- `commands::reclassify_relation::tests::literal_from_applies_to_literal_to_applies_to_hyphen_migrates`: prova que a migração canônica `--literal-from applies_to --literal-to applies-to` reescreve o literal armazenado para a forma com hífen e reporta a contagem migrada.
- `commands::merge_entities::tests::cross_namespace_merges_source_from_other_namespace`: prova que `--cross-namespace` resolve `--ids`/`--into-id` em TODOS os namespaces e funde a origem externa no destino.
- `commands::merge_entities::tests::cross_namespace_default_false_rejects_cross_id`: prova que SEM `--cross-namespace` um id cross-namespace é REJEITADO (padrão seguro) e nenhuma fusão ocorre.
- `commands::enrich::queue::tests::stale_processing_claim_is_reset_after_threshold`: prova que uma claim `processing` cujo `claimed_at` é mais antigo que o limiar de stale é resetada para `pending` pela varredura do startup.
- `commands::enrich::queue::tests::fresh_processing_claim_is_preserved`: prova que uma claim `processing` cujo `claimed_at` está dentro do limiar é MANTIDA em `processing` (nenhum reset falso de trabalho vivo).
- `commands::enrich::queue::tests::heartbeat_updates_claimed_at`: prova que o heartbeat em background avança o `claimed_at` enquanto um item está sendo processado, então um worker vivo nunca é classificado como stale.
- `commands::enrich::tests::enrich_reset_stale_claims_manual_flag`: prova que `enrich --reset-stale-claims` força um reset de toda claim stale imediatamente, independente da varredura do startup.
- `commands::enrich::scan::tests::scan_chunks_of_soft_deleted_memory_are_selected`: prova que o scan de chunks usa `LEFT JOIN memories` para que chunks cuja memória-mãe foi soft-deletada AINDA sejam selecionados para re-embedding (comportamento anterior os pulava).
- `commands::enrich::scan::tests::count_backlog_includes_orphan_chunks`: prova que o `scan_backlog` do `--status` para o alvo chunks inclui chunks órfãos na contagem (nenhum `pending=0` falso).
- `commands::split_body::tests::split_body_divides_long_memory_into_parts`: prova que `split-body --name <N>` divide um corpo sobredimensionado em filhas `{name}-part-{i}` no limiar configurado.
- `commands::split_body::tests::split_body_marks_original_as_superseded`: prova que a memória original é marcada com metadata `superseded_by_split: true` e é preservada no histórico.
- `commands::split_body::tests::split_body_creates_replaces_relations`: prova que cada filha ganha uma relação canônica `replaces` apontando para a original, então `related`/`graph traverse` ainda alcançam o corpo sobrescrito.
- `commands::split_body::tests::split_body_preserves_history`: prova que a operação de split cria uma entrada de histórico para a original (versionada, reversível via `restore`).

### Camada 2 (integration) — adições
- `tests/split_body_integration.rs`: `split-body --batch --threshold 25000` ponta-a-ponta sobre um corpus de fixture, seguido de `enrich --operation re-embed --target memories`, afirmando que as filhas se tornam pesquisáveis via `recall` e a original ainda resolve via arestas `replaces`.

### Gate
- Nenhum `migrate` no banco principal; schema permanece v15. O sidecar `.enrich-queue.sqlite` ganha `claimed_at` via `ALTER TABLE ADD COLUMN` idempotente. `Cargo.toml` é 1.1.3.


## Plano de Teste v1.1.02 — Remoção do GLiNER + TooManyTokens Tipado + Regressão Re-Embed + Prune de Órfãos de Entidade (ADR-0062)

### Adições na Camada 1 (unit)
- `commands::enrich::queue::tests::prune_dead_entity_orphans_removes_only_entity_dead_rows`: prova que o novo helper `prune_dead_entity_orphans` deleta apenas linhas `status='dead' AND item_type='entity'`, preservando linhas dead com chave de memória e linhas vivas de entidade.

### Adições na Camada 2 (integração)
- `tests/prune_dead_entity_orphans_integration.rs`: exercício end-to-end da CLI `enrich --prune-dead-entity-orphans --json`; planta linhas entity-dead e memory-dead no sidecar, roda a flag e afirma `pruned==1`, a linha entity-dead removida, a memory-dead preservada.
- `tests/reembed_entities_integration.rs`: regressão do Gap 3 — `remember --graph-stdin` planta 2 entidades com embeddings vazios (`--llm-backend none`), depois `enrich --operation re-embed --target entities` faz backfill dos dois vetores (entity_embeddings 0→2); uma segunda execução é idempotente (sem linhas duplicadas). Esqueleto idiomático espelha `tests/v1063_features.rs` (`assert_cmd` + `serial_test` + `tempfile`).

### Justificativa de Regressão
- O dispatch `strip_prefix("entity:")` em `call_reembed` estava silenciosamente quebrado para re-embed com chave de entidade desde que o caminho foi adicionado; este teste de regressão garante que o dispatch continue roteando para `call_reembed_entity`.

## Plano de Teste v1.0.97 — Sidecar de Fila do `--db` + Poda de Órfãos Dead-Letter (ADR-0056/0057/0058, GAP-SG-57..66)

### Adições na Camada 1 (unit)
- `paths::sidecar_path` (3 testes): um `--db` absoluto deriva o sidecar ao lado dele; um nome puro (sem parent) cai no layout do CWD; um `--db` em diretório aninhado deriva o sidecar naquele diretório
- `prune_dead_orphans_removes_only_orphan_memory_rows`: somente linhas `status='dead'` com `item_type='memory'` cujo `item_key` não existe mais no banco principal são deletadas; linhas dead com chave de entidade e linhas dead de memória viva ficam intocadas
- Auditoria de `unwrap`/`expect` de produção (GAP-SG-57..60, ADR-0056) imposta por um lint gate do Clippy (`-D warnings`); a desduplicação de `parse_claude_output` mantém os parsers do enrich e do ingest_claude comportamentalmente idênticos

### Adições na Camada 2 (integração)
- `tests/enrich_queue_db_isolation.rs`: o enrich enfileira contra `tmpA/db.sqlite`, depois `enrich --status --db tmpA/db.sqlite` de um CWD diferente reporta o backlog enquanto `--db tmpB/db.sqlite` reporta zero — prova que a fila segue o `--db`, não o CWD

### Endurecimento de testes flaky
- GAP-SG-61 `concurrency_peak_never_exceeds_permits` e o cluster `llm_slots::tests` do GAP-SG-63 foram des-flakados (contabilidade determinística de permits); ambos verdes sob a suite completa

### Smoke do binário instalado (GAP-SG-62)
- `cargo install --path . --locked --force` realinhou `~/.cargo/bin/sqlite-graphrag` para 1.0.97; `installed_binary_smoke` agora roda 26/0 SEM o bypass de version-mismatch

### Totais de selagem
- `cargo test --lib` 973 passed / 0 failed; `cargo test` default 1164 / 0; `cargo test --features slow-tests` 1522 / 0 / 11 ignored; `cargo fmt --check` 0 diffs; `cargo clippy --all-targets --features slow-tests -- -D warnings` 0 warnings

## Plano de Teste v1.0.96 — Dead-Letter do Enrich + Concorrência REST OpenRouter (ADR-0055, GAP-ENRICH-BACKLOG-CONVERGE, GAP-OPENROUTER-REST-CONCURRENCY)

### Adições na Camada 1 (unit)
- Classificação de outcome (`commands::enrich::tests`, 8 testes): rate-limit / timeout / db-busy mapeiam para `AttemptOutcome::Transient`; validação / parse mapeiam para `HardFailure`
- `open_queue_db`: `ALTER TABLE` idempotente adicionando as colunas `error_class` e `next_retry_at` (re-execução é no-op)
- `record_item_failure`: um HardFailure marca o item como `dead` imediatamente; um Transient marca como `pending` com `next_retry_at` futuro via `compute_delay`; um Transient além de `--max-attempts` marca como `dead`
- Elegibilidade de dequeue: linhas com `next_retry_at` futuro são puladas e linhas `dead` são excluídas, logo o conjunto vivo é estritamente decrescente
- Ordem do fan-out de embedding (`embedder::tests::reassemble_ordered_restores_input_order`): conclusão fora de ordem do `JoinSet` é remontada pelo índice de chunk, restaurando a ordem de entrada

### Adições na Camada 2 (integração)
- Convergência do dead-letter: ingest de 6 ADRs com `--mode none`, depois `enrich --until-empty --rest-concurrency 8` drena `unbound_backlog` 6 → 0
- Segunda passada idempotente: re-executar `enrich --until-empty` faz zero trabalho (~6 ms) — nenhum item elegível resta

### Delta na Camada 8 (smoke com LLM real)
- `tests/openrouter_live_concurrency.rs` (`#[ignore]`, rodar com `cargo test --test openrouter_live_concurrency -- --ignored --nocapture`): embeda 64 chunks de `docs/*.md` em k=1 vs k=8
- Prova de ordem: cosseno diagonal 0.9999, off-diagonal max 0.899, argmax 64/64 — ordem de chunk preservada apesar da conclusão fora de ordem do JoinSet
- Total da suite: 1086 passed, 0 failed, 6 skipped via nextest

## Plano de Teste v1.0.95 — Enrich via Chat OpenRouter (ADR-0054, GAP-OR-ENRICH)

### Adições na Camada 1 (unit)
- Montagem de `ChatRequest` (`src/chat_api.rs`, `OpenRouterChatClient`): testes wiremock verificando `response_format` `json_schema` com `strict:true`, `provider.require_parameters:true` e `reasoning.enabled:false`
- Parse da resposta: extração de `choices[].message.content` seguida de um segundo parse JSON do payload de schema rígido
- Leitura de `usage.cost` do corpo da resposta
- Retry: `429` com header `retry-after`, backoff exponencial em `5xx`, `401` permanente sem retry
- Erros `400`/`404` retornados sem retry
- Conteúdo vazio / resposta de recusa tratados como modelo incompatível
- `validate_mode_flags`: rejeita flags `claude`/`codex`/`opencode` sob `--mode openrouter`
- `--openrouter-model` obrigatória: retorna exit 1 antes de qualquer chamada de rede quando ausente

### Adições na Camada 2 (integração)
- Dispatch do JUDGE para `call_openrouter` em todas as operações de enrich (`memory-bindings`, `entity-descriptions`, `body-enrich`)
- Validação de chave API via `resolve_api_key` sem spawn de subprocesso

### Delta na Camada 8 (smoke com LLM real)
- `tests/openrouter_chat_real.rs` (`#[ignore]`, rodável com `OPENROUTER_API_KEY`) iterando os 13 modelos de texto contra o schema rígido
- Matriz de compatibilidade 13/13 (9 diretos com `reasoning.enabled:false`, 4 via fallback reasoning-mandatory)

## Plano de Teste v1.0.93 — Backend de Embedding OpenRouter (ADR-0052, GAP-OR-INGEST)

### Adições na Camada 1 (unit)
- `model_default_input_type()`: 10 testes cobrindo seleção de `input_type` por modelo (fix BUG-OR-1 — NVIDIA Nemotron retorna `"passage"`, Mistral retorna `None`, demais retornam `"search_document"`)
- `model_supports_mrl()`: testes cobrindo detecção MRL para todos os 10 modelos verificados incluindo NVIDIA e BAAI (fix BUG-OR-2)
- `validate_model_id()`: testes cobrindo validação de ID de modelo contra os 10 modelos aprovados e rejeição de 5 IDs inexistentes (fixes BUG-OR-3, BUG-OR-4)
- `execute_with_retry()`: teste cobrindo retry de corpo malformado em HTTP 200 (fix BUG-OR-5 — parse error em HTTP 200 tratado como transitório)

### Adições na Camada 2 (integração)
- `tests/openrouter_embedding.rs`: testes de integração baseados em wiremock cobrindo o fluxo completo de embedding via API REST OpenRouter — construção de request, truncamento MRL, `input_type` por modelo, chunking em batch (MAX_BATCH_SIZE=32), retry de erro e tratamento de chave API via `secrecy::SecretString`
- Propagação de `EmbeddingBackendChoice`: testes verificando que `--embedding-backend openrouter` alcança todos os 8 comandos (remember, remember-batch, ingest, recall, edit, restore, hybrid-search, deep-research)
- Flag `--enrich-after`: testes verificando que `ingest --enrich-after` dispara `enrich --operation memory-bindings` sequencialmente após a fase de embedding

### Adições na Camada 7 (pós-publicação)
- Round-trip de embedding OpenRouter: `remember` com `--embedding-backend openrouter --embedding-model "qwen/qwen3-embedding-8b"` seguido de `recall` com as mesmas flags, verificando similaridade vetorial
- Exit 78 na ausência de `--embedding-model` quando `--embedding-backend openrouter` está especificado

### Delta na Camada 8 (smoke com LLM real)
- Opcional: um teste smoke de embedding OpenRouter usando `OPENROUTER_API_KEY` real (opt-in via `SQLITE_GRAPHRAG_OPENROUTER_E2E=1`)
- Orçamento: 1 chamada de API, menos de 5 segundos, latência esperada de embedding abaixo de 500ms

## Plano Histórico — Deltas do Plano v1.0.80 — G45, G53, G55 S2, G56, G58, ADR-0033, ADR-0034

A release v1.0.80 (bump patch, sem migração de schema) adicionou os
seguintes deltas de teste à matriz por camada acima.
Consumidores da biblioteca são FORTEMENTE aconselhados a fixar em
`=1.0.80` porque a API da lib é instável em v1.x.y (ADR-0032).

### Adições na Camada 1 (unit)

- `acquire_embedding_singleton` (G45): 5 testes cobrindo contenção
  de lock no mesmo banco, independência entre bancos distintos,
  polling via `--wait-embed-singleton`, flag `force` e detecção
  de lock stale via PID.
- `AppError::MemoryNotFound` e `AppError::MemoryNotFoundById`
  (G55 S2): 6 testes afirmando que o identificador é parte da
  variante, exit code é 4 e a mensagem localizada em pt-BR
  carrega nome e namespace explicitamente.
- `embed_entity_texts_cached` (G56): 4 testes afirmando hit de
  cache na segunda chamada com mesmo model+text, miss em texto
  diferente, contabilidade do `EmbedCacheStats` e comportamento
  quando o embedder subjacente retorna erro.
- `recall --fallback-fts-only` e `hybrid-search --fallback-fts-only`
  (G58): 3 testes cobrindo o caminho FTS5-only, mais 1 teste
  `#[ignore]` que exercita o caminho `EmbeddingFailed` (exige
  `PATH` sem `codex` ou `claude`).

### Adições na Camada 2 (integração)

- `tests/completions.rs`: 7 testes end-to-end para o subcomando
  `completions` (bash, zsh, fish, powershell, elvish, exit code
  de shell inválido, validação de output não-vazio por shell).
- `tests/shutdown_bypass.rs`: 3 testes de integração cobrindo a
  receita de bypass SHUTDOWN em 3 camadas (`PATH=tests/mock-llm:...`
  mais `SQLITE_GRAPHRAG_IGNORE_SHUTDOWN=1` mais `setsid -w timeout`).
- `tests/embedder_singleton.rs`: 2 testes de integração cobrindo
  o singleton de embedding cross-process contra um banco
  temporário (invocações concorrentes de `remember` no mesmo par
  `(namespace, db)` serializam; pares distintos prosseguem em
  paralelo).

### Adições na Camada 3 (doctest)

- 4 novos exemplos de doctest para `acquire_embedding_singleton`,
  `embed_entity_texts_cached`, construção de `MemoryNotFound` e
  a receita de bypass SHUTDOWN em 3 camadas (verificados via
  `cargo test --doc` em todo commit).

### Adições na Camada 4 (contratos lentos)

- `tests/doc_contract_integration.rs`: 2 novos testes de contrato
  validando que os campos `vec_degraded`, `vec_error` e `warning`
  do envelope aparecem nas respostas JSON de `recall` e
  `hybrid-search` quando o subprocesso LLM falha (G58).
- `tests/prd_compliance.rs`: 1 novo teste de compliance PRD
  validando que os 6 novos símbolos públicos da biblioteca
  documentados em CHANGELOG.md (G45 e G56) são todos `pub` e
  têm as assinaturas documentadas.

### Adições na Camada 7 (pós-publicação)

- A matriz black-box pós-publicação agora inclui 3 novos contratos
  de exit code: `EmbeddingSingletonLocked` (exit 75, retentável),
  `MemoryNotFound` com identificador na mensagem (exit 4) e
  `vec_degraded: true` em `recall` (exit 0 com warning).

### Delta na Camada 8 (smoke com LLM real)

- O score do top hit do round-trip de `recall` com LLM real é
  registrado como o novo baseline de qualidade de retrieval
  do G54 (campo já existente no protocolo de smoke; a v1.0.80
  apenas torna o registro obrigatório).

### Gates — adições

- Gate 2 (clippy) ganha `--all-features` (era somente
  `--all-targets`) e permanece a barra bloqueante.
- Gate 8 (CI VERDE) agora exige o novo job `semver-checks`
  (modo informativo em v1.0.80, vira bloqueante em v1.0.81).
  O bug de `--manifest-path` duplicado do commit inicial da
  v1.0.79 está corrigido.
- Os jobs da matrix windows-2025 ganharam steps de pre-warm e
  verify gateados em `if: matrix.os == 'windows-2025'`
  (ADR-0033, G53-WINDOWS-INFRA). Validação local de cross-compile:
  `cargo check --target x86_64-pc-windows-msvc --lib --all-features`
  reproduzido e o `E0463` resolvido via `rustup target add
  x86_64-pc-windows-msvc --toolchain 1.88`; o build então atinge
  a fronteira `cc-rs: failed to find tool "lib.exe"`, que é o
  limite esperado de cross-compile MSVC a partir de host Linux.

### Atualização de gatilhos

- Por commit: Camadas 1-3 mais Gates 1-2 (inalterado).
- Por release (antes do `cargo publish`): Camadas 1-6 mais os
  8 gates mais os gates informativos. O novo gate informativo
  `semver-checks` agora faz parte deste gatilho.
- Pós-publicação: Camadas 7-8 contra o binário instalado do
  registry (inalterado). A matriz da Camada 7 agora inclui os
  3 novos contratos de exit code da v1.0.80 acima.

## Rastreabilidade
- Toda falha encontrada por este plano vira um gap numerado no `gaps.md` com status, causa raiz e cadeia causa-efeito
- Gaps corrigidos devem referenciar o teste de regressão que protege a correção
- Auditoria de 2026-06-11: a primeira execução deste plano produziu o G46-G54 e suas correções