Repository navigation
fix: keep the kb_indexed_at stamp honest, and out of a dry run - #381
Merged
Merged
Conversation
Three defects around `filing_document.kb_indexed_at`, the cache the index
selection reads as a narrowing of its `kb_document` anti-join.
**A knowledge base rebuilt from empty was never repopulated.** The stamp was
only ever set, never cleared, so dropping the three knowledge-base tables and
re-running `sec index` — the documented way to change the embedding model —
left every previously indexed filing stamped. Opening the index re-creates
`kb_document` empty, which switches the narrowing on, and the stamps then
exclude the whole corpus from the rebuild: `sec index` reported `indexed: 0`
and `sec ask` answered from an empty index without an error. The stamp now
moves in both directions. `syncKbIndexedStamp` clears the stamp of any
document the knowledge base no longer holds, and clears the column outright
where the table is gone; opening the index clears every stamp when it holds no
documents at all, which is the cheap check the rebuild path needs (one
`LIMIT 1` probe in the steady state, rather than the full reconciliation's
probe per stamped row). Both directions err toward a traversal, never toward a
document that is never offered again.
**`--dry-run` issued a real UPDATE and a real CREATE INDEX.** The stamp pass
runs raw SQL, which goes around the `ReadOnlyTabularStorage` wrapper, and sat
outside any guard in `setupAllDatabases`, so `sec db setup --dry-run` and
`sec setup --dry-run` both wrote. The `isDryRun()` bail now lives inside the
functions themselves, where a future caller cannot bypass it.
**A plan assertion could never fail.** `expect(detail).not.toContain("SCAN d\n")`
tested for a newline that `EXPLAIN QUERY PLAN` details never contain. It is
replaced by the whole plan: one step, and it walks the partial index. A
companion test drops that index and pins what is actually lost — not a table
scan, since another index still serves the ORDER BY, but an index over every
row instead of only the unindexed tail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fRkYZYfNTzqATS6DA7y7y
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three defects around
filing_document.kb_indexed_at, the cache the index selection reads as a narrowing of itskb_documentanti-join.1. A knowledge base rebuilt from empty was never repopulated (critical)
The stamp was only ever set, never cleared.
getSecKnowledgeBasetells the operator to dropkb_document,kb_chunkandkb_indexand re-runsec indexwhen the embedding model changes — andIndexFilingSectionsTask.executeopens the knowledge base before it selects, so the DDL re-createskb_documentempty.kbDocumentTableExists()is then true, thed.kb_indexed_at IS NULLclause switches on, and the stamps from the previous index exclude the whole corpus. The anti-join on its own would have selected every document. The run reportedindexed: 0andsec askanswered from an empty index with no error;db setupcould not heal it, becausesyncKbIndexedStamponly ever set stamps.The stamp now moves in both directions:
syncKbIndexedStampclears the stamp of any document with no survivingkb_documentrow, and clears the column outright in the branch where the table is gone.clearKbIndexedStampIfIndexEmpty), which is the check the rebuild path needs and is cheap enough to run on every open: oneLIMIT 1probe wherever the index holds anything, against the full reconciliation's probe per stamped row. The scan it falls through to only happens where nothing is indexed — a run that is about to embed a corpus.Both directions err toward a traversal (a missing stamp costs one; the anti-join still prevents a double index), never toward a document that is never offered again.
2.
--dry-runwrote to the database (high)syncKbIndexedStamp(getDb())sat outside any guard insetupAllDatabases, sosec db setup --dry-runandsec setup --dry-runissued a realUPDATE filing_document SET kb_indexed_at = …and a realCREATE INDEX. Raw SQL goes around theReadOnlyTabularStoragewrapper. TheisDryRun()bail now lives inside the functions rather than at the call site, so a future caller cannot bypass it.3. A plan assertion that could never fail (high)
expect(detail).not.toContain("SCAN d\n")tested for a newline thatEXPLAIN QUERY PLANdetails never contain, so the test's "no table scan" claim was unenforced.Worth noting for reviewers: the suggested
not.toMatch(/\bSCAN d\b/)would have failed the passing case — SQLite reports a walk of the partial index asSCAN d USING INDEX filing_document_kb_unindexed. So the assertion is now the whole plan: one step, and it walks the partial index, plus a check that no step reaches the table itself and none sorts into a temp B-tree. A companion test drops the partial index and pins what is actually lost — not a table scan, sincefiling_document_filing_date_accession_numberstill serves theORDER BY, but an index over every row instead of only the unindexed tail.Tests
src/config/kbIndexedStamp.sqlite.test.tsgains four tests; each fails onmain(verified by reverting the two source files and re-running):clears a stamp the knowledge base no longer backsclears every stamp when the knowledge base tables are gonere-offers every document after the index is dropped and rebuilt empty— the traced failure, end to end throughgetSecKnowledgeBaseandselectDocumentsToIndexwrites nothing under a dry runPlus
leaves the stamps alone when the index still holds documents, which pins the other half of the rule, and the reworked plan tests.Verification
🤖 Generated with Claude Code
https://claude.ai/code/session_019fRkYZYfNTzqATS6DA7y7y
Generated by Claude Code