chore: drop the A6 rollback tombstones
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m23s
Deploy / deploy (push) Successful in 39s

A6 deployed cleanly and the provider keys are gone from the production `.env`,
which makes the legacy collector inert regardless of any settings row. The two
tombstones migration 029 pinned have no remaining job, and nothing in the
codebase reads either key.

Migration 030 deletes them and drops the Admin filter that hid them. Unlike
029's, its downgrade is meaningful — it restores both rows at their safe values,
since going back past this revision means going back toward code that reads them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 12:34:28 +02:00
co-authored by Claude Opus 5
parent e1607ddbff
commit c2a3b56aaa
4 changed files with 78 additions and 30 deletions
@@ -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)
)
+6 -5
View File
@@ -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
-15
View File
@@ -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
+1 -10
View File
@@ -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 <SkeletonTable rows={4} cols={2} />;
if (isError) return <p className="text-sm text-red-400">{(error as Error)?.message || 'Failed to load settings'}</p>;
if (!settings || settings.length === 0) return <p className="text-sm text-gray-500">No settings found.</p>;
const visibleSettings = settings.filter((setting) => !MANAGED_SETTINGS.has(setting.key));
return (
<div className="space-y-4">
{visibleSettings.map((setting) => (
{settings.map((setting) => (
<div key={setting.key} className="glass p-4 flex flex-wrap items-center gap-3 glass-hover">
<label className="min-w-[140px] text-sm font-medium text-gray-300">{setting.key}</label>
{setting.key === 'registration' ? (