fix(health): expõe indexOnly — o orphans do vectorCoverage é cego para vetor fora do map - #25
Conversation
… para vetor fora do map `orphans` é `totalMap − embedded`: os dois lados vêm de `vec_chunk_map`. Logo um vetor válido no índice vec0 que perdeu a linha de map não pode aparecer nessa conta — e o mesmo vale para o `prune-orphan-vectors`, que varre a partir do map, e para qualquer alerta que consuma o campo. Três instrumentos, mesma cegueira. Medido num deploy de produção em 2026-08-27: `orphans: 0` com **2.074** vetores nessa condição (`vec_chunks_rowids` = 69.261 contra `vec_chunk_map` = 67.187, e zero linhas de map cujo chunk não exista). Inalcançáveis pela busca, imprunáveis pela ferramenta que existe para isso, e invisíveis a todos os guardas. A mudança é aditiva: `indexOnly` sai ao lado de `orphans`, lendo a shadow table `vec_chunks_rowids` direto — ela é tabela normal e não exige o módulo vec0 carregado, ao contrário de `vec_chunks`. `−1` sinaliza shadow ausente (vec0 nunca criado), que não é zero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXG3rN3mBLHSwKtiGSRktV
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 577a848ce6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| embeddingOrphans = Math.max(0, totalMap - embedded); | ||
| try { | ||
| const idx = (db.prepare("SELECT COUNT(*) AS c FROM vec_chunks_rowids").get() as { c: number }).c; | ||
| indexOnly = Math.max(0, idx - totalMap); |
There was a problem hiding this comment.
Count unmatched rowids instead of subtracting table totals
When vec_chunk_map also contains any stale vec_rowid absent from the vector index, subtracting the two table sizes lets those map-only rows cancel real index-only vectors. This state is reachable because upsertEmbedding deletes and inserts the vector and map rows in separate autocommit statements, so failures can leave either side unmatched; for example, one stale map row plus one unmapped vector makes this report indexOnly: 0. Query vec_chunks_rowids with an anti-join on vec_chunk_map.vec_rowid so the new health field measures the advertised set difference.
Useful? React with 👍 / 👎.
| * | ||
| * −1 = shadow table ausente (vec0 nunca criado), que NÃO é zero. | ||
| */ | ||
| vectorCoverage: { embedded, total, orphans: embeddingOrphans, indexOnly }, |
There was a problem hiding this comment.
Include index-only vectors in observability health
When monitoring uses the advertised /api/observability/health endpoint rather than /api/health, this new metric is never exposed or incorporated into the vector indicator: handleObsHealth still derives vector_coverage and vectorHealth(...) solely from embedded, total, and map-based orphans. Consequently the production state described here (embedded === total, orphans === 0, but thousands of index-only vectors) still reports the vector indicator as green; propagate indexOnly into that endpoint and its health threshold.
Useful? React with 👍 / 👎.
O quê
vectorCoveragepassa a exporindexOnlyao lado deorphans. Mudança aditiva, 21 linhas, um arquivo.Por quê
orphansétotalMap − embedded— os dois lados vêm devec_chunk_map. Um vetor válido no índice vec0 que perdeu a linha de map não pode aparecer nessa conta. E a mesma cegueira existe noprune-orphan-vectors(varreFROM vec_chunk_map LEFT JOIN chunks) e em qualquer alerta que consuma o campo: três instrumentos, um único ângulo.Medido num deploy de produção em 2026-08-27:
São 2.074 vetores inalcançáveis pela busca, imprunáveis pela ferramenta que existe para isso, e reportados como
0por todos os guardas. O tamanho (~25 MB) não é o ponto — a classe inteira é invisível, e se o mecanismo que a produziu ainda existir, ela cresce em silêncio.prunenemtrg_chunks_delete_cascadepodem tê-los criado (os dois são atômicos). Este PR não tenta consertar a causa nem apagar os vetores — apagar sem saber a origem é apagar evidência. Ele só torna a classe medível.Lição generalizável, que motivou o campo: um guarda cujo predicado exige o dado que falta não cobre a falta desse dado. O teste antes de aceitar um
nothing to do: em que estado do mundo este guarda ficaria calado por não ter o dado, em vez de por não haver problema?Como testei
/api/healthdevolve{"embedded": 67187, "total": 67187, "orphans": 0, "indexOnly": 2074};npm run buildlimpo (0 erros TS) e suíte em 377/380 — as 2 falhas são pré-existentes emedge-typing(KG), reproduzem isoladas e não tocam este arquivo;−1de shadow ausente;vec_chunks_rowids, que é tabela normal — não exige o módulo vec0 carregado, ao contrário devec_chunks(que falha comno such module: vec0no CLI). Envolvido emtry/catchcom−1, então instalação sem vec0 não regride.Escopo, e o que deliberadamente não vem aqui
Os lineages são intencionalmente diferentes (
docs/REDEPLOY-VPS-LINEAGE-PLAN.md: "3.2.1 é SUBSET do lineage da VPS — não unificar"), e a direção registrada é promover fix por fix. Duas correções irmãs ficaram fora de propósito:countIndexOnlyVectors()noprune-orphan-vectors— o módulo não existe neste repo (é do core-kit trim), então não há o que corrigir aqui;search_telemetry.top_chunk_ids— o schema deste repo não tem essas colunas; adicioná-las é decisão de produto, não correção.🤖 Generated with Claude Code
https://claude.ai/code/session_01FXG3rN3mBLHSwKtiGSRktV