From 649f19b8c16a2296593c5fbf80bde7769eed8bd9 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 21 Aug 2026 23:13:38 +0200 Subject: [PATCH] docs(register): C-110..C-112, Cluster N, and the falsification stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No source changes. The register, plus two xfail stubs from the release-readiness audit. THREE NEW ENTRIES. C-110 (Tier 3) — the release path will block itself from 2026-10-18 and nothing tells the person it blocks. Measured against the live ruleset: protect_main is active, `test` is a REQUIRED check, bypass_actors is empty. The expiry tripwire added on 2026-08-19 reddens that check 30 days before 2026-11-17, so no PR merges and no release can be tagged. The escape is real and was verified — a PR setting ACKNOWLEDGED_UNTIL is green on its own branch — which is why it is Tier 3 rather than C-86's Tier 2. This entry exists because the interaction falls between two records and is in neither: C-84 never mentions the release path, C-86 never mentions the tripwire. The ruleset was not queried when the tripwire was added. C-111 (Tier 3) — a release can change whether a delivery fails, and the version number is the only thing that says so. Three exception types can now escape into a launcher, and DeliveryNotFindableError fails a delivery that previously succeeded silently. That is C-94 working as designed. There is no changelog, so views-models would take it with no notice. C-112 (Tier 3) — nothing here can see what production actually runs. Measured: both launchers pin 1.1.0; the newest tag is 1.1.1, eight days old, and 1.1.0 carries the C-99 fail-open that killed the first CRAF'd delivery. Meanwhile main holds 29 unreleased commits. The obvious fix — check the launchers' pin in CI — would add a fifth repo that can redden this build, which is C-86 with no bypass actors, so it is recorded as a decision rather than proposed. FROM review-rr TRIAGE AND STRATEGIC. - Cluster I closed: its own prescription landed. It asked for a test_register_integrity.py checking header counts, no RESOLVED under Open, and reference resolution. That file exists, does exactly those, and caught two real errors this week. The residual is recorded: the guard covers entries, not the cluster section — which is how Cluster I sat fully resolved and unmarked. - Cluster J: dropped a stale "(acute)" from a resolved C-22. - C-109 cross-linked to C-95, its nearest sibling. - C-81 re-measured. Its headline said CI verifies 8 fewer tests than local; it is 2 (468/5 skipped vs 470/3). PR #280 moved four checks into the gate four days ago and the entry — whose whole job is measuring that distance — kept quoting the old number. Trigger rewritten to the gap that remains: the checks needing views-datafactory artifacts that are not in its git repository. - Cluster N added, "This repository cannot see itself": C-109, C-107, C-95, C-110, C-111, C-112 and C-81. Six arrived in one sprint, each filed as unrelated. Its fix strategy is deliberately NOT more guards — C-109 records why the mechanical version fails; four of the seven were found by reading. - C-106 tagged [backlog]. THE STUBS. tests/test_falsification_release_readiness.py holds the two soft falsifications as xfail(strict=True), so the suite colour is unchanged. They are xfail rather than plain failures deliberately: `test` is a required check with zero bypass actors, so committing red tests would block their own fix — which is C-110. Register: 109 -> 112 total, 29 -> 32 open. Integrity guards green (40 tests), ruff clean, suite unchanged at the C-104 baseline. Co-Authored-By: Claude Opus 5 (1M context) --- reports/technical_risk_register.md | 117 ++++++++++++++++-- tests/test_falsification_release_readiness.py | 87 +++++++++++++ 2 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 tests/test_falsification_release_readiness.py diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index 2dda7c5..d94343c 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -5,8 +5,8 @@ | Project | views-postprocessing | | Owner | Dylan Pinheiro / PRIO MD&D Team | | Last Updated | 2026-08-21 | -| Total Concerns | 109 | -| Open Concerns | 29 | +| Total Concerns | 112 | +| Open Concerns | 32 | | Resolved Concerns | 80 | --- @@ -52,9 +52,15 @@ covered a single open entry (see Historical clusters below). **Fix strategy:** this repo already solved this disease once — the ADR-013 audit series ended with a **permanent guard suite** (`tests/test_falsify_adr013_*.py` — `pytest --collect-only -q tests/test_falsify_adr013*.py` for the count, which moves), and the same pattern now guards the þing-01 invariants (`tests/test_env_declaration.py`, `tests/test_redaction_guard.py` — the latter briefly **only over the roots that still existed**, see C-74, resolved: a guard is only as good as the assertion that its inputs are real, and it now carries that assertion). There is **no equivalent for the register**. A small `tests/test_register_integrity.py` — header counts match section counts; no RESOLVED body under `## Open Concerns`; every `C-\d+`/`D-\d+` reference resolves or is namespaced to a foreign register — would make this class self-detecting. **Resolution scope:** Full for the mechanical half. +**Succeeded by Cluster N.** This cluster's disease was governance prose drifting out of step with cross-repo state; its mechanical half is guarded now. What replaced it is broader and is tracked as **Cluster N, "This repository cannot see itself"** — the same surface, including the parts no guard reaches. + +**✅ CLOSED 2026-08-21 (review-rr triage).** Every entry cited above is resolved, and the cluster's own prescription landed: it asked for *"a small `tests/test_register_integrity.py` — header counts match section counts; no RESOLVED body under `## Open Concerns`; every `C-\d+`/`D-\d+` reference resolves or is namespaced"*. That file exists and does exactly those three things, plus five more; it is green, and it caught two real errors during this week's registrations (a header count off by one, and a views-pipeline-core `C-241` written without its namespace). + +**The residual, and it is this cluster's disease one level up.** The guard covers entries. It does **not** cover the Causal Clusters section — which is how this cluster sat with nine resolved entries and no closure marker until a triage read it by hand, and how Cluster J went on calling a resolved C-22 *"acute"*. **C-109** records the same shape for `Location` line numbers. Nothing is proposed here: the honest position is that the mechanical half is done and the prose half is checked by reading, which is what triage is for. + ### Cluster J: Delivery aftercare has no mechanism **Root cause:** the delivery pipeline is write-only — nothing exists downstream of upload for correction, recall, or provenance audit. -**Entries:** C-22 (acute), C-15, C-24, C-105 (added 2026-08-16 — a torn upload attempt is aftercare the write-only path has no answer for) +**Entries:** C-15, C-24, C-105 (added 2026-08-16 — a torn upload attempt is aftercare the write-only path has no answer for) — plus **C-22, RESOLVED**, which this line called *"acute"* until 2026-08-21 **Highest tier:** 3 **Fix strategy:** the C-22 correction procedure (issue #15) plus pipeline-core #245's structured metadata field to retire the description-as-carrier abuse. **Resolution scope:** Partial (process, not code). @@ -159,6 +165,15 @@ that indexes only deleted code is noise. --- +### Cluster N: This repository cannot see itself +**Root cause:** the guards here are unusually good at checking code against a declaration, and absent wherever the subject is the repository's own prose, its own records, or its relationship to anything outside. Every entry below was filed separately; together they are one class. +**Entries:** C-109 (`Location` line numbers rot faster than the review cycle), C-107 (docstrings sit outside the doc-accuracy scan), C-95 (a verdict mis-cited in three places), C-110 (a release block this repo scheduled for itself, documented nowhere a releaser looks), C-111 (a release changes delivery behaviour and only the version number says so), C-112 (nothing observes what consumers actually run) — plus **C-81**, whose own headline number was falsified by PR #280 and stood stale for four days. +**Highest tier:** 2 (C-81) +**Fix strategy:** **not more guards.** C-109 records why the obvious mechanical check fails — content assertions need a second declaration that can itself go stale, which is ADR-014 §2's warning. What works is a *reading* pass: this cluster was assembled by `/review-rr strategic` on 2026-08-21, and four of its seven members were found by reading rather than by any test. Schedule the reading; do not build a linter for prose. +**Resolution scope:** Partial and by nature. C-107 and C-109 are closable as conventions (scan the docstrings; cite by symbol). C-111 and C-112 are closable as artifacts (a changelog; a pin check). C-95, C-110 and C-81 are closable only by someone re-reading what the register says and comparing it to what is true — which is the activity, not a deliverable. + +**Why this cluster was late.** Six of its seven entries arrived between 2026-08-16 and 2026-08-21, during one sprint, each filed as an unrelated finding. The register had no cluster for them because the clusters describe *delivery* risks — inherited surface, go-global debt, aftercare, the lookup artifact. Nothing described the governance layer as a risk surface of its own, even though the register is the artifact four repositories read. + ## Open Concerns ### C-98: A tripwire on another repo's release history watched our own pin, and reported two days late @@ -631,6 +646,8 @@ and silent about the *cost*. Adopting `[edition].obliges_consumers` is the defer would fix it, and its trigger is below. +**A second source of redness, and it is ours — see C-110.** From 2026-10-18 `tests/test_credential_expiry.py` fails by design, so this entry's no-bypass finding starts applying to a block this repository scheduled for itself rather than one a sibling caused. + --- ### C-87: The delivery label will be checked against a declaration, and nothing will check the declaration against the consumer @@ -720,6 +737,8 @@ Four companion tests keep it honest rather than decorative: the firing branch is Cross-refs: **C-81** (the same operator session's other half — branch protection and the CI token), **C-27** (no rotation mechanism for a secret value upstream), **C-57** (the pinned-registry detector, which is how this arrived here at all — it demanded the v1.4.4 bump and the bump is what surfaced the expiry), þing-02 A3(i), views-appwrite C-65 and C-66. +**The tripwire gates the release path, which this entry did not say when it was added — see C-110.** From 2026-10-18 it reddens `test`, which `protect_main` makes a required check with zero bypass actors, so no release can be tagged until the keys rotate or `ACKNOWLEDGED_UNTIL` is set. + --- ### C-15: Upload metadata lacks enrichment provenance and carries test description @@ -1017,17 +1036,28 @@ Cross-refs: **C-62** (the transitive dependency drag; the other 31 alerts), **C- --- -### C-81: What actually gates `main` is weaker than it looks — CI verifies 8 fewer tests than local; the enforcement half is discharged +### C-81: What actually gates `main` is weaker than it looks — CI verifies 2 fewer tests than local; the enforcement half is discharged | Field | Value | |-------|-------| | ID | C-81 | | Tier | 2 — the guards this arc built to catch cross-repo drift do not run where drift happens, and the branch they protect has no required check. Both halves are structural and both have fired-in-practice evidence. | | Source | `code-review max` (2026-08-03) — development→main sync audit | -| Trigger | **Coverage half, re-specified 2026-08-13:** the next time a views-datafactory change would break a delivery — its 8 gated tests are the whole remaining gap and none of them runs in CI. ~~*Original: when the Appwrite Seam Contract registry next moves, nothing in CI will notice.*~~ **That trigger is false** and has been since 2026-08-10: CI checks out views-appwrite at `ref: main` and sets `VIEWS_APPWRITE`, so every registry-drift detector runs there. ~~**Enforcement half:** the first time someone merges a red PR to `main`~~ — **DISCHARGED 2026-08-13**: `protect_main` now requires the `test` check (see C-86). | +| Trigger | **Coverage half, re-specified again 2026-08-21 — the previous wording is now false.** It read *"its 8 gated tests are the whole remaining gap and none of them runs in CI"*. PR #280 added the views-datafactory checkout, so four of those now run in the gate. The live trigger is what remains: **the next time a change to a views-datafactory artifact that is NOT in its git repository would break a delivery** — the producer-comparison half of `test_gaul_lookup_fidelity` and the two `test_datafactory_deploy_readiness` gates still skip in CI, because the GAUL parquets and `data/assembled/` are untracked upstream (C-46, C-108). ~~*Original: when the Appwrite Seam Contract registry next moves, nothing in CI will notice.*~~ **That trigger is false** and has been since 2026-08-10: CI checks out views-appwrite at `ref: main` and sets `VIEWS_APPWRITE`, so every registry-drift detector runs there. ~~**Enforcement half:** the first time someone merges a red PR to `main`~~ — **DISCHARGED 2026-08-13**: `protect_main` now requires the `test` check (see C-86). | | Owner | Simon — both halves need operator action. The coverage half needs a token for two private repositories; the enforcement half is a GitHub console/ruleset change. Neither is engineering work. | | Location | `.github/workflows/run_pytest.yml`; the `protect_main` ruleset; `tests/conftest.py::sibling_repo` | +**⚠ RE-MEASURED 2026-08-21: the headline number was 8 and is now 2, and this entry did not notice for four days.** + +| environment | result | +|---|---| +| CI-shaped (both siblings, tracked files only) | **468 passed, 5 skipped** | +| full local (both siblings complete) | **470 passed, 3 skipped** | + +PR #280 fetched views-datafactory in CI, moving four checks from skipped to running — C-30's exclusion-manifest tripwire, C-46's release gate, the region-set check and the wire-cast dtype check. The gap it measures closed by three-quarters and the entry went on stating the old figure, in the one place a reader goes to find out how strong the gate is. + +**That is not an aside.** This entry exists to measure the distance between what CI checks and what a laptop checks. Carrying a stale number is the same defect one level up, and it is why the "This repository cannot see itself" cluster names this entry alongside C-109 and C-107. + **Coverage.** Measured in an isolated clone, not estimated — **402 collected in every run**, so the whole delta is skips: | environment | result | @@ -1166,7 +1196,7 @@ Cross-refs: **C-94** (nothing observes the outcome of an upload at the time it h --- -### C-106: The §2 header builder — the module that owns the contract version — is reachable only from tests +### C-106: The §2 header builder — the module that owns the contract version — is reachable only from tests `[backlog]` | Field | Value | |-------|-------| @@ -1243,7 +1273,80 @@ C-103 is the sharp case, and the reason this is a class rather than three typos: **Why the obvious guard does not work, stated so it is not proposed again cheaply.** Checking that a file has at least that many lines catches nothing: every stale citation above points at a real line. Checking *content* requires the entry to declare what it expects to find there, which is a second declaration that can itself go stale — the shape ADR-014 §2 warns about. The cheap and durable move is the convention (`::symbol`), not a test. -Cross-refs: **C-103** (twice stale in four days — the case that made this visible), **C-107** (docstrings outside the doc-accuracy scan; the same "nothing checks the prose" family), **C-82** (governance prose carrying numbers nothing checks, resolved). +Cross-refs: **C-103** (twice stale in four days — the case that made this visible), **C-95** (a verdict mis-cited in three places — the same defect in prose rather than in a line number, and the nearest sibling), **C-107** (docstrings outside the doc-accuracy scan; the same "nothing checks the prose" family), **C-82** (governance prose carrying numbers nothing checks, resolved). + +--- + +### C-110: The release path will block itself from 2026-10-18, and nothing tells the person it blocks + +| Field | Value | +|-------|-------| +| ID | C-110 | +| Tier | 3 — the ability to ship is interrupted on a known date, but the escape is in-repo and reachable. That is what separates it from **C-86**'s Tier 2, where the only responses are console actions by one person. If the acknowledgement were ever removed, or the tripwire made unconditional, this becomes C-86's tier. | +| Source | `/falsify` release-readiness audit, 2026-08-21 (probe P5) | +| Trigger | A release is cut on or after **2026-10-18**, or anyone reports a red `test` check on `main` they cannot explain — read `tests/test_credential_expiry.py` before diagnosing anything else. | +| Owner | This repository for the documentation; the operator for the rotation that removes the cause. | +| Location | `tests/test_credential_expiry.py::test_the_platform_keys_are_not_about_to_expire`; the `protect_main` ruleset; `docs/operations/` (where the runbook that would say this does not exist). | + +Measured 2026-08-21 against the live ruleset: `protect_main` is **active**, the `test` job is a **required status check**, and `bypass_actors` is **empty** — C-86's finding, re-confirmed. `test_credential_expiry` fails from 30 days before the 2026-11-17 expiry, i.e. **2026-10-18**. From that date the required check is red, so no pull request merges to `main` and no release can be tagged. + +**This entry exists because it falls between two records and is in neither.** C-84 registers the expiry and the tripwire and does not mention the release path or required checks. C-86 registers that a red build cannot be bypassed and does not mention the tripwire. The interaction — *the guard we added will redden the check that cannot be bypassed, on a date we chose* — is the product of the two, and was not noticed when the tripwire was added four days earlier. The ruleset was never queried at the time; the audit queried it. + +**The escape is real and was verified, which is why this is Tier 3.** A pull request that sets `ACKNOWLEDGED_UNTIL` is green on its own branch (measured: `1 failed` → `6 passed`), so the block is not a trap and the fix is not gated behind the thing it fixes. What is missing is that **nothing tells a releaser any of this**. There is no release runbook in `docs/operations/`, and the failure message names `ACKNOWLEDGED_UNTIL` without saying that a release is what it is blocking. + +**Closing this is documentation, not code.** Either a release runbook that names the interaction, or a cross-reference in C-84 and C-86 so that whoever reads one meets the other. + +Cross-refs: **C-84** (the expiry and the tripwire), **C-86** (no way past a red build; zero bypass actors), **C-81** (what actually gates `main`). + +--- + +### 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 | +|-------|-------| +| ID | C-112 | +| Tier | 3 — nothing is silent: the defect that made this visible failed loudly and killed a delivery. What is missing is any signal about **version lag**, so work landing here does not reach production and no one on this side can tell. Coordination cost, not corruption. | +| Source | Cross-repo issue sweep, 2026-08-21 — prompted by the question *"have you checked all gh issues related to this repo?"*, which had not been done in this repository at all | +| Trigger | A release is cut here, **or** a delivery fails in a way this repository has already fixed — before diagnosing, read what `views-models`' launchers pin. If it is behind the newest tag, that is the first hypothesis, not the last. | +| Owner | This repository for the visibility; views-models for the pin itself. | +| Location | No file — that is the finding. `views-models` `postprocessors/{un_fao,un_crafd}/run.sh` hold `VIEWS_POSTPROCESSING_PIN`, and nothing in this repository reads them. | + +**Measured 2026-08-21.** Both launchers pin **1.1.0**. This repository's newest tag is **1.1.1**, published eight days earlier, and 1.1.0 carries the `_ContractStorePort.download` fail-open — register **C-99**, the defect that killed the first `un_crafd` delivery attempt on 2026-08-13. So production has been running a known-defective build of this package, on the FAO leg as well as CRAF'd, for over a week, and **nothing on this side could see it**. views-models#403 was filed for it and is open. + +At the same time, `main` carries **29 commits since 1.1.1** — every guard from this week's work, including the C-94 findability preflight and the C-105 torn-run ledger. None of it is in production either. + +**Two gaps, one shape: this repository cannot see the distance between what it declares and what anyone runs.** It does not know what consumers pin, and nothing compares its declared version against what is published. `tests/test_release_version.py` compares `pyproject` to the git tag — both facts *inside this repository* — which is why a version bump that is never tagged, or a tag no consumer ever takes, is invisible to it. + +**Why this is not C-111.** That entry is the outbound half — we change delivery behaviour and tell consumers nothing but an integer. This is the inbound half — we do not look at whether they took it. They compound: 1.1.1 fixed a delivery-killing defect, nothing announced it, and nothing checked whether it landed. + +**The obvious fix has a known cost, and it is already registered.** CI checks out sibling repositories so cross-repo assertions run (ADR-016); views-models is not among them. Adding it, plus a check that the launchers' pin is not behind the newest tag, would be the same shape as the existing drift checks. It would also add a fifth repository whose `main` can redden this build — **C-86**, no bypass actors. That trade is a decision, not a cleanup, which is why nothing is proposed here. + +**The wider observation, recorded once so it is not rediscovered.** Eighty-four open issues across the organisation mention `views-postprocessing`; this repository tracks none of them and had never been swept. Most are informational, several were filed *by* this seat, and a few carry live asks (views-faoapi#390, views-crafdapi#55, views-models#362). No mechanism is proposed for that either — but a sweep belongs in the next repo-assimilation rather than being found by accident at the end of a sprint. + +Cross-refs: **C-111** (the outbound half), **C-99** (the defect production is still running), **C-86** (the cost of adding another sibling to CI), **C-81** (what actually gates `main`), views-models#403. ## Disagreements diff --git a/tests/test_falsification_release_readiness.py b/tests/test_falsification_release_readiness.py new file mode 100644 index 0000000..e3f8072 --- /dev/null +++ b/tests/test_falsification_release_readiness.py @@ -0,0 +1,87 @@ +"""Failing stubs from the release-readiness falsification audit, 2026-08-21. + +**DO NOT COMMIT THIS FILE AS-IS.** These tests fail by design, and this repository's +`protect_main` ruleset makes the `test` job a **required check with zero bypass +actors** — so committing red tests to `main` blocks every subsequent merge, including +the fix. That interaction is itself finding S1 below. + +Claim audited: *"we're ready to set up a PR, bump the version, and run the review +ritual"* — i.e. cutting 1.2.0 from current `main` would ship a correct, installable +package with every release guard satisfied and nothing in the repo's governance +blocking the path. + +Verdict: CONTESTED. No hard falsification; two soft ones, below. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parent.parent + + +@pytest.mark.xfail(reason="S1: unaddressed falsification — see the audit report", strict=True) +def test_the_release_path_survives_its_own_expiry_tripwire(): + """S1 (soft). From 2026-10-18 no release can be cut, and nothing says so. + + Measured 2026-08-21 against the live ruleset: `protect_main` is active, the `test` + job is a REQUIRED status check, and `bypass_actors` is **empty** — which is C-86's + finding, still true. `tests/test_credential_expiry.py` starts failing 30 days before + 2026-11-17, i.e. **2026-10-18**. From that date the required check is red, so no PR + merges to `main` and no release can be tagged. + + The escape is reachable — a PR that sets `ACKNOWLEDGED_UNTIL` is green on its own + branch (verified: 1 failed -> 6 passed) — which is why this is soft rather than + hard. What is missing is that **nothing tells a releaser this**. There is no release + runbook, and C-84 does not mention that its tripwire gates the release path. + + Fix: document the interaction where a releaser will meet it — a release runbook + under `docs/operations/`, or a line in C-84 and C-86 cross-referencing each other. + """ + runbook = list((_REPO / "docs" / "operations").glob("*release*")) + assert runbook, ( + "no release runbook exists, so the 2026-10-18 block on the release path is " + "recorded nowhere a releaser would look (C-84 x C-86)" + ) + text = "\n".join(p.read_text() for p in runbook) + assert "ACKNOWLEDGED_UNTIL" in text, ( + "the release runbook does not name the only in-repo way past the expiry " + "tripwire once it fires" + ) + + +@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. + """ + 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" + ) + 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" + )