diff --git a/alembic/versions/030_drop_legacy_fundamentals_tombstones.py b/alembic/versions/030_drop_legacy_fundamentals_tombstones.py new file mode 100644 index 0000000..10013e9 --- /dev/null +++ b/alembic/versions/030_drop_legacy_fundamentals_tombstones.py @@ -0,0 +1,71 @@ +"""Drop the A6 rollback tombstones + +Revision ID: 030 +Revises: 029 +Create Date: 2026-08-07 00:00:00.000000 + +Migration ``029`` kept two SystemSetting rows alive as rollback tombstones, +pinned to the values a pre-A6 process needed to behave safely. A6 is deployed +and healthy, and the provider keys are gone from the production ``.env`` — which +makes the legacy collector inert regardless of any settings row — so the +tombstones have no remaining job. + +Nothing in the current codebase reads either key. +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = "030" +down_revision: Union[str, None] = "029" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +# The safe values 029 pinned. Kept here so downgrade restores real protection +# rather than leaving a rolled-back process reading absent rows permissively. +_TOMBSTONES: dict[str, str] = { + "fundamental_data_sec_dolt_cutover_enabled": "true", + "job_fundamental_collector_enabled": "false", +} + +_settings = sa.table( + "system_settings", + sa.column("id", sa.Integer), + sa.column("key", sa.String), + sa.column("value", sa.Text), + sa.column("updated_at", sa.DateTime(timezone=True)), +) + + +def upgrade() -> None: + conn = op.get_bind() + for key in _TOMBSTONES: + row = conn.execute( + sa.select(_settings.c.value).where(_settings.c.key == key) + ).fetchone() + if row is None: + print(f"a6_tombstone_drop {key}: absent", flush=True) + continue + print(f"a6_tombstone_drop {key}: {row[0]!r}", flush=True) + conn.execute(sa.delete(_settings).where(_settings.c.key == key)) + + +def downgrade() -> None: + """Restore the tombstones at their safe values. + + Unlike 029's no-op downgrade, this one is meaningful: going back past this + revision implies going back toward code that still reads these keys. + """ + conn = op.get_bind() + now = sa.func.now() + for key, pinned in _TOMBSTONES.items(): + exists = conn.execute( + sa.select(_settings.c.id).where(_settings.c.key == key) + ).fetchone() + if exists is None: + conn.execute( + sa.insert(_settings).values(key=key, value=pinned, updated_at=now) + ) diff --git a/docs/dolt-integration-plan.md b/docs/dolt-integration-plan.md index 0fed5a6..6d8e859 100644 --- a/docs/dolt-integration-plan.md +++ b/docs/dolt-integration-plan.md @@ -432,9 +432,10 @@ workstream B — Alpaca remains the price source throughout. remaining production action is flipping that switch on and observing it. - A6. **DONE 2026-08-07.** FMP/Finnhub/Alpha Vantage removed, along with the weekly `fundamental_collector` job, the A5 cutover toggle (SEC+Dolt is now the - unconditional path) and the parity report. Migration `029` tombstones the two - behavior-bearing settings rows for the rollback window; the archived parity - bundles stay as the A5 evidence trail. + unconditional path) and the parity report. Migration `029` tombstoned the two + behavior-bearing settings rows for the rollback window and `030` dropped them + once the deploy was confirmed healthy; the archived parity bundles stay as the + A5 evidence trail. **Workstream B (independent, start when wanted):** @@ -514,8 +515,8 @@ observed in production, so the legacy providers, their config/env keys, the week collector job and the parity report were all removed. Two consequences to carry: (1) `fundamental_data` now has no provider fallback — recovery is restore-from-backup; (2) disabling **SEC Fundamentals Import** stops the SEC fetch only, because the local -cache refresh was deliberately moved outside the job-enable check. Remaining -follow-up: delete the migration-029 tombstone rows once the rollback window closes. +cache refresh was deliberately moved outside the job-enable check. No follow-ups +remain: migration `030` dropped the tombstone rows after the deploy was verified. **Known caveats to carry (documented in the findings report, not bugs to fix):** - KLAC-class post-filing splits: P/E wrong until the next 10-Q; undetectable from diff --git a/docs/fundamentals-deployment.md b/docs/fundamentals-deployment.md index e20ae29..e64172c 100644 --- a/docs/fundamentals-deployment.md +++ b/docs/fundamentals-deployment.md @@ -180,21 +180,6 @@ WHERE dimension = 'fundamental' GROUP BY dimension, is_stale; ``` -### Retired settings rows (delete later) - -Migration `029` pinned two SystemSetting rows as rollback tombstones rather than -deleting them, because migrations run before the service restarts and a -rolled-back pre-A6 process reads an absent `job_..._enabled` row as *enabled*: - -| key | pinned value | -|---|---| -| `fundamental_data_sec_dolt_cutover_enabled` | `true` | -| `job_fundamental_collector_enabled` | `false` | - -Neither controls anything now; Admin -> Settings hides both. Once the A6 -rollback window has closed, delete the rows and the `MANAGED_SETTINGS` filter in -`frontend/src/components/admin/SettingsForm.tsx`. - ## Failure and rollback - **There is no provider fallback any more, and no Admin switch that freezes the diff --git a/frontend/src/components/admin/SettingsForm.tsx b/frontend/src/components/admin/SettingsForm.tsx index a4d7165..5e4f49b 100644 --- a/frontend/src/components/admin/SettingsForm.tsx +++ b/frontend/src/components/admin/SettingsForm.tsx @@ -3,14 +3,6 @@ import { useSettings, useUpdateSetting } from '../../hooks/useAdmin'; import { SkeletonTable } from '../ui/Skeleton'; import type { SystemSetting } from '../../lib/types'; -// Retired keys kept in the database as rollback tombstones (migration 029). -// They no longer control anything; hide them so nobody edits a dead switch. -// Delete both the rows and this filter once the A6 rollback window has closed. -const MANAGED_SETTINGS = new Set([ - 'fundamental_data_sec_dolt_cutover_enabled', - 'job_fundamental_collector_enabled', -]); - export function SettingsForm() { const { data: settings, isLoading, isError, error } = useSettings(); const updateSetting = useUpdateSetting(); @@ -40,11 +32,10 @@ export function SettingsForm() { if (isLoading) return ; if (isError) return

{(error as Error)?.message || 'Failed to load settings'}

; if (!settings || settings.length === 0) return

No settings found.

; - const visibleSettings = settings.filter((setting) => !MANAGED_SETTINGS.has(setting.key)); return (
- {visibleSettings.map((setting) => ( + {settings.map((setting) => (
{setting.key === 'registration' ? (