core_data_updater.jobs.metadata
jobs/metadata.py — MetadataFlow: the ROOT island (everything reads its tables).
Design record: METADATA_FLOW.md (pre-stamped from the records; §7 decisions taken per recommendation, 2026-08-16). Shape: DAILY metadata_daily the roster heartbeat: universes (land_universe + stamp_tradable) · bounded SEC CIK resolution · catalogs · constituent diffs — cron 00:15 UTC Tue-Sat, born STOPPED WEEKLY metadata_weekly venues · calendar re-union · SEC/info enrichers — cron Sat 06:00 UTC, born STOPPED BUILD metadata_build the genesis run that unblocks prices + events — manual, max_concurrent=1 RECONCILE metadata_reconcile the rename hole, closed off events’ landed symbol_changes — fired by the AFTER gate on that asset ALONE, born STOPPED BACKFILL metadata_backfill the three enrichment holes — cron 10:00 UTC, born STOPPED
Laws carried here: configs read LIVE per step (per-step rows, §7-D1; enabled
comes back VISIBLE); every landing through CoreDataRepo — land() on the
METADATA policies (NULL_KEY drops undated windows/null-cik names, counted),
land_universe for the incarnation tables (three-strata resolution, the
transition rule, ABSENCE-NEVER-FLIPS, half-assembly refusal), the merge verbs
for enrichment (entity fill-never-overwrite · etf-info fresh-fact-wins on the
ACTIVE row) and mutate_windows for atomic close+open membership changes — this file
carries ZERO SQL, ZERO wire parsing, ZERO conversion. State-as-params: stored
open windows, CIK maps and rosters are READ from raw tables and passed to
feed verbs. Anchor-failure ≠ no-changes: a None diff skips that index NAMED
and closes NOTHING. Cron-primary by design (§5.4) — the root island has no
grounded probe; downstream sensors compose off these asset keys instead.
Classes
MetadataFlow
Bases: CoreDataFlow
The metadata flow — METADATA_FLOW.md §5, method for method.
backfill_entity_info
backfill_entity_info(cfg) -> NoneThe EDGAR hole: ACTIVE equities carrying a cik but NO sic.
backfill_etf_holdings
backfill_etf_holdings(cfg) -> NoneThe N-PORT hole: ACTIVE etfs with NO holdings rows at all (the absent-rows arm of enrichment_debt — the child table IS the fact).
backfill_etf_info
backfill_etf_info(cfg) -> NoneThe etf/info hole: ACTIVE etfs with no etf_company fact landed.
build_calendar
build_calendar(cfg) -> Nonebuild_constituents
build_constituents(cfg) -> NoneFull event-history windows (GSPC → 1957) — replace scoped to the SERVED index codes (§7-D2: stale-free rebuilds). The stored open windows are passed into the feed as a destructive-write baseline: a 200-empty/truncated current anchor fails before exact replacement.
build_crypto_catalog
build_crypto_catalog(cfg) -> Nonebuild_entity_info
build_entity_info(cfg) -> Nonebuild_equity_ciks
build_equity_ciks(cfg) -> Nonebuild_etf_ciks
build_etf_ciks(cfg) -> Nonebuild_etf_holdings
build_etf_holdings(cfg) -> Nonebuild_etf_info
build_etf_info(cfg) -> Nonebuild_exchanges
build_exchanges(cfg) -> Nonebuild_index_catalog
build_index_catalog(cfg) -> Nonebuild_universe_equities
build_universe_equities(cfg) -> Nonebuild_universe_etfs
build_universe_etfs(cfg) -> Nonereconcile_renames
reconcile_renames(cfg) -> NoneClose the OLD incarnation of every CORROBORATED rename — the
ABSENCE-NEVER-FLIPS hole (§4, §9): the roster assembly can only see
that FB stopped appearing, and absence must never flip a row, so the
dead ticker stays is_active forever. events’ landed
raw.symbol_changes is the witness; close_renamed_incarnations
carries the composed UPDATE and both guards (already-inactive rows
untouched · no new_symbol row = no close).
Reconciling off a STALE rename stream is the one way this step can be
wrong — which is why it has its OWN job (metadata_reconcile, 2026-
08-17), fired by the AFTER gate on the symbol_changes asset and by
nothing else. It used to ride metadata_backfill, and that dragged the
three enrichment legs along on every rename landing: only THIS step
needs a fresh symbol_changes.
refresh_calendar
refresh_calendar(cfg) -> Noneupdate_constituents
update_constituents(cfg) -> NoneStored open windows → fresh anchors → atomically close+open.
Each index’s departure instructions and new-window feed rows enter one repository mutation. Its SQL statement is the transaction boundary: an opening failure rolls back the closes for that index. The feed receipt still drives fatal/gap verdicts and the opening/closing counts retain their distinct monitoring stages.
A None diff = the anchor FAILED: that index skips NAMED and closes NOTHING (vendor down ≠ everyone left the index).
update_crypto_catalog
update_crypto_catalog(cfg) -> Noneupdate_entity_info
update_entity_info(cfg) -> Noneupdate_equity_ciks
update_equity_ciks(cfg) -> Noneupdate_etf_ciks
update_etf_ciks(cfg) -> Noneupdate_etf_holdings
update_etf_holdings(cfg) -> Noneupdate_etf_info
update_etf_info(cfg) -> Noneupdate_exchanges
update_exchanges(cfg) -> Noneupdate_index_catalog
update_index_catalog(cfg) -> Noneupdate_universe_equities
update_universe_equities(cfg) -> Noneupdate_universe_etfs
update_universe_etfs(cfg) -> None