Epic: #148 · Closes C-63, #145 · Resolves C-29 · Unblocks S2–S8
Background
The frame-native contract path won on 2026-07-27 (run-0, global-land, FAO served since). The pandas path it replaced was kept behind a config fork "until run 0 proves the contract path live" (register C-40) and never removed. This story is that removal. It is not new work — it is the named post-run-0 follow-up, four days overdue, and D-11 warned that if it lingers the duplication becomes permanent.
Two independent dispatch axes, four theoretical modes, one used:
| Axis |
Gate |
Declared by |
| Historical representation |
declared_data_format(queryset) == "feature_frame" (unfao.py:130) |
views-models config_queryset.py:62 |
| Delivery protocol |
configs.get("wire_contract") (:233, :294, :323, :409, :520) |
views-models config_meta.py:26 |
Production declares both. Omitting either silently selects the retired half — that is the inference ADR-003 forbids.
Work
Delete the legacy leg at every fork:
_read_forecast_data :236-270 — legacy body; keep the contract call
_read_historical_data :134-146 — pandas leg; _read_historical_frame becomes the reader
_transform :288-298 — legacy leg (geography attaches at artifact build)
_validate :300-348 — both legacy legs; keep the contract assertion + _check_coverage
_check_coverage :414-418 — legacy forecast branch
_save :523-577 — body becomes return self._save_contract()
_append_metadata :276-286, _delivery_description :619-636 — legacy-only
LEGACY_FORECAST_FILTERS :29-34
_historical_coverage_source :579+ — collapse to the frame branch
- imports
:5, :7, :14 — read_dataframe, PGMDataset, pandas if unreferenced
Add the single refusal. One place, at the manager's entry, raising and naming the missing key — not .get() with a silent default. Follow the shape of appwrite_env.assert_env_declared (unfao/appwrite_env.py:41), which is this repo's existing fail-loud-on-missing-declaration idiom.
Reuse, do not rewrite: _ContractStorePort (:37-77) already carries the fail-loud upload guard that #145 asks for; _unfao_appwrite_config() (:499) already builds the config the deleted legacy body duplicated verbatim; _save_contract (:442) is the surviving path.
Do not touch unfao/enrichment.py or unfao/extraction.py in this story — S3 and S4 handle them once the callers are gone.
Acceptance criteria
Testing / validation
ruff check .; pytest -q --deselect tests/test_datafactory_deploy_readiness.py — the 5 known local pyarrow byte-parity failures only, no new ones
- Four test files couple to legacy-only symbols and need rework, not deletion:
test_append_metadata.py (12 hits), test_selection_guard.py (3), test_input_integrity_e2e.py (3), test_falsification_fao_notes_relevance.py (1)
test_selection_guard.py pins LEGACY_FORECAST_FILTERS as an ADR-013 §11.4 golden string. Re-point it at the ADR's post-adoption record — do not drop the guarantee, the historical fact stays pinned after the code goes.
Risk
The legacy path has no end-to-end test, so it could already be broken and nothing would report it. That cuts both ways: it is also why deleting it is low-risk — unreachability is established by config (config_meta.py:26, config_queryset.py:62), not by test.
ADR-013 §11.4 is satisfied, not violated: it required a type-aware guard live before the first contract upload — both guards merged 2026-07-15, run-0 uploaded 2026-07-27. Deleting the legacy reader retires the selector §11.4 was protecting.
Epic: #148 · Closes C-63, #145 · Resolves C-29 · Unblocks S2–S8
Background
The frame-native contract path won on 2026-07-27 (run-0, global-land, FAO served since). The pandas path it replaced was kept behind a config fork "until run 0 proves the contract path live" (register C-40) and never removed. This story is that removal. It is not new work — it is the named post-run-0 follow-up, four days overdue, and D-11 warned that if it lingers the duplication becomes permanent.
Two independent dispatch axes, four theoretical modes, one used:
declared_data_format(queryset) == "feature_frame"(unfao.py:130)config_queryset.py:62configs.get("wire_contract")(:233, :294, :323, :409, :520)config_meta.py:26Production declares both. Omitting either silently selects the retired half — that is the inference ADR-003 forbids.
Work
Delete the legacy leg at every fork:
_read_forecast_data:236-270— legacy body; keep the contract call_read_historical_data:134-146— pandas leg;_read_historical_framebecomes the reader_transform:288-298— legacy leg (geography attaches at artifact build)_validate:300-348— both legacy legs; keep the contract assertion +_check_coverage_check_coverage:414-418— legacy forecast branch_save:523-577— body becomesreturn self._save_contract()_append_metadata:276-286,_delivery_description:619-636— legacy-onlyLEGACY_FORECAST_FILTERS:29-34_historical_coverage_source:579+— collapse to the frame branch:5, :7, :14—read_dataframe,PGMDataset,pandasif unreferencedAdd the single refusal. One place, at the manager's entry, raising and naming the missing key — not
.get()with a silent default. Follow the shape ofappwrite_env.assert_env_declared(unfao/appwrite_env.py:41), which is this repo's existing fail-loud-on-missing-declaration idiom.Reuse, do not rewrite:
_ContractStorePort(:37-77) already carries the fail-loud upload guard that #145 asks for;_unfao_appwrite_config()(:499) already builds the config the deleted legacy body duplicated verbatim;_save_contract(:442) is the surviving path.Do not touch
unfao/enrichment.pyorunfao/extraction.pyin this story — S3 and S4 handle them once the callers are gone.Acceptance criteria
unfao/managers/unfao.py< 450 lines (from 636)wire_contract/data_formatbranches outside the single refusal#145's silent uploads are gone: nodsm.upload_data(...)whose result is discardedtest_hop_b_sink_e2e.pybyte-parity unchanged and green — proves the surviving path did not moveTesting / validation
ruff check .;pytest -q --deselect tests/test_datafactory_deploy_readiness.py— the 5 known local pyarrow byte-parity failures only, no new onestest_append_metadata.py(12 hits),test_selection_guard.py(3),test_input_integrity_e2e.py(3),test_falsification_fao_notes_relevance.py(1)test_selection_guard.pypinsLEGACY_FORECAST_FILTERSas an ADR-013 §11.4 golden string. Re-point it at the ADR's post-adoption record — do not drop the guarantee, the historical fact stays pinned after the code goes.Risk
The legacy path has no end-to-end test, so it could already be broken and nothing would report it. That cuts both ways: it is also why deleting it is low-risk — unreachability is established by config (
config_meta.py:26,config_queryset.py:62), not by test.ADR-013 §11.4 is satisfied, not violated: it required a type-aware guard live before the first contract upload — both guards merged 2026-07-15, run-0 uploaded 2026-07-27. Deleting the legacy reader retires the selector §11.4 was protecting.