From 3fae5f24567956a5e5157cbb2f3776d8e55b6c98 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 26 Aug 2026 01:59:59 +0200 Subject: [PATCH] =?UTF-8?q?release:=201.2.0=20=E2=80=94=20and=20tell=20con?= =?UTF-8?q?sumers=20what=20changed=20about=20failing=20(C-111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps 1.1.1 -> 1.2.0. MINOR rather than PATCH: the delta adds a new module (delivery/findability.py), three exception types, a changed return value on store_port.upload(), and a required keyword on build_provenance. Adds CHANGELOG.md, which this repository has never had. C-111's trigger was "the next version is cut — write what changed for a consumer, or record why the number alone is enough", and this is that release. The entry leads with the three exceptions that can now escape into a launcher, because the sharp one is C-94: a delivery whose artifacts land where the consumer cannot see them previously reported success and now raises. That is the intended behaviour and still a change views-models would otherwise learn about only as an integer. Venue chosen deliberately: the root CHANGELOG.md rather than the GitHub release body, so a consumer reading the source tree at a pinned version sees it without leaving the checkout. C-111 left the venue open; this is the choice, recorded. Two falsification probes asserted this gap and both retire here — S2 from the 2026-08-21 audit and H1 from the 2026-08-25 one. They are one finding located twice, because the second audit designed probes without reading the first's stubs. Noted in the test module rather than the register: it is a lesson about the audit procedure, not about the concern. Three probes stay open and xfail: the expiry tripwire blocking releases from 2026-10-18 (S1/C-110), tag-push not publishing (S3), and the publish job shipping without running tests (S4). None blocks this release; all three are about the release path rather than the package. Register: C-111 moved to Resolved, counts 32->31 open, 80->81 resolved, verified by tests/test_register_integrity.py. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 82 ++++++++++++++++ pyproject.toml | 2 +- reports/technical_risk_register.md | 73 +++++++++----- tests/test_falsification_release_readiness.py | 95 +++++++++++++------ 4 files changed, 194 insertions(+), 58 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c0aeb8e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,82 @@ +# Changelog + +What changed **for a consumer** — the launcher that pins this package, and the partner +that receives its artifacts. Not a commit log: entries here name behaviour someone +outside this repository can observe, in particular **behaviour that can turn a +previously-passing run into a failing one**. + +This file exists because the version number was the only signal a consumer got +(register C-111). Releases before 1.2.0 are summarised from their tags rather than +reconstructed in detail. + +## 1.2.0 — 2026-08-26 + +**A previously-passing delivery can now fail in three new ways. All three are +deliberate, and each replaces a silent failure with a loud one.** + +### New failure modes that escape into the launcher + +- **`views_postprocessing.delivery.findability.DeliveryNotFindableError`** — after + upload, the delivery now asks the store the same question the consumer asks: *is + the newest document under the consumer's name the one this run just uploaded?* If + it is not, the run raises. **Previously a delivery whose artifacts landed somewhere + the consumer could not see reported success.** (C-94) + +- **`…findability.FindabilityUnverifiedError`** — raised when the check itself could + not run, e.g. the store errored on the read-back. Deliberately distinct from + `DeliveryNotFindableError`: "could not ask" is not "asked and got nothing". + +- **`views_postprocessing.contract.source_metadata.ProducerClientUnavailable`** — + reading the producer's `last_valid_month_id` now raises if `datafactory_query` + cannot be loaded, whether it is absent or raises on import. **Previously a broken + environment degraded open and shipped unobserved months as observed history.** + A producer that publishes no boundary is still handled as before — that is a normal + older store, and a different condition. (C-103) + +- **`views_postprocessing.contract.wire.sink.TornRunError`** — a failure partway + through uploading a run now raises a refusal naming the run, every object confirmed + uploaded, and the object that failed. It does **not** delete anything; a torn run + still leaves orphans, but it no longer leaves them undocumented. Note this is a + `RuntimeError`, not a `SinkError` — `SinkError` means do-not-retry, and a torn run + may be retried. (C-105) + +### Changed data reaching the partner + +- **Delivered artifacts carry a new provenance field, `observed_through`.** It records + the producer boundary the observed-range clip used, or `null` when the boundary + could not be read and the clip was therefore **skipped** — meaning unobserved months + may be present. Carried in the file's `description` metadata, which is a JSON string; + consumers that treat that field as opaque text are unaffected. (#297) + +### Changed internals a direct caller would notice + +- `unfao.store_port` / `crafd.store_port` `upload()` now **returns the uploaded file + id** instead of discarding it. Required by the findability read-back above. +- `delivery.provenance.build_provenance()` gained a **required** keyword argument, + `observed_through`. Required rather than optional because a caller that omits it is + the failure being fixed. + +### Documentation + +- ADR-013 gains **§5.1a**, recording that nullable int64 GAUL code columns were + considered and rejected, with the measured pandas round-trip behaviour that decided + it. No `contract_version` change: the wire bytes are untouched. (#278) + +### Upgrading + +Nothing to change in a launcher. The three new exceptions surface conditions that were +already wrong and already silent — if one fires after upgrading, it is reporting a +pre-existing problem, not creating a new one. + +## 1.1.1 — 2026-08-15 + +Fixes C-99. Consumers on 1.1.0 should move; see views-models#403. + +## 1.1.0 — 2026-08-13 + +Adds the CRAF'd producer package alongside UN-FAO, and the observed-range clip that +drops months above the producer's declared boundary. + +## 1.0.0 — 2026-08-01 + +First released version. diff --git a/pyproject.toml b/pyproject.toml index f42e9a5..d31e13d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "views-postprocessing" -version = "1.1.1" +version = "1.2.0" description = "" authors = [ "Dylan Pinheiro ", diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index e23d8a5..b1d07e5 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -4,10 +4,10 @@ |-------------------|--------------------------------------| | Project | views-postprocessing | | Owner | Dylan Pinheiro / PRIO MD&D Team | -| Last Updated | 2026-08-21 | +| Last Updated | 2026-08-26 | | Total Concerns | 112 | -| Open Concerns | 32 | -| Resolved Concerns | 80 | +| Open Concerns | 31 | +| Resolved Concerns | 81 | --- @@ -1318,29 +1318,6 @@ Cross-refs: **C-84** (the expiry and the tripwire), **C-86** (no way past a red --- -### C-111: A release can change whether a delivery fails, and the version number is the only thing that says so - -| Field | Value | -|-------|-------| -| ID | C-111 | -| Tier | 3 — nothing is silent and nothing corrupts; the cost lands on a consumer who takes a version that changes their pipeline's outcome with no notice, and on whoever then diagnoses it across two repositories. | -| Source | `/falsify` release-readiness audit, 2026-08-21 (discovered during execution, not predicted) | -| Trigger | The next version is cut — write what changed for a consumer, or record why the number alone is enough. | -| Owner | This repository. | -| Location | The repository root — there is no `CHANGELOG.md`, no `docs/operations/release_notes.md`, and no release notes on the existing tags. | - -The delta since tag `1.1.1` adds three exception types that can escape into a launcher: `delivery.findability.DeliveryNotFindableError` and `FindabilityUnverifiedError` (C-94), and `contract.source_metadata.ProducerClientUnavailable` (C-103). The first is the sharp one — **a delivery whose artifacts land somewhere the consumer cannot see previously succeeded silently and now raises.** - -That is the intended behaviour and the entire point of C-94. It is still a change a consumer must be told about, and the only signal views-models receives is a MINOR version bump in `VIEWS_POSTPROCESSING_PIN`. Nothing in this repository states that a previously-passing run can now fail. - -**The asymmetry is the finding.** This repository is unusually careful about telling *contributors* things — a register, ADRs, CICs, guards that refuse with paragraph-long explanations. It tells *consumers* nothing but an integer. views-models#403 is the shape of the consequence in the other direction: launchers sat on `1.1.0` for eight days after `1.1.1` fixed a defect that had already killed a delivery, because nothing made the difference legible. - -**Deliberately not proposed: a full changelog discipline.** What is needed is a line per release naming behaviour a consumer can observe. Whether that lives in `CHANGELOG.md`, in the GitHub release body, or in the pre-release notes FAO already receives is a choice, not a requirement. - -Cross-refs: **C-94** and **C-103** (the new failure modes), **C-112** (the inbound half — nothing checks whether a consumer took the release either; the two compound), **views-models#403** (the same gap costing eight days in the other direction), **C-24** (a consumer-facing contract divergence nobody surfaced). - ---- - ### C-112: Nothing here can see what production actually runs, and it ran a defect we had already fixed for eight days | Field | Value | @@ -3429,6 +3406,50 @@ Cross-refs: **views-faoapi C-161** (the deploy constraint §0.2a now surfaces --- +### C-111: A release can change whether a delivery fails, and the version number is the only thing that says so + +| Field | Value | +|-------|-------| +| ID | C-111 | +| Tier | 3 — nothing is silent and nothing corrupts; the cost lands on a consumer who takes a version that changes their pipeline's outcome with no notice, and on whoever then diagnoses it across two repositories. | +| Source | `/falsify` release-readiness audit, 2026-08-21 (discovered during execution, not predicted) | +| Trigger | The next version is cut — write what changed for a consumer, or record why the number alone is enough. | +| Owner | This repository. | +| Location | The repository root — there is no `CHANGELOG.md`, no `docs/operations/release_notes.md`, and no release notes on the existing tags. | + +The delta since tag `1.1.1` adds three exception types that can escape into a launcher: `delivery.findability.DeliveryNotFindableError` and `FindabilityUnverifiedError` (C-94), and `contract.source_metadata.ProducerClientUnavailable` (C-103). The first is the sharp one — **a delivery whose artifacts land somewhere the consumer cannot see previously succeeded silently and now raises.** + +That is the intended behaviour and the entire point of C-94. It is still a change a consumer must be told about, and the only signal views-models receives is a MINOR version bump in `VIEWS_POSTPROCESSING_PIN`. Nothing in this repository states that a previously-passing run can now fail. + +**The asymmetry is the finding.** This repository is unusually careful about telling *contributors* things — a register, ADRs, CICs, guards that refuse with paragraph-long explanations. It tells *consumers* nothing but an integer. views-models#403 is the shape of the consequence in the other direction: launchers sat on `1.1.0` for eight days after `1.1.1` fixed a defect that had already killed a delivery, because nothing made the difference legible. + +**Deliberately not proposed: a full changelog discipline.** What is needed is a line per release naming behaviour a consumer can observe. Whether that lives in `CHANGELOG.md`, in the GitHub release body, or in the pre-release notes FAO already receives is a choice, not a requirement. + +Cross-refs: **C-94** and **C-103** (the new failure modes), **C-112** (the inbound half — nothing checks whether a consumer took the release either; the two compound), **views-models#403** (the same gap costing eight days in the other direction), **C-24** (a consumer-facing contract divergence nobody surfaced). + +**✅ RESOLVED 2026-08-26, at the release its trigger named.** The trigger read *"the +next version is cut — write what changed for a consumer, or record why the number alone +is enough."* 1.2.0 is that release, and `CHANGELOG.md` now exists at the repository root +with an entry naming each of the three escaping exception types, the condition each one +replaces, and the new `observed_through` provenance field — under the heading the entry +asked for: **what changed for a consumer**, with the failure modes stated first. + +The venue was left open by the entry deliberately, and the choice made here is the root +`CHANGELOG.md` rather than the GitHub release body, so that a consumer reading the source +tree at a pinned version can see it without leaving the checkout. The release body quotes it. + +Two falsification probes asserted this gap and both are retired by this change — S2 from +the 2026-08-21 audit and H1 from the 2026-08-25 one. **They are the same finding, located +twice**, because the second audit designed its probes without reading the first's stubs. +That is a lesson about the audit procedure rather than about this entry, and it is recorded +in `tests/test_falsification_release_readiness.py` where the next auditor will meet it. + +**What this does not close: C-112**, the inbound half — nothing here can still see what a +consumer actually runs. A changelog tells views-models what changed; it does not tell this +repository whether views-models read it. + +--- + ## Resolved Disagreements ### D-07: Historical data route — keep pipeline-core dispatcher vs call datafactory directly — RESOLVED diff --git a/tests/test_falsification_release_readiness.py b/tests/test_falsification_release_readiness.py index e3f8072..a076698 100644 --- a/tests/test_falsification_release_readiness.py +++ b/tests/test_falsification_release_readiness.py @@ -52,36 +52,69 @@ def test_the_release_path_survives_its_own_expiry_tripwire(): ) -@pytest.mark.xfail(reason="S2: unaddressed falsification — see the audit report", strict=True) -def test_a_release_announces_delivery_failure_modes_it_adds(): - """S2 (soft). 1.2.0 can fail a delivery that 1.1.1 completed, and nothing says so. - - The shipped delta since tag 1.1.1 adds three exception types that can escape into a - launcher: `DeliveryNotFindableError` and `FindabilityUnverifiedError` (C-94) and - `ProducerClientUnavailable` (C-103). The first is the sharp one — a delivery whose - artifacts land somewhere the consumer cannot see previously **succeeded silently** - and now raises. - - That is the intended behaviour and the whole point of C-94. It is still a change a - consumer must be told about, and the only signal they get is a MINOR version bump. - This repository has no CHANGELOG, so views-models' launchers would take 1.2.0 with - no notice that a previously-passing run can now fail. - - Fix: a CHANGELOG naming the new failure modes, or release notes on the tag. Either - satisfies this; the assertion below is deliberately loose about which. +# ───────────────────────────────────────────────────────────────────────────── +# Second audit, 2026-08-25. Claim: *"we are ready to bump the version, set up a +# PR to main, review, merge when review is good, then tag and publish."* +# +# Verdict: FALSIFIED — one hard, two soft. +# +# **H1 DISCHARGED 2026-08-26** (release 1.2.0). Its probe asserted that a release +# names what changed about failing for a consumer. `CHANGELOG.md` now exists and +# 1.2.0's entry names the three escaping exception types and the new provenance +# field, so the probe would XPASS and `strict=True` would turn that into a failure. +# Removed by hand rather than left to flip, per the S4 precedent (#200). Register +# C-111 is closed by the same change. +# +# **S2 DISCHARGED 2026-08-26** by the same change — and it is the same finding. +# S2 (2026-08-21) and H1 (2026-08-25) are one concern found twice by two audits, +# which is itself worth recording: the second audit did not read the first's stubs +# before designing probes. Both are C-111; both are closed by CHANGELOG.md. +# +# S1, S3 and S4 remain open and are below. The bump SIZE (minor, +# not patch) is recorded as an observation, not a falsification: nothing in the +# repo is wrong about it, it is a way the releaser could be. +# ───────────────────────────────────────────────────────────────────────────── + + +@pytest.mark.xfail(reason="S3: unaddressed falsification — see the audit report", strict=True) +def test_tagging_actually_publishes(): + """S3 (soft). "Tag and publish" is not the mechanism this repo has. + + `.github/workflows/publish_package.yml` triggers on `release: published` and + `workflow_dispatch` — **not** on tag push. Pushing a tag runs nothing. + + The evidence that this is a live trap rather than a technicality: tags `1.0.0` + and `1.1.0` both exist and **neither has a GitHub Release**. Only `1.1.1` does, + which is the only version this workflow has ever published. + + Fails until the workflow triggers on tag push, or until a release runbook states + that cutting a GitHub Release — not tagging — is the publishing step. """ - candidates = [ - _REPO / "CHANGELOG.md", - _REPO / "docs" / "CHANGELOG.md", - _REPO / "docs" / "operations" / "release_notes.md", - ] - present = [p for p in candidates if p.exists()] - assert present, ( - "no changelog or release-notes file exists, so a consumer's only signal that " - "1.2.0 can fail a delivery 1.1.1 completed is the version number itself" + wf = (_REPO / ".github/workflows/publish_package.yml").read_text() + assert "tags:" in wf or "push:" in wf, ( + "publish triggers only on `release: published`; a plan that says 'tag, then " + "publish' will tag and stop, and nothing will say so" + ) + + +@pytest.mark.xfail(reason="S4: unaddressed falsification — see the audit report", strict=True) +def test_the_publish_job_cannot_ship_untested_code(): + """S4 (soft). The publish job runs no tests and declares no dependency. + + `publish_package.yml` has no `needs:`, no pytest step, and one gate: that the + version in `pyproject.toml` parses higher than the newest on PyPI. So a GitHub + Release cut from any commit — a branch, a stale `main`, a commit whose `test` + job failed — builds and uploads to PyPI unconditionally. + + Nothing has gone wrong yet because releases have been cut from a green `main` + by hand. The guard is the habit, not the workflow, and habits are what C-86 + already showed this repo cannot rely on when one person holds them. + + Fails until the publish job depends on a passing test run, or refuses a ref + whose checks are not green. + """ + wf = (_REPO / ".github/workflows/publish_package.yml").read_text() + assert "needs:" in wf or "pytest" in wf, ( + "publish validates only version-greater-than-PyPI; nothing establishes that " + "the code being shipped passes its own suite" ) - text = "\n".join(p.read_text() for p in present) - for failure_mode in ("DeliveryNotFindableError", "ProducerClientUnavailable"): - assert failure_mode in text, ( - f"{failure_mode} can escape into a launcher and is not announced anywhere" - )