Epic: #85 · S3 · [UNILATERAL] · depends on #86, #87
Background
The forecast enters at _read_forecast_data as pd.read_parquet(io.BytesIO(...)) → PGMDataset (unfao.py:125-127), whose object-dtype cells already hold the sample arrays. Today those samples are never represented as a frame — the interior runs on pandas and the values are effectively collapsed. This story makes the interior sample-aware without touching either wire.
Work
- Immediately after the
PGMDataset is built, construct an internal PredictionFrame (via S1) from the forecast.
- Route the interior computations that consume the forecast (coverage cell counts, observed-range decisions where applicable, provenance counts) through the S2 frame-native readers.
- Both wires stay pandas: the Appwrite read is unchanged, and
_save still emits the same object-dtype parquet (the outbound change is S6). This step changes only vpp's internal representation.
Acceptance criteria
Parity / validation
Head-to-head on a captured forecast fixture (object-dtype-cell parquet): run the existing pandas path and the new frame path over the same bytes; assert identical coverage counts, identical fabricated-month clip decisions, identical provenance dict, and byte-identical emitted parquet (outbound unchanged this step).
Dependencies
Depends on S1 + S2. This is the unilateral slice of the gated forecast problem — it de-risks S6 by proving the frame path produces identical deliveries before any wire change.
Files
views_postprocessing/unfao/managers/unfao.py (_read_forecast_data, the interior call sites), tests (a captured forecast fixture + head-to-head).
Review amendment (expert-code-review, 2026-06-28) — don't deepen the manager
Introduce the frame conversion behind a small delivery source adapter the manager calls, rather than inlining frame-construction into _read_forecast_data.
- Why:
_read_forecast_data/_save already interleave infra + logic (register C-40); adding representation logic inline accretes a third responsibility (violates SRP/DIP). A thin source/sink seam is exactly C-40's thin-shell mitigation.
- Cross-ref C-40; keep the manager a thin orchestrator.
- Acceptance criteria addition:
Epic: #85 · S3 ·
[UNILATERAL]· depends on #86, #87Background
The forecast enters at
_read_forecast_dataaspd.read_parquet(io.BytesIO(...))→PGMDataset(unfao.py:125-127), whose object-dtype cells already hold the sample arrays. Today those samples are never represented as a frame — the interior runs on pandas and the values are effectively collapsed. This story makes the interior sample-aware without touching either wire.Work
PGMDatasetis built, construct an internalPredictionFrame(via S1) from the forecast._savestill emits the same object-dtype parquet (the outbound change is S6). This step changes only vpp's internal representation.Acceptance criteria
(N,S)PredictionFrame; interior invariants are computed from it.delivery/untouched.Parity / validation
Head-to-head on a captured forecast fixture (object-dtype-cell parquet): run the existing pandas path and the new frame path over the same bytes; assert identical coverage counts, identical fabricated-month clip decisions, identical
provenancedict, and byte-identical emitted parquet (outbound unchanged this step).Dependencies
Depends on S1 + S2. This is the unilateral slice of the gated forecast problem — it de-risks S6 by proving the frame path produces identical deliveries before any wire change.
Files
views_postprocessing/unfao/managers/unfao.py(_read_forecast_data, the interior call sites), tests (a captured forecast fixture + head-to-head).Review amendment (expert-code-review, 2026-06-28) — don't deepen the manager
Introduce the frame conversion behind a small delivery source adapter the manager calls, rather than inlining frame-construction into
_read_forecast_data._read_forecast_data/_savealready interleave infra + logic (register C-40); adding representation logic inline accretes a third responsibility (violates SRP/DIP). A thin source/sink seam is exactly C-40's thin-shell mitigation.PredictionFrameconversion lives behind a callable seam (a delivery source adapter), not inline in the manager method.