Scope
Tracking issue for PRP-35 — Forecast Intelligence A: Feature Frame V2 execution.
Roadmap: #295. PRP: PRPs/PRP-35-forecast-intelligence-A-feature-frame-v2.md.
Lands V2 as an additive, opt-in feature-frame surface alongside the frozen V1 contract:
- Shared V2 contract (38 default / 53 max columns across 11
FeatureGroups) +
V2HistoricalSidecar / V2FutureSidecar data carriers +
build_historical_feature_rows_v2 / build_future_feature_rows_v2 row builders.
POST /forecasting/train accepts optional feature_frame_version: int = 1 and
feature_groups: list[str] | None = None; the trained bundle persists
feature_frame_version, feature_columns, feature_groups,
feature_safety_classes, feature_pinned_constants in metadata.
POST /scenarios/simulate reads feature_frame_version from the loaded bundle
metadata and dispatches V1 vs V2 future-frame assembly transparently.
- Three LOAD-BEARING leakage specs land alongside the V1 spec:
app/shared/feature_frames/tests/test_leakage_v2.py
app/features/forecasting/tests/test_regression_features_v2_leakage.py
app/features/scenarios/tests/test_future_frame_v2_leakage.py
- No Alembic migration — V2 reads existing tables only, writes nothing.
- V1 bundles trained before this PRP load, predict, scenario-simulate, and
backtest unchanged.
Deferred follow-up: V2 backtesting dispatch
PRP-35 landed V2 training + scenarios + shared V2 builders. Backtesting V2
dispatch is deferred and tracked here.
Reason. Current backtesting (app/features/backtesting/service.py) trains
fresh per fold from BacktestConfig.model_config_main and does not load a
fitted bundle, so PRP-35 Task 13's literal instruction ("READ
feature_frame_version from the fitted bundle BEFORE the fold loop") cannot
apply as written. The correct opt-in surface is a request-time field on
BacktestConfig itself — a re-design Task 13 did not spec. Estimated ~400 LOC
source + ~150 LOC tests + one design call.
Follow-up scope:
- Add additive
feature_frame_version: int = 1 and feature_groups: list[str] | None = None
to BacktestConfig (frozen-safe), with a model_validator mirroring TrainRequest
(V1 + groups → 422; V2 + unknown group name → 422).
- Implement V2 fold feature construction:
- extend
ExogenousFrame (or introduce V2ExogenousFrame) with V2 sidecar
per-day arrays (inventory / replenishment / returns / promo kinds / weather / macro)
- branch
_build_historical_matrix and _run_feature_aware_fold on
feature_frame_version
- per-fold
V2FutureSidecar assembled by positional slicing of pre-loaded
full-series arrays at split.test_indices (leakage-critical alignment)
- fold-start log carries
feature_frame_version
- Add leakage-focused V2 backtesting tests:
tests/test_schemas.py — validator gates
tests/test_feature_aware_backtest.py — V2 column counts (38 default / 53 max);
V1 default path byte-stable
tests/test_feature_aware_backtest_v2.py (PRP-35 Task 18) — LOAD-BEARING:
window-aggregate columns NaN at j >= 2 in X_future
- Resolve loader-sharing /
app/shared design decision — the required async
loaders already exist in app/features/forecasting/v2_loaders.py. Importing
them into backtesting/service.py violates the vertical-slice rule. Two
options: (1) promote loaders + assemblers to
app/shared/feature_frames/loaders.py (recommended; cleanest;
scenarios slice can adopt them in a future refactor), or (2) duplicate the
SQL inline (mirrors scenarios/feature_frame.py precedent, doubles
maintenance).
The PRP-35 PR explicitly mentions this deferral.
Acceptance — PRP-35 (this issue)
Acceptance — Follow-up (V2 backtesting dispatch)
Scope
Tracking issue for PRP-35 — Forecast Intelligence A: Feature Frame V2 execution.
Roadmap: #295. PRP:
PRPs/PRP-35-forecast-intelligence-A-feature-frame-v2.md.Lands V2 as an additive, opt-in feature-frame surface alongside the frozen V1 contract:
FeatureGroups) +V2HistoricalSidecar/V2FutureSidecardata carriers +build_historical_feature_rows_v2/build_future_feature_rows_v2row builders.POST /forecasting/trainaccepts optionalfeature_frame_version: int = 1andfeature_groups: list[str] | None = None; the trained bundle persistsfeature_frame_version,feature_columns,feature_groups,feature_safety_classes,feature_pinned_constantsinmetadata.POST /scenarios/simulatereadsfeature_frame_versionfrom the loaded bundlemetadata and dispatches V1 vs V2 future-frame assembly transparently.
app/shared/feature_frames/tests/test_leakage_v2.pyapp/features/forecasting/tests/test_regression_features_v2_leakage.pyapp/features/scenarios/tests/test_future_frame_v2_leakage.pybacktest unchanged.
Deferred follow-up: V2 backtesting dispatch
PRP-35 landed V2 training + scenarios + shared V2 builders. Backtesting V2
dispatch is deferred and tracked here.
Reason. Current backtesting (
app/features/backtesting/service.py) trainsfresh per fold from
BacktestConfig.model_config_mainand does not load afitted bundle, so PRP-35 Task 13's literal instruction ("READ
feature_frame_version from the fitted bundle BEFORE the fold loop") cannot
apply as written. The correct opt-in surface is a request-time field on
BacktestConfigitself — a re-design Task 13 did not spec. Estimated ~400 LOCsource + ~150 LOC tests + one design call.
Follow-up scope:
feature_frame_version: int = 1andfeature_groups: list[str] | None = Noneto
BacktestConfig(frozen-safe), with amodel_validatormirroringTrainRequest(V1 + groups → 422; V2 + unknown group name → 422).
ExogenousFrame(or introduceV2ExogenousFrame) with V2 sidecarper-day arrays (inventory / replenishment / returns / promo kinds / weather / macro)
_build_historical_matrixand_run_feature_aware_foldonfeature_frame_versionV2FutureSidecarassembled by positional slicing of pre-loadedfull-series arrays at
split.test_indices(leakage-critical alignment)feature_frame_versiontests/test_schemas.py— validator gatestests/test_feature_aware_backtest.py— V2 column counts (38 default / 53 max);V1 default path byte-stable
tests/test_feature_aware_backtest_v2.py(PRP-35 Task 18) — LOAD-BEARING:window-aggregate columns NaN at
j >= 2in X_futureapp/shareddesign decision — the required asyncloaders already exist in
app/features/forecasting/v2_loaders.py. Importingthem into
backtesting/service.pyviolates the vertical-slice rule. Twooptions: (1) promote loaders + assemblers to
app/shared/feature_frames/loaders.py(recommended; cleanest;scenarios slice can adopt them in a future refactor), or (2) duplicate the
SQL inline (mirrors
scenarios/feature_frame.pyprecedent, doublesmaintenance).
The PRP-35 PR explicitly mentions this deferral.
Acceptance — PRP-35 (this issue)
TrainRequest.feature_frame_version=2/scenarios/simulatedispatch via bundle metadataAcceptance — Follow-up (V2 backtesting dispatch)
BacktestConfiggainsfeature_frame_version+feature_groups_run_feature_aware_foldV2 branch with per-foldV2FutureSidecarslicingtests/test_v2_loaders.py— PRP-35 Task 15) lands