sqlite-graphrag 1.2.0

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# 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.2.0 (XDG + dim 1024 + E2E offline)

- Comando: `bash scripts/e2e_offline_v120.sh` (espera **20/20 PASS** — 15 `check()` + 5 PASS manuais; binário 1.2.0+; wrapper histórico `e2e_offline_v118.sh` supersedido)
- Companheiros unitários/integração:
  - Contrato de help `tests/help_no_product_env` — o help não deve anunciar product env `SQLITE_GRAPHRAG_*` como config
  - Regressão **GAP-SG-139** `tests/cli_db_noop_host_surfaces_regression.rs` — folhas host/XDG aceitam `--db` como no-op documentado (`src/cli_db_noop.rs`: config×9, slots×3, cache×3, codex-models, completions)
- Escopo: harness só XDG; config set/list/effective; purge --now dry-run; fold de EntityType; description em remember-batch; pending-embeddings status; cache stats; scrub de help (sem Box about); flags offline **`list-skipped` / `requeue-skipped`** + presença no help; **DEFAULT_EMBEDDING_DIM=1024** efetivo (`embedding.dim`)
- Smoke de contrato (manual ou suite):
  - `deep-research "q" -o /tmp/dr.json --quiet --json` materializa arquivo + ack blake3
  - `memory-entities --name … --json` inclui `entities[].description`
  - `remember … --enqueue-enrich --json` pode emitir `entities_created` / `enrich_recommended`
  - `enrich --operation entity-descriptions --status --force-redescribe --json` expõe campos de qualidade
  - `enrich --operation entity-descriptions --list-skipped --json` / `--requeue-skipped` recuperam o sink skipped sem SQL bruto
  - entity-connect é totalmente implementado (persiste); não documentado como scan-only
  - no-op de folha host: `config doctor --db /tmp/x.sqlite` aceita `--db` sem abrir esse path
- Critério: harness offline **20/20**; zero propaganda de product env no help; regressão GAP-SG-139 verde
- Docs complementares: [TESTING.pt-BR.md]TESTING.pt-BR.md, [MIGRATION.pt-BR.md]MIGRATION.pt-BR.md, [CHANGELOG.pt-BR.md]../CHANGELOG.pt-BR.md `[1.2.0]`


## 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 1024 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