Skip to content

Stop --dry-run creating the index tables, and report them in db stats - #358

Merged
sroussey merged 1 commit into
mainfrom
claude/p1-sec-352-dry-run-kb
Sep 8, 2026
Merged

Stop --dry-run creating the index tables, and report them in db stats#358
sroussey merged 1 commit into
mainfrom
claude/p1-sec-352-dry-run-kb

Conversation

@sroussey

@sroussey sroussey commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Closes #352.

The problem

The knowledge base's three tables are built lazily against the getDb() connection instead of through createStorage, so neither dry-run guard reached them: no ReadOnlyTabularStorage wrapper in the path, and this module's one isDryRun() protected the kb_index row write several lines below the three CREATE TABLEs.

sec --dry-run ask created all three on a database that had none — DDL from a command whose whole promise is to change nothing.

Changes

The guard. getSecKnowledgeBase now checks isDryRun() before setupDatabase(). A dry run against an index that already exists is the ordinary case and still reads it; one that would have to build the index refuses and names sec index, rather than quietly building it.

Existence is read off sqlite_master rather than by probing each storage — a SELECT against a missing table throws, and telling "no such table" from a real failure by its message is a guess this can simply avoid.

db stats reports the index. The same bypass had a second consequence the issue documents: the report derives its rows from the storage registry, these three are not in it, and there is no repository token to count them through — so an operator had no way to see whether an index existed or how large it was. getKbTableStats counts them off the connection and the report appends them.

Three cases it handles deliberately:

  • Before the index is builtrows: null, the same n/a signal a registered-but-uncreated table reports.
  • Non-SQLite backend → omitted entirely. The index is SQLite-only by design and getSecKnowledgeBase refuses Postgres by name, so three permanent n/a rows would read as a setup gap an operator could close, and there is nothing to close.
  • SQLite location unbound → omitted. Found while running the existing DbStatsTask suite, which went red: getDb() would otherwise open a database from an unset folder token, and one unreadable appendix must not cost the operator every row count above it.

What is deliberately left open

Registry adoption (the issue's option 2) would retire the exception outright, but defineStorage cannot yet express a vector storage at a fixed width. That stays on the issue — this closes the dry-run defect and the visibility gap, not the contract exception.

Verification

Tests written first: the two "creates no table" assertions were red against the old code, reproducing the issue exactly, while the "already exists" and "not a dry run" cases passed — confirming only the missing-table path was broken.

format-check, lint, typecheck clean. 11/11 in secKnowledgeBase.test.ts, 4/4 in the new kbTableStats.test.ts, and 49/49 across src/web, src/task/db, src/kb, setupAllDatabases and resetAllDatabases.

Merge note

Conflicts with claude/p1-sec-347-release-floor on CHANGELOG.md only — both append to the same ### Changed list. Resolution is to keep both bullet sets; no code overlap.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LWp6Z6wvAPDaDCjAFcTSj6


Generated by Claude Code

… db stats

The knowledge base's three tables are built lazily against the `getDb()`
connection instead of through `createStorage`, so neither dry-run guard reached
them: no `ReadOnlyTabularStorage` wrapper in the path, and this module's one
`isDryRun()` protected the `kb_index` row write several lines below the three
`CREATE TABLE`s. `sec --dry-run ask` created all three on a database that had
none — DDL from a command whose whole promise is to change nothing.

A dry run against an index that already exists is the ordinary case and still
reads it. One that would have to build the index now refuses and names
`sec index`, rather than quietly building it.

The same bypass had a second consequence the issue documents: `db stats` derives
its rows from the storage registry, these three are not in it, and there is no
repository token to count them through — so an operator had no way to see
whether an index existed or how large it was. `getKbTableStats` counts them off
the connection directly and the report appends them: `n/a` before the index is
built, the same signal a registered-but-uncreated table reports, and omitted
entirely where the index cannot exist (a non-SQLite backend, or before the
SQLite location is bound — one unreadable appendix must not cost the operator
every row count above it).

Registry adoption, which would retire the exception outright, is left open on
the issue: `defineStorage` cannot yet express a vector storage at a fixed width.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWp6Z6wvAPDaDCjAFcTSj6
@sroussey
sroussey merged commit 5e58fcf into main Sep 8, 2026
1 check passed
@sroussey
sroussey deleted the claude/p1-sec-352-dry-run-kb branch September 8, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--dry-run creates the three knowledge-base tables: getSecKnowledgeBase bypasses createStorage, so ReadOnlyTabularStorage and the DDL guard never see it

2 participants