⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
packages/loopover-miner/lib/schema-version.ts's applySchemaMigrations is this package's shared
convention for evolving a SQLite store's schema safely: each migration runs inside its own
db.exec("BEGIN") / migration(db) / PRAGMA user_version = ... / db.exec("COMMIT") block, with
ROLLBACK on any thrown error — so a crash mid-migration leaves the file at the LAST fully-applied
version and resumes cleanly on the next open, per the function's own comment. Every other repo-
scoped store in this package that needs a post-creation schema change
(claim-ledger.ts's addApiBaseUrlScope, run-state.ts's addApiBaseUrlScope,
event-ledger.ts's addTenantIdColumn, policy-doc-cache.ts, policy-verdict-cache.ts,
governor-ledger.ts, prediction-ledger.ts) routes it through applySchemaMigrations.
packages/loopover-miner/lib/governor-state.ts never imports or calls applySchemaMigrations at
all. Instead, openGovernorState calls ensurePauseColumns(db) and
ensureReputationHistoryForgeScope(db) (around lines 265-274) as plain, un-transacted function
calls outside any migration framework. ensureReputationHistoryForgeScope (lines 217-242) performs
the same "SQLite can't ALTER a PRIMARY KEY, so rebuild the table" pattern as claim-ledger.ts's and
run-state.ts's addApiBaseUrlScope migrations (whose own comments cite the identical reasoning),
but as four separate, un-wrapped, autocommitted statements:
db.exec(`CREATE TABLE governor_reputation_history_v2 (...)`);
db.prepare(`INSERT OR IGNORE INTO governor_reputation_history_v2 (...) SELECT ... FROM governor_reputation_history`).run(...);
db.exec("DROP TABLE governor_reputation_history");
db.exec("ALTER TABLE governor_reputation_history_v2 RENAME TO governor_reputation_history");
If the process is killed between the DROP TABLE and the RENAME statements, the file is left
with NO governor_reputation_history table but a populated governor_reputation_history_v2. On
the next openGovernorState call, the un-conditional CREATE TABLE IF NOT EXISTS governor_reputation_history (lines ~267-273) silently recreates an EMPTY, old-shape table
(masking that the real data now sits, orphaned, under _v2); ensureReputationHistoryForgeScope
then sees the new empty table lacks api_base_url and tries CREATE TABLE governor_reputation_history_v2 again — which throws (the table already exists from the
interrupted run, and this statement has no IF NOT EXISTS). This makes openGovernorState throw
unconditionally on every subsequent call against that file — a permanently broken store requiring
manual database surgery — and the interim reputation-decay history is silently orphaned in the
unreferenced _v2 table.
Requirements
- Route
governor-state.ts's post-baseline schema evolution through applySchemaMigrations, the
same way every other repo-scoped store in this package does. Register ensurePauseColumns and
ensureReputationHistoryForgeScope (or their equivalents) as SchemaMigration functions passed
to applySchemaMigrations(db, [...]), so both get the same BEGIN/COMMIT/ROLLBACK
atomicity and resumable-versioning guarantee every sibling store already has.
- If either migration needs adjustment to be idempotent/resumable under the
applySchemaMigrations
contract (e.g. handling a partially-completed _v2 table left over from a PRE-FIX file that
already hit the crash scenario described above), handle that adjustment as part of this same fix
— a store that already has an orphaned _v2 table from before this fix must still be recoverable
by the fixed code, not just newly-safe going forward.
- Preserve the exact resulting schema (table/column names, data) for a store that migrates cleanly
— this issue is about transactional safety, not changing the target schema.
Deliverables
All deliverables are required in this one PR.
Test Coverage Requirements
This repo's Codecov patch gate is 99%+ (branch-counted) on packages/loopover-miner/lib/**. Add
the new regression tests to test/unit/miner-governor-state.test.ts (this package's tests live in
the shared root test/ directory, not packages/loopover-miner/test/**). The new
applySchemaMigrations-routed migration path, and its crash-recovery branch, must both be
exercised — a passing normal-migration test alone is not sufficient without the crash-simulation
test.
Expected Outcome
governor-state.ts's schema evolution has the same crash-safety guarantee every other store in
this package already has: a process killed mid-migration leaves the file in a recoverable state on
the next open, rather than a permanently unopenable file with orphaned data.
Links & Resources
packages/loopover-miner/lib/schema-version.ts — applySchemaMigrations (~lines 41-70), the
transactional convention to adopt.
packages/loopover-miner/lib/governor-state.ts — ensurePauseColumns (~line 193),
ensureReputationHistoryForgeScope (~lines 217-242), openGovernorState (~lines 244-274+).
packages/loopover-miner/lib/claim-ledger.ts / run-state.ts — their respective
addApiBaseUrlScope migrations, the closest existing precedent for the same
"rebuild the table" pattern already run safely inside applySchemaMigrations.
Context
packages/loopover-miner/lib/schema-version.ts'sapplySchemaMigrationsis this package's sharedconvention for evolving a SQLite store's schema safely: each migration runs inside its own
db.exec("BEGIN")/migration(db)/PRAGMA user_version = .../db.exec("COMMIT")block, withROLLBACKon any thrown error — so a crash mid-migration leaves the file at the LAST fully-appliedversion and resumes cleanly on the next open, per the function's own comment. Every other repo-
scoped store in this package that needs a post-creation schema change
(
claim-ledger.ts'saddApiBaseUrlScope,run-state.ts'saddApiBaseUrlScope,event-ledger.ts'saddTenantIdColumn,policy-doc-cache.ts,policy-verdict-cache.ts,governor-ledger.ts,prediction-ledger.ts) routes it throughapplySchemaMigrations.packages/loopover-miner/lib/governor-state.tsnever imports or callsapplySchemaMigrationsatall. Instead,
openGovernorStatecallsensurePauseColumns(db)andensureReputationHistoryForgeScope(db)(around lines 265-274) as plain, un-transacted functioncalls outside any migration framework.
ensureReputationHistoryForgeScope(lines 217-242) performsthe same "SQLite can't ALTER a PRIMARY KEY, so rebuild the table" pattern as
claim-ledger.ts's andrun-state.ts'saddApiBaseUrlScopemigrations (whose own comments cite the identical reasoning),but as four separate, un-wrapped, autocommitted statements:
If the process is killed between the
DROP TABLEand theRENAMEstatements, the file is leftwith NO
governor_reputation_historytable but a populatedgovernor_reputation_history_v2. Onthe next
openGovernorStatecall, the un-conditionalCREATE TABLE IF NOT EXISTS governor_reputation_history(lines ~267-273) silently recreates an EMPTY, old-shape table(masking that the real data now sits, orphaned, under
_v2);ensureReputationHistoryForgeScopethen sees the new empty table lacks
api_base_urland triesCREATE TABLE governor_reputation_history_v2again — which throws (the table already exists from theinterrupted run, and this statement has no
IF NOT EXISTS). This makesopenGovernorStatethrowunconditionally on every subsequent call against that file — a permanently broken store requiring
manual database surgery — and the interim reputation-decay history is silently orphaned in the
unreferenced
_v2table.Requirements
governor-state.ts's post-baseline schema evolution throughapplySchemaMigrations, thesame way every other repo-scoped store in this package does. Register
ensurePauseColumnsandensureReputationHistoryForgeScope(or their equivalents) asSchemaMigrationfunctions passedto
applySchemaMigrations(db, [...]), so both get the sameBEGIN/COMMIT/ROLLBACKatomicity and resumable-versioning guarantee every sibling store already has.
applySchemaMigrationscontract (e.g. handling a partially-completed
_v2table left over from a PRE-FIX file thatalready hit the crash scenario described above), handle that adjustment as part of this same fix
— a store that already has an orphaned
_v2table from before this fix must still be recoverableby the fixed code, not just newly-safe going forward.
— this issue is about transactional safety, not changing the target schema.
Deliverables
governor-state.tsroutes its schema evolution throughapplySchemaMigrations, matchingevery sibling store in this package.
DROP TABLEandRENAMEsteps (e.g. byinjecting a throwing statement at that point, or by manually constructing a database file
already in that intermediate state) and asserting that
openGovernorStateeither recoverscleanly or fails in a way that does NOT leave the store permanently unopenable on a
subsequent retry.
the reputation-history data that was present before the interrupted migration (i.e. the data
that ended up in the orphaned
_v2table, if the fix's recovery path is triggered, is notsimply discarded).
All deliverables are required in this one PR.
Test Coverage Requirements
This repo's Codecov patch gate is 99%+ (branch-counted) on
packages/loopover-miner/lib/**. Addthe new regression tests to
test/unit/miner-governor-state.test.ts(this package's tests live inthe shared root
test/directory, notpackages/loopover-miner/test/**). The newapplySchemaMigrations-routed migration path, and its crash-recovery branch, must both beexercised — a passing normal-migration test alone is not sufficient without the crash-simulation
test.
Expected Outcome
governor-state.ts's schema evolution has the same crash-safety guarantee every other store inthis package already has: a process killed mid-migration leaves the file in a recoverable state on
the next open, rather than a permanently unopenable file with orphaned data.
Links & Resources
packages/loopover-miner/lib/schema-version.ts—applySchemaMigrations(~lines 41-70), thetransactional convention to adopt.
packages/loopover-miner/lib/governor-state.ts—ensurePauseColumns(~line 193),ensureReputationHistoryForgeScope(~lines 217-242),openGovernorState(~lines 244-274+).packages/loopover-miner/lib/claim-ledger.ts/run-state.ts— their respectiveaddApiBaseUrlScopemigrations, the closest existing precedent for the same"rebuild the table" pattern already run safely inside
applySchemaMigrations.