From e2f4f1c25971d3a174e284e6a2e17bf84acb0f92 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 20 Jul 2026 01:54:11 +0200 Subject: [PATCH] =?UTF-8?q?feat(wire):=20S1/#106=20=E2=80=94=20declared=20?= =?UTF-8?q?FAO=20product=20(unfao/product.py)=20+=20Hop-B=20name=20templat?= =?UTF-8?q?es=20(wire/naming.py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The §4.2a configuration home settles as code: TARGETS (wire vocabulary), S_MIN=2 (skeleton floor), CONSUMER_DOCUMENT_NAME='un_fao' (§4.1a pin), UPLOAD_ENABLED=False (§11.4 interlock seed — the default configuration is provably unable to touch the live bucket). wire/ opens as the one-closure ADR-013 subpackage; naming.py carries the §4.1b templates as locators-only functions, golden-string-tested against the §10 fixture's actual file names. Part of epic #105. Closes #106. Co-Authored-By: Claude Fable 5 --- tests/test_product.py | 22 +++++++++++++ tests/test_wire_naming.py | 30 +++++++++++++++++ views_postprocessing/unfao/product.py | 36 +++++++++++++++++++++ views_postprocessing/unfao/wire/__init__.py | 8 +++++ views_postprocessing/unfao/wire/naming.py | 31 ++++++++++++++++++ 5 files changed, 127 insertions(+) create mode 100644 tests/test_product.py create mode 100644 tests/test_wire_naming.py create mode 100644 views_postprocessing/unfao/product.py create mode 100644 views_postprocessing/unfao/wire/__init__.py create mode 100644 views_postprocessing/unfao/wire/naming.py diff --git a/tests/test_product.py b/tests/test_product.py new file mode 100644 index 0000000..ab4b0a8 --- /dev/null +++ b/tests/test_product.py @@ -0,0 +1,22 @@ +"""The declared FAO product (unfao/product.py) — ADR-013 §4.2a/§6/§4.1a/§11.4 pins.""" + +from views_postprocessing.unfao import product + + +def test_targets_are_the_pinned_wire_vocabulary(): + # §7a: wire names, never internal model names; order stable for manifests. + assert product.TARGETS == ("lr_ged_sb", "lr_ged_ns", "lr_ged_os") + + +def test_s_min_is_the_walking_skeleton_floor(): + assert product.S_MIN == 2 + + +def test_consumer_document_name_is_the_faoapi_pin(): + # §4.1a: any other name is invisible to faoapi's unconditional name filter. + assert product.CONSUMER_DOCUMENT_NAME == "un_fao" + + +def test_upload_interlock_defaults_off(): + # §11.4: the default configuration must be unable to touch the live bucket. + assert product.UPLOAD_ENABLED is False diff --git a/tests/test_wire_naming.py b/tests/test_wire_naming.py new file mode 100644 index 0000000..943dad5 --- /dev/null +++ b/tests/test_wire_naming.py @@ -0,0 +1,30 @@ +"""Hop-B name templates (wire/naming.py) — golden-string tests against the §10 +fixture's actual file names (the templates' executable pin).""" + +from pathlib import Path + +from views_postprocessing.unfao.wire import naming + +_FIX = Path(__file__).resolve().parent / "fixtures" / "wire_contract" + + +def test_shard_name_reproduces_the_fixture(): + name = naming.shard_name("fixture_run_0", "lr_ged_sb", 543) + assert name == "fixture_run_0__lr_ged_sb__m000543.arrow.parquet" + assert (_FIX / name).exists() + + +def test_run_manifest_name_reproduces_the_fixture(): + name = naming.run_manifest_name("fixture_run_0") + assert name == "fixture_run_0__manifest.json" + assert (_FIX / name).exists() + + +def test_sidecar_name_reproduces_the_fixture(): + name = naming.sidecar_name("fixture_run_0") + assert name == "fixture_run_0__sidecar.parquet" + assert (_FIX / name).exists() + + +def test_month_padding_is_six_digits(): + assert naming.shard_name("r", "t", 7).endswith("__m000007.arrow.parquet") diff --git a/views_postprocessing/unfao/product.py b/views_postprocessing/unfao/product.py new file mode 100644 index 0000000..c9bbcac --- /dev/null +++ b/views_postprocessing/unfao/product.py @@ -0,0 +1,36 @@ +"""The declared FAO delivery product (ADR-013 §4.2a's "views-postprocessing +configuration" — settled home per the 2026-07-19 CCP decision recorded in the ADR). + +Everything here is a **declaration**, never an inference: the delivery refuses to +ship until reality matches these constants, and changing the product is a human +decision plus an edit here — reviewed, git-historied, fail-loud. One reason to +change: the FAO partner relationship. + +The constants and their contract homes: + +- ``TARGETS`` — the expected target set (§4.2a): a run is *complete* only when every + target listed here has a manifested Hop-A leg. Adding a target follows Amendment + A1 (§7a): maintainer names it, producer's mapping gains an entry, this tuple + gains an entry. Wire vocabulary only — never internal model names. +- ``S_MIN`` — the §6 no-collapse floor passed to ``delivery.draws``. 2 is the + walking-skeleton value; the production pinning mechanism is an OPEN maintainer + item (ADR-013 Post-adoption record, 2026-07-19). +- ``CONSUMER_DOCUMENT_NAME`` — the §4.1a store-document ``name`` pin. faoapi's + query layer filters on it unconditionally; a document under any other name is + invisible to the consumer. Config-owned by views-faoapi; changing it is a + contract amendment. +- ``UPLOAD_ENABLED`` — the §11.4 upload interlock: ``False`` means the sink writes + artifacts locally and never calls the store. Overriding requires an explicit + launch-config declaration (wired in the sink story), and the first live + enablement is gated on faoapi's C-161 closure notice. +""" + +from __future__ import annotations + +TARGETS: tuple[str, ...] = ("lr_ged_sb", "lr_ged_ns", "lr_ged_os") + +S_MIN: int = 2 + +CONSUMER_DOCUMENT_NAME: str = "un_fao" + +UPLOAD_ENABLED: bool = False diff --git a/views_postprocessing/unfao/wire/__init__.py b/views_postprocessing/unfao/wire/__init__.py new file mode 100644 index 0000000..781f6d6 --- /dev/null +++ b/views_postprocessing/unfao/wire/__init__.py @@ -0,0 +1,8 @@ +"""ADR-013 Hop-B wire mechanics (epic #105). + +One closure: this package changes when the wire contract +(``docs/ADRs/013_sampled_forecast_wire_contract.md``) changes, and for no other +reason. The FAO *product* declaration lives outside it (``unfao/product.py`` — +different reason to change); representation-free *rules* live below it +(``delivery/``); the manager above composes it. +""" diff --git a/views_postprocessing/unfao/wire/naming.py b/views_postprocessing/unfao/wire/naming.py new file mode 100644 index 0000000..3368167 --- /dev/null +++ b/views_postprocessing/unfao/wire/naming.py @@ -0,0 +1,31 @@ +"""Hop-B file-name templates (ADR-013 §4.1b). + +A file name is a **locator only, never identity** (§3.3's lesson): consumers select +by store-document metadata and verify by manifest content hashes; these names exist +so humans and buckets can address the artifacts. Do not parse meaning back out of +them. + +Not to be confused with the store-document ``name`` *field* (§4.1a, +``product.CONSUMER_DOCUMENT_NAME``): that routes faoapi's queries; these name files. +The two never interchange. + +Templates are pinned by the §10 golden fixture and golden-string tested — a drift +here is a contract change, not a refactor. +""" + +from __future__ import annotations + + +def shard_name(run_id: str, target: str, time_id: int) -> str: + """`{run_id}__{target}__m{time_id:06d}.arrow.parquet` — one per (target, month).""" + return f"{run_id}__{target}__m{time_id:06d}.arrow.parquet" + + +def run_manifest_name(run_id: str) -> str: + """`{run_id}__manifest.json` — ONE per run (§4.2, the commit marker).""" + return f"{run_id}__manifest.json" + + +def sidecar_name(run_id: str) -> str: + """`{run_id}__sidecar.parquet` — one per run (§5).""" + return f"{run_id}__sidecar.parquet"