Repository navigation
[EPIC] ADR-013 Hop-B sink leg: frame-native FAO delivery (shards + sidecar + run manifest + gates) #105
Description
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
- All 8 stories closed; suite + 41 ADR guards + design-contract tests green.
- e2e: fixture Track-A in → byte-identical Hop-B artifacts out (shard, sidecar, manifest), §6 gate invoked, upload order shards→sidecar→manifest-last, zero store calls at default config.
- ADR-013 post-adoption + risk register updated; FAO delivery path is point/DataFrame-based — won't carry rusty_bucket's pooled draws #45 closed as delivered; seats notified.
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
Activity
Merge pull request #122 from views-platform/feat/hop-b-s8-closeout
Polichinel commentedon Jul 20, 2026
Epic complete (2026-07-20). All 8 stories shipped (PRs #115–#122): the Hop-B sink exists, fixture-proven byte-for-byte, upload-disabled behind the §11.4 interlock. See the tracking issue #114 (all boxes checked) and ADR-013's Post-adoption record for the shipped-state entry. Live enablement awaits faoapi C-161.
🤖 Generated with Claude Code