fix(commands): research's read-only commands stop opening the db read-write - #620
Merged
Conversation
…db read-write (#610) keel research significance --from deployment, factors and independence are questions about a deployment's state -- "is this edge distinguishable from zero", "do these factors correlate" -- but all three reached their data through _open_repo, which calls migrate(conn), and migrate commits unconditionally. Asking a question opened the database read-write, and on a binary newer than the file, migrated its schema as a side effect of being asked. keel research pooled-review, in the same file, already does the opposite on the same tables (orders/trade_outcomes): mode=ro, no migrate. Nothing explained why one command in research.py reads a deployment database safely and its three siblings do not, and docs/research-toolkit.md's new page was about to invite operators to point all four at live ledgers without saying so. _open_repo_ro (keel/commands/_common.py) is pooled-review's own mode=ro shape promoted to a shared seam, with two refusals neither predecessor had to make explicit because pooled-review is only ever pointed at databases this binary already understands: - a missing file refuses with a plain message instead of letting sqlite3.connect silently create one (mode=ro alone already refuses to create it -- OperationalError: unable to open database file -- but that is not a message an operator can act on, so the seam checks first and says so); - a database stamped below this binary's SCHEMA_VERSION refuses before any data is read, rather than being read under a schema this binary was never tested against. That gap is an operator's to close -- run a command that writes (keel migrate), or run a binary that matches the file -- not a question these commands can answer quietly. #601 already drew this line (an operator mistake is not a result the evidence produced); this is the same call applied to a new seam. Scope, the other open decision: the seam plus the three research/ commands, nothing wider. insights, status, pnl and activity share the same read-write shape and are explicitly left for a follow-up once this seam has proven itself, rather than widened here past what #610 asked. docs/research-toolkit.md's hazard section is rewritten from "these three are inconsistent with pooled-review, fix filed separately" to stating what is now true, including the refusal message and the remaining scope boundary. New tests in tests/commands/test_research_commands.py, mutation-verified (each restored after): - test_significance_factors_independence_never_write_to_the_database -- the pooled-review pin's own shape (hash the db file before and after a real CLI run). - test_research_readonly_commands_refuse_a_database_older_than_this_binary -- removing the `if current < SCHEMA_VERSION` guard: `AssertionError: ['research', 'significance', '--from', 'deployment'] should refuse a stale-schema database / assert 0 != 0` - test_research_readonly_commands_refuse_a_missing_database -- removing the explicit existence check (sqlite3's own mode=ro refusal alone is not a message naming the file or the reason): `AssertionError: assert 'no database at' in ''` against `Result OperationalError('unable to open database file')`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2
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.
Closes #610.
Premise checked against origin/main
Confirmed:
keel research significance --from deployment,factorsandindependenceall reach their data through_open_repo(keel/commands/_common.py), which callsmigrate(conn), andmigratecommits unconditionally.keel research pooled-review, in the same file, already connects every profile dbmode=rovia its own_connect_roand never migrates. The inconsistency the issue describes is real and unchanged since filing.The two decisions the issue left open
1. What a read-only command should do against a database that needs migrating. Refuse, before any data is read. A database stamped below this binary's
SCHEMA_VERSIONis a shape this binary was never tested against -- reading it quietly would be answering a question with evidence of unknown provenance. The issue's own instinct (refusal as operator error, not an evidence refusal -- #601's distinction) is what I implemented:_open_repo_roraises aClickExceptionnaming the stored version, this binary's version, and the remedy (run a command that writes, e.g. keel migrate, or run a binary that matches its version) rather than silently reading a schemaRepositorywas never built against.2. How wide to take the seam. The seam (
_open_repo_roinkeel/commands/_common.py) plus the threeresearch/commands, exactly as the issue's own inclination suggested -- nothing ininsights,status,pnl,activitywas touched. They share the same read-write shape and are explicitly left as a follow-up once this seam has a track record, per the issue's own "doing all of them is a bigger change than this issue needs" framing.What changed
keel/commands/_common.py--_open_repo_ro(ctx):pooled-review's ownmode=roURI shape, promoted to a shared seam, with two refusals before any data is read: a missing file (mirrorskeel/mcp/tools.py::_open_readonly_repo's existence check --mode=roalone already refuses to create the file, but the rawsqlite3.OperationalErrornames neither the file nor the reason) and a stale schema version (decision 1, above).keel/commands/research.py--research_significance,research_factors,research_independencenow call_open_repo_roinstead of_open_repo.pooled-review's own_connect_ro/read_orders/read_ledgerare untouched -- they already had the right shape.docs/research-toolkit.md-- the "which of these open your database read-write" hazard section is rewritten to state what's now true (all four research commands that touchorders/trade_outcomesare read-only, the stale-schema refusal message, and the explicit remaining-scope boundary).tests/commands/test_research_commands.py-- three new tests (below).Tests, mutation-verified (each restored after)
test_significance_factors_independence_never_write_to_the_database-- the same shape as the existingtest_pooled_review_never_writes_to_the_profile_dbs: hash the db file before/after a real CLI invocation of each of the three commands.test_research_readonly_commands_refuse_a_database_older_than_this_binary-- builds a db migrated to the current version, then rewrites the stamp down by one. Mutation: removing theif current < SCHEMA_VERSIONguard ->test_research_readonly_commands_refuse_a_missing_database-- pins the message, not just the exit code, becausemode=roagainst a missing path already refuses on its own (baresqlite3.OperationalError). Mutation: removing the explicit existence check ->Results
uv run pytest -q: 4570 passed, 3 skipped in 149.30suv run ruff check keel tests packages: All checks passed!uv run mypy: Success: no issues found in 359 source files🤖 Generated with Claude Code
https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2