Problem
The Hop-B half of ADR-013 does not exist. Today's outbound _save (unfao/managers/unfao.py:283-329) uploads a pandas point-estimate parquet under the ensemble's document name — the confirmed F1 invisibility: faoapi's unconditional name filter has never been able to see a forecast uploaded this way. Meanwhile everything upstream is built: pipeline-core's Hop-A publish leg ships Track-A archives, vpp's track_a_source.py verifies and assembles them, the §6 draws gate is merged (but wired nowhere), and the §10 golden fixture pins every byte the sink must produce.
Why now
The wire exists end-to-end in code from PFE to the internal store. This epic is the missing middle. The C-161 constraint (faoapi's Hop-B guard must reach production before any contract upload) orders the finish, not the building: the sink ships upload-disabled by default (ADR-013 §11.4 interlock) and cannot touch the live bucket until explicitly enabled after C-161 closes.
End state
A composed, fixture-proven Hop-B sink: contract-mode inbound (Hop-A manifest selection, await-all-configured-targets, verified assembly) → §6 no-collapse gate → per-(target,month) arrow shards → §5 sidecar (+ gid parity) → ONE run manifest uploaded LAST → all documents under name="un_fao" with §4.1a fields — with the e2e proof that fixture Track-A artifacts in produce byte-identical fixture Hop-B artifacts out. UPLOAD_ENABLED=False throughout the epic.
Layout (maintainer-approved 2026-07-20)
New subpackage views_postprocessing/unfao/wire/ (naming, header, shard, sidecar, run_manifest, source_selection, sink) + unfao/product.py (the declared FAO product: targets, S_MIN, consumer document name, upload interlock) + delivery/parity.py (gid-set parity invariant, pandas-free). Manager gains a thin declared contract-mode branch only. Principles: wire/ = one closure (CCP/CRP, screams the contract); product separate (SRP — changes with the partner product); delivery/ ← seams ← wire/ ← manager (SDP/ADP); sink composed with an injected store port (DIP).
In scope
Stories S1–S8 (linked below): product+naming, header, shard writer, sidecar+parity, run manifest, source selection, sink+wiring+e2e, closeout.
Out of scope
Legacy-path deletion (later story); S7/#92 historical inbound; partner generalization (#97 — WET stands); repo rename (#96 / D-12); track_a_source.py relocation into wire/ (future mechanical move); live upload enablement (gated on C-161).
Epic acceptance criteria
Supersedes #91 (S6 outbound) and #88 (S3 convert-at-the-door); resolves #45. Contract: docs/ADRs/013_sampled_forecast_wire_contract.md.
🤖 Generated with Claude Code
Problem
The Hop-B half of ADR-013 does not exist. Today's outbound
_save(unfao/managers/unfao.py:283-329) uploads a pandas point-estimate parquet under the ensemble's document name — the confirmed F1 invisibility: faoapi's unconditionalnamefilter has never been able to see a forecast uploaded this way. Meanwhile everything upstream is built: pipeline-core's Hop-A publish leg ships Track-A archives, vpp'strack_a_source.pyverifies and assembles them, the §6 draws gate is merged (but wired nowhere), and the §10 golden fixture pins every byte the sink must produce.Why now
The wire exists end-to-end in code from PFE to the internal store. This epic is the missing middle. The C-161 constraint (faoapi's Hop-B guard must reach production before any contract upload) orders the finish, not the building: the sink ships upload-disabled by default (ADR-013 §11.4 interlock) and cannot touch the live bucket until explicitly enabled after C-161 closes.
End state
A composed, fixture-proven Hop-B sink: contract-mode inbound (Hop-A manifest selection, await-all-configured-targets, verified assembly) → §6 no-collapse gate → per-(target,month) arrow shards → §5 sidecar (+ gid parity) → ONE run manifest uploaded LAST → all documents under
name="un_fao"with §4.1a fields — with the e2e proof that fixture Track-A artifacts in produce byte-identical fixture Hop-B artifacts out.UPLOAD_ENABLED=Falsethroughout the epic.Layout (maintainer-approved 2026-07-20)
New subpackage
views_postprocessing/unfao/wire/(naming, header, shard, sidecar, run_manifest, source_selection, sink) +unfao/product.py(the declared FAO product: targets, S_MIN, consumer document name, upload interlock) +delivery/parity.py(gid-set parity invariant, pandas-free). Manager gains a thin declared contract-mode branch only. Principles: wire/ = one closure (CCP/CRP, screams the contract); product separate (SRP — changes with the partner product); delivery/ ← seams ← wire/ ← manager (SDP/ADP); sink composed with an injected store port (DIP).In scope
Stories S1–S8 (linked below): product+naming, header, shard writer, sidecar+parity, run manifest, source selection, sink+wiring+e2e, closeout.
Out of scope
Legacy-path deletion (later story); S7/#92 historical inbound; partner generalization (#97 — WET stands); repo rename (#96 / D-12);
track_a_source.pyrelocation into wire/ (future mechanical move); live upload enablement (gated on C-161).Epic acceptance criteria
Supersedes #91 (S6 outbound) and #88 (S3 convert-at-the-door); resolves #45. Contract:
docs/ADRs/013_sampled_forecast_wire_contract.md.🤖 Generated with Claude Code