diff --git a/docs/ADRs/013_sampled_forecast_wire_contract.md b/docs/ADRs/013_sampled_forecast_wire_contract.md index 35099b5..6911d44 100644 --- a/docs/ADRs/013_sampled_forecast_wire_contract.md +++ b/docs/ADRs/013_sampled_forecast_wire_contract.md @@ -186,7 +186,7 @@ means, and cleanup of the internal store has no owner yet.*) it defines the ensembles and launches the runs that enter the wire (using pipeline-core's machinery); it owns the **delivery pointer** — the declaration of *which source ships to which partner* (as of adoption, the `postprocessors/un_fao` -launch config naming the ensemble; their ADR-017 proposes making it first-class — see +launch config naming the ensemble; their `vmo_017` (views-models ADR-017) proposes making it first-class — see §4.2a for how the pointer meets this contract); it owns the run-0 end-to-end verification (views-models#230, §11.1–§11.2); and it hosts the platform's decision record (this contract was ratified on views-models#149). @@ -942,13 +942,19 @@ record execution progress against it. version is written by the release that built it, so it cannot disagree with the code beside it. In force now, for 3.0.0 onward. - **`"unknown"` when the producer ran an editable install** — and this half is **not yet in - any released version**, which is the part the originating request (#228) stated too - strongly. An editable install's recorded version is fixed at the moment `pip install -e` - last ran and never tracks the source afterwards; pipeline-core#403 makes that case report - `"unknown"` instead of a stale number. That fix merged **2026-08-04**, a day and a half - *after* 3.0.0 was uploaded (2026-08-03 02:06 UTC), so **no released version contains it.** - It becomes true of producers at pipeline-core's next release. + **`"unknown"` when the producer ran an editable install** — this half was **not in any + released version when E3 was written**, which is the part the originating request (#228) + stated too strongly. An editable install's recorded version is fixed at the moment + `pip install -e` last ran and never tracks the source afterwards; pipeline-core#403 makes + that case report `"unknown"` instead of a stale number. That fix merged **2026-08-04**, a + day and a half *after* 3.0.0 was uploaded (2026-08-03 02:06 UTC). + + **Discharged 2026-08-13: it is now released.** views-pipeline-core **3.0.1** was uploaded + to PyPI on **2026-08-11 13:40 UTC** and contains the fix — verified at the tag rather than + taken on trust: at 3.0.0 `_pipeline_core_version()` is `return version("views_pipeline_core")` + with no editable detection at all, and at 3.0.1 it reads `direct_url.json`, checks + `dir_info.editable`, and returns `"unknown"` when it is set. Both halves of the lift are + therefore in force, the second for producers running 3.0.1 or later. Verified here rather than taken on trust, 2026-08-10: in this repository's development environment `importlib.metadata` reports pipeline-core **2.3.0** while the source beside @@ -958,9 +964,9 @@ record execution progress against it. **What consumers must do, and it is the operational point.** Treat `"unknown"` as *"do not infer the producing version"*, never as an error. The set of runs producing - `"unknown"` will **widen** at pipeline-core's next release, because every developer run - joins it. A consumer that starts rejecting `"unknown"` on the strength of this lift would - break exactly those runs. + `"unknown"` **has now widened**, as this erratum predicted it would: every developer run + on 3.0.1 or later joins it. A consumer that starts rejecting `"unknown"` on the strength + of this lift would break exactly those runs. This repository does not produce the value — Hop-B re-embeds the Hop-A header untouched (`contract/wire/sink.py`) — so nothing here changes. The field is declared by this diff --git a/docs/ADRs/017_facts_across_a_private_boundary.md b/docs/ADRs/017_facts_across_a_private_boundary.md index 80ae053..525cd21 100644 --- a/docs/ADRs/017_facts_across_a_private_boundary.md +++ b/docs/ADRs/017_facts_across_a_private_boundary.md @@ -1,6 +1,13 @@ -# ADR-017: Facts shared with a repository we cannot read +# vpp_017 (ADR-017): Facts shared with a repository we cannot read **Status:** Accepted + +> **Cite this as `vpp_017` outside this repository.** views-models and views-crafdapi each +> have their own ADR-017 (*Forecast Sources, Composition, and Delivery*, and *Reference Data +> in Repository*), so a bare "ADR-017" resolves to the wrong document for a reader sitting +> in either of them (#264). The number is unchanged and every existing citation stays valid +> — the prefix is additive. + **Date:** 2026-08-10 **Decider:** Simon Polichinel von der Maase **Scope:** what this repository does when it depends on a fact held in a repository it diff --git a/docs/ADRs/README.md b/docs/ADRs/README.md index b8fd52b..3fac273 100644 --- a/docs/ADRs/README.md +++ b/docs/ADRs/README.md @@ -104,16 +104,46 @@ These ADRs form the architectural constitution of the repository. for a dated declaration rather than prose. The credential for the one genuinely private sibling is deferred with a named trigger. -- **ADR-017** — Facts Shared With a Repository We Cannot Read +- **[vpp_017](017_facts_across_a_private_boundary.md)** — Facts Shared With a Repository We Cannot Read The delivery label this repository writes is owned by the consuming API and mirrored here; if the two drift the upload succeeds, the file is stored, and the consumer's endpoint is - empty with no error anywhere. Verifying the mirror currently means reading the consumer's + empty with no error anywhere. Verifying the mirror used to mean reading the consumer's source, which is impossible in CI when that consumer is private — and private consumer APIs - are a standing category, not a one-off. Decides that such a fact is declared in the public + are a standing category, not a one-off. *(That reading is gone: #248 deleted both source + reads on 2026-08-12 after two consumers broke them in a day by improving their own code.)* Decides that such a fact is declared in the public coordinate registry and each side verifies itself against it, so neither reads the other's source. No credential, for any number of private APIs. States plainly the half it does not cover: the consumer's own code against its own declaration. +### Why one of these carries a `vpp_` prefix + +Three repositories each numbered an ADR **017**, and a bare "ADR-017" in a cross-repo +sentence resolves to the wrong document for a reader sitting in either of the other two: + +| Repo | Prefix | ADR-017 | +|---|---|---| +| views-models | `vmo_` | *Forecast Sources, Composition, and Delivery* | +| **views-postprocessing** | **`vpp_`** | ***Facts shared with a repository we cannot read*** | +| views-crafdapi | `vcr_` | *Reference Data in Repository* | + +**The prefix is additive.** The number does not change and no existing citation breaks. + +**The usage rule** (adopted from views-models, which landed this first in its #393): +intra-repo prose may stay bare; write `vpp_017` wherever the sentence is read from, or +could be read from, another repository. + +This is not yet a platform-wide convention — these three issues are its first application, +driven by an active collision rather than a sweep. views-postprocessing#264, +views-models#393 (landed), views-crafdapi#58. + +*Audited when adopting: this repository has **no wrong referents** — no sentence here is +about the wrong decision, which is the defect the collision causes elsewhere. It has **one +bare foreign citation**: ADR-013 §7(d) writes "their ADR-017" of views-models', qualified +only by an antecedent two sentences earlier. Now `vmo_017`. Every other reference to +views-models' 017 already named it, and every bare `ADR-017` here means this document. The +prefix is for readers arriving from another repo.* + + ADRs numbered 010 and above define: - Domain-specific decisions diff --git a/docs/CICs/UNFAOPostProcessorManager.md b/docs/CICs/UNFAOPostProcessorManager.md index d8848a8..e98b6d2 100644 --- a/docs/CICs/UNFAOPostProcessorManager.md +++ b/docs/CICs/UNFAOPostProcessorManager.md @@ -164,7 +164,7 @@ that cannot raise — and "accessing `_enricher` directly", an attribute removed - **Beige tests:** Missing ensemble name in config; None environment variables; empty forecast bucket; DataFrames with unexpected index structure - **Red tests:** Corrupted parquet downloads; network timeouts during upload; DataFrames where all cells map to None (all-ocean input) -Currently: the manager cannot be instantiated without `views-pipeline-core`, so its stage logic is covered by **source-scan and seam tests** — `tests/test_validation.py` (the metadata null-gate, tested against `contract/historical.assert_metadata_complete` where it fires, plus pins that it has not drifted back into `_validate`), `tests/test_launch_config.py` (the refusals), `tests/test_gaul_lookup_access.py` (one lookup read, threaded). There is no longer a replica of any manager method: `tests/test_validation.py` held one until S4 (#185) and it had diverged from `_validate` since #149, while `tests/test_append_metadata.py` was deleted in #149 with the method it mirrored. A full end-to-end test against the **live manager** still requires a production-like environment and is tracked as **#18**; þing-02 **D2** forbids integration tests against the production Appwrite project, no non-production one existing. +Currently: the manager cannot be instantiated without `views-pipeline-core`, so its stage logic is covered by **source-scan and seam tests** — `tests/test_validation.py` (the metadata null-gate, tested against `contract/historical.assert_metadata_complete` where it fires, plus pins that it has not drifted back into `_validate`), `tests/test_launch_config.py` (the refusals), `tests/test_gaul_lookup_access.py` (one lookup read, threaded). There is no longer a replica of any manager method: `tests/test_validation.py` held one until S4 (#185) and it had diverged from `_validate` since #149, while `tests/test_append_metadata.py` was deleted in #149 with the method it mirrored. A full end-to-end test against the **live manager** still requires a production-like environment and is tracked as **#18**; **þing-01 D2** forbids *integration* tests against the production Appwrite project while no non-production one exists — conditionally, and it **permits read-only preflight validation** (register C-95, C-96). The input-integrity guards (S0–S6, epic #51) are representation-free invariants in `views_postprocessing/delivery/` that the manager **calls** (never inherits). Each has primitives unit tests — `tests/test_delivery_coverage.py` (S1/S4), `tests/test_delivery_observed_range.py` (S2), `tests/test_provenance.py` (S5), `tests/test_frame_extraction.py` (the seam), `tests/test_store_metadata.py` (store identity) — and `tests/test_input_integrity_e2e.py` drives the invariants on **primitives**, which is how the manager calls them. S3's forecast-identity rule moved to the wire layer (#150) and is covered by `tests/test_wire_source_selection.py`. The design contract (representation-free, called-not-inherited) is pinned by `tests/test_input_integrity_design_contract.py`. diff --git a/poetry.lock b/poetry.lock index bbc9180..6110028 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3545,14 +3545,14 @@ docs = ["jupyterlab (>=4,<5)", "matplotlib (>=3.8,<4)", "nbmake (>=1.5,<2)", "py [[package]] name = "views-pipeline-core" -version = "3.0.0" +version = "3.0.1" description = "Core orchestration, data and model-management library for the VIEWS conflict forecasting platform." optional = false python-versions = "<3.15,>=3.11" groups = ["main"] files = [ - {file = "views_pipeline_core-3.0.0-py3-none-any.whl", hash = "sha256:400aeff47b8b56b443165e587db706596f9129763e1dc792e6dbff817289ff3c"}, - {file = "views_pipeline_core-3.0.0.tar.gz", hash = "sha256:3da2b1c98b45f612ba136dda9374543beb6a89f3e0b58791ab73c69cefdb5e4f"}, + {file = "views_pipeline_core-3.0.1-py3-none-any.whl", hash = "sha256:816e0093fda788d801bda38aa82c5de606867caa3cd561aa9652c98c740995fe"}, + {file = "views_pipeline_core-3.0.1.tar.gz", hash = "sha256:c05d88dfe8aa28feff5ae466bb1e5fd96c4a799aa54d665b274469d4a81403d9"}, ] [package.dependencies] diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index abaae4b..640e686 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -5,9 +5,9 @@ | Project | views-postprocessing | | Owner | Dylan Pinheiro / PRIO MD&D Team | | Last Updated | 2026-08-12 | -| Total Concerns | 97 | -| Open Concerns | 21 | -| Resolved Concerns | 76 | +| Total Concerns | 98 | +| Open Concerns | 21 | +| Resolved Concerns | 77 | --- @@ -161,6 +161,28 @@ that indexes only deleted code is noise. ## Open Concerns +### C-98: A tripwire on another repo's release history watched our own pin, and reported two days late + +| Field | Value | +|-------|-------| +| ID | C-98 | +| Tier | 3 — the guard worked and the claim it protected was corrected within the hour, so nothing was published wrong. What is registered is the *observation channel*, which was the wrong one and would be wrong again in the same shape. | +| Source | CI failure on PR #266, 2026-08-13 | +| Trigger | A guard is written whose subject is an event in another repository — a release, a tag, a published artifact — and the value it actually reads lives in this one. | +| Location | `tests/test_falsify_adr013_s2.py` (the retired `test_e3s_claim_about_unreleased_behaviour_is_still_true`); `docs/ADRs/013_sampled_forecast_wire_contract.md`, Erratum E3 | + +ADR-013's Erratum E3 stated that a views-pipeline-core fix — an editable install reporting `"unknown"` instead of a stale version — was *"not yet in any released version."* That is a dated claim about someone else's release history, so a tripwire was attached to it: if the installed pipeline-core is a released distribution past 3.0.0, E3's wording is stale. + +**The tripwire fired, in CI, exactly as designed, and E3 was wrong.** views-pipeline-core 3.0.1 was uploaded to PyPI on **2026-08-11 13:40 UTC** and does contain the fix — verified at the tag, not assumed: at 3.0.0 `_pipeline_core_version()` is `return version("views_pipeline_core")` with no editable detection; at 3.0.1 it reads `direct_url.json` and returns `"unknown"` when `dir_info.editable` is set. E3 now records the discharge. + +**The defect is that it fired on 2026-08-13 and not on 2026-08-11.** The guard observed *our* installed distribution, so it could not see 3.0.1 until this repository's lockfile moved to it. For two days E3 carried a false claim about a released artifact and every check was green. The guard caught it only because the lock bump and the release inspection happened in the same change — had the bump come later, the staleness would have waited for it. + +This is the same shape as vpp_017 §7a, arrived at from the other side: a check may rest on a fact we own, on a fact another repository has declared in the public registry, or on an outcome we can observe. Our pin is a fact we own, but it was standing in for *pipeline-core's release feed*, which is none of the three. The guard measured a proxy and reported the proxy's date. + +**What replaces it is smaller on purpose.** E3 now says the lift is in force for producers running 3.0.1 or later, and no future release falsifies that — there is no expiry left to watch, so re-pointing the tripwire at 3.0.1 would be inventing one. The successor checks only that E3 does not regain the superseded sentence and still names the release that discharged it, mutation-proven on three branches. Related: C-86 (upstream editions), C-97. + +--- + ### C-97: Coordinate values sit in docstrings and comments, where the scan deliberately does not look | Field | Value | @@ -170,7 +192,7 @@ that indexes only deleted code is noise. | Source | `/code-review max` on #242, 2026-08-12; count reproduced here | | Trigger | **Either.** (a) A check whose failing function contains one of these values starts firing in CI. (b) Someone proposes widening the no-copy scan to docstrings — at which point this entry is the measurement that says what that would cost. | | Owner | This repository. | -| Location | Measured across `git ls-files '*.py'`, **25 standalone occurrences in 8 files** (2026-08-12, down from 33 in 9 when filed — #242 cleared the no-copy scan's own docstring and #248 cleared `tests/test_product.py`). Includes `views_postprocessing/{unfao,crafd}/managers/`, `contract/store_metadata.py`, `contract/wire/sink.py`. **The count moves whenever prose is edited; re-measure rather than cite it.** | +| Location | **Counting distinguishable coordinates only** — i.e. excluding the two whose declared values are this package's own directory names, which appear everywhere and are not evidence of anything. On that basis, measured across `git ls-files '*.py'`: **25 standalone occurrences in 8 files** (re-confirmed 2026-08-13; 2026-08-12, down from 33 in 9 when filed — #242 cleared the no-copy scan's own docstring and #248 cleared `tests/test_product.py`). Includes `views_postprocessing/{unfao,crafd}/managers/`, `contract/store_metadata.py`, `contract/wire/sink.py`. **The count moves whenever prose is edited; re-measure rather than cite it — and state which basis you used.** Counting *all* values including the package-name collisions gives 139 in 31 files on the same tree; an audit that did not know the basis reported 75 in 24 and read as a contradiction. The number was never wrong; the method was never written down. | The no-copy scan compares string **constants** and excludes docstrings outright. That exclusion is C-57's recorded lesson: an early draft fired on refusal labels and on docstrings naming which store a function serves, and *"a guard that fails on `def file_metadata(record)` gets deleted — after which the real rule is unguarded"* (ADR-014 §3). @@ -218,7 +240,7 @@ Cross-refs: **C-94** (the mechanism this permission authorises), **C-95** (the m | Source | `/expert-code-review` of the standing decisions, 2026-08-12 | | Trigger | Anyone reasons from the integration-test prohibition — for a preflight, a drill, or a new ADR. | | Owner | This repository. | -| Location | `reports/technical_risk_register.md` — corrected at all three sites 2026-08-12 (#249); this entry is the record, and the remaining mentions of `þing-02 D2` are its own narration. | +| Location | `reports/technical_risk_register.md` (three sites, corrected 2026-08-12 in #249); `tests/test_store_construction.py` and `docs/CICs/UNFAOPostProcessorManager.md` (two more, **found 2026-08-13 and corrected then** — the #249 claim of "all three sites" counted only the register). Remaining mentions of `þing-02 D2` are this entry's own narration. | This register cites **þing-02 D2** for the ruling that integration tests against the production Appwrite project are forbidden. þing-02 D2 is about identity and key separation. The ruling is **þing-01 D2** (`þingit/01_identity_secrets_config/orð_dómr.md:53-61`), and it differs from the paraphrase in two ways that matter: it is **conditional** (*"until the operator creates one"*), and it **grants** read-only preflight validation as the permitted live check. It also records that creating a test project is **assigned to the operator** and gates the provisioning-path drill — an open assignment, not a closed door. @@ -233,7 +255,7 @@ Cross-refs: **C-94**, **C-96**, þing-01 `orð_dómr.md` D2, issue #249. | ID | C-94 | | Tier | 2 — the failure mode is invisible by construction and lands on the live FAO path: upload succeeds, storage is billed, the consumer's endpoint returns empty, nothing raises anywhere. ADR-013 §4.1a's *"invisible to the consumer, not merely degraded."* | | Source | `/expert-code-review` of the standing decisions, 2026-08-12 | -| Trigger | **Either.** (a) A delivery is reported empty by a consumer or by FAO. (b) `APPWRITE_READ_API_KEY` is provisioned for the launcher — at which point the deferral below has no remaining cost. | +| Trigger | **Re-specified twice on 2026-08-13; the first attempt was not exclusive and its own worked example matched two arms.** (a) A delivery is reported empty **and an upload occurred after the bucket reached the state under investigation** — that is what the preflight below would catch, and the time bound is what the first attempt omitted. (b) `APPWRITE_READ_API_KEY` is provisioned, at which point the deferral has no remaining cost. *(A third arm — "reported empty with no upload since" — was drafted and withdrawn: it is not observable from this repository, which the amendment says four lines on, and ADR-014 §4 requires a trigger someone can notice. It is a gap, and is stated as one below rather than dressed as a trigger.)* | | Owner | This repository, for the mechanism. The credential is the operator's. | | Location | `views_postprocessing/contract/wire/sink.py` (the upload path, where nothing verifies); `views_postprocessing/delivery/`. | @@ -241,7 +263,24 @@ Every mechanism this platform has aimed at invisible delivery is a **CI-time pro **The mechanism that would close it is known and is legal.** A producer-side **read-only findability preflight**: after upload, query the store read-only for a document whose `name` equals the declared `CONSUMER_DOCUMENT_NAME`; assert non-empty; log at ERROR and raise (ADR-008); remedy is the existing operator quarantine. It is authorised by the seam contract (see **C-96**), the `APPWRITE_READ_API_KEY` slot is already declared, and it is the only mechanism that survives a **third-party-operated private consumer**, because it asks nothing of them. -**Why it is deferred, stated honestly.** It needs a read credential wired into the launcher — an operator action, not a code change — and it adds a live network call to the delivery path. Delivery works today. Building it now would be building the right thing at the wrong time. That is a deferral with a trigger and an owner (ADR-014 §4), not an omission. +**Why it is deferred, stated honestly.** It needs a read credential wired into the launcher — an operator action, not a code change — and it adds a live network call to the delivery path. ~~Delivery works today.~~ Building it now would be building the right thing at the wrong time. That is a deferral with a trigger and an owner (ADR-014 §4), not an omission. + +**⚠ THE ORIGINAL TRIGGER FIRED ON 2026-08-12, WHILE THIS ENTRY WAS BEING WRITTEN — and the mechanism it defers would not have caught it.** Both halves of that matter. + +FAO emailed at **09:15 UTC** that `faoapi.viewsforecasting.org` returned no data and that a listing of the partner bucket showed **0 files**. In faoapi's words: *"They found it by hand and emailed us; nothing on our side paged."* That is trigger (a) as originally worded, verbatim. The struck sentence above — *"Delivery works today"* — was false at the moment it was written, and it was the entire justification for deferring. + +**But the cause was not an invisible delivery.** faoapi's post-mortem (`views-faoapi/reports/post_mortems/2026-08-13_fao_empty_bucket_unannounced_migration.md`) records that *"the seam coordinates match (the producer writes to the FAO bucket under the declared document name; the consumer reads exactly that — the ADR-017 invisible-delivery work held)"*, and that the empty bucket was *"the migration + no-delivery-since state, not a producer/seam/credential failure"* — a deliberate destructive migration upstream, with no run executed since. **No upload occurred**, so a post-upload findability check would have observed nothing and reported nothing. + +**So the trigger was mis-specified, not the mechanism.** "A delivery is reported empty" names a symptom with at least two causes, and this entry's preflight addresses only one of them. + +**Two uncovered causes, stated as gaps rather than dressed as triggers.** Neither is observable from here, so neither can be a trigger under ADR-014 §4 — a trigger nobody can notice is a wish: + +1. *The bucket is empty because nothing was delivered.* This repository is not told when a delivery is due and has no view of whether the last one is still present. That is the 2026-08-12 case. +2. *The bucket is not empty but what is served is stale.* faoapi's own post-mortem records a warm per-key cache that can serve stale historical over an emptied bucket — so "reported empty" would not even be the symptom. + +Both belong to whoever can see delivery cadence, which is not this seat. Recorded here so the next reader does not mistake the trigger's narrowness for coverage. + +**The preflight is still not built**, and the reason is now sharper than "delivery works today": the case that fired is not the case it catches, and `[secret.APPWRITE_READ_API_KEY]` on the live registry still reads `status = "planned — operator issues (D4)"`. **What it would not cover, so nobody over-reads it later:** it proves the document is findable by that name in the store. It does not prove the consumer's code queries by that name. That last link is theirs, and issues asking each consumer to bind their *query* to their constant are filed under #248. @@ -278,7 +317,7 @@ Two of the three are fixed, and the third moved: 1. **Both branches name the coordinate, never the value.** Each builds its own sentence — two f-strings, deliberately not one shared formatter — and neither has the value in hand. They name **all** coordinates declaring that value: measured, two pairs share one (the prod-forecasts bucket and collection share both id and name), so a `value -> name` map would have named the wrong coordinate half the time in the message a maintainer uses to find the copy. 2. **The rotation proof asserts both sides absent**, taken from the fixture's own variables rather than from two literals a rename would quietly orphan. Mutation-proven: leaking the post-rotation side while keeping the pinned side digested fails now and **passed before** — the more damaging half, unchecked. -3. **The rule is asserted behaviourally**, by `test_the_scan_reports_a_copy_without_reprinting_it`: plant a value in a fixture tree, run the real scan, read the finished message. +3. **The rule is asserted behaviourally**, by `test_the_scan_reports_where_a_copy_is_and_never_what_it_is`: plant a value in a fixture tree, run the real scan, read the finished message. **And the third one took two attempts, which is the part worth recording.** The first version asserted that every finding was *routed* through `_report_a_copy` — an AST walk over the scan's own source. Five independent reviewers were run against it and **routing turned out not to be safety**: the value could be smuggled through either of that helper's two parameters, appended with `extend` or `+=`, or reported from a renamed accumulator. Six of eight mutations survived, and one legitimate refactor *failed* it — a guard that misses the thing and fires on the innocent, which is C-82's shape and ADR-014 §3's deletion criterion at once. The docstring's claim that a caller "cannot print one however it is written" was false when written. @@ -300,10 +339,19 @@ Net **−140 lines**, leaving the file **+55 over its pre-story size** rather th **What is deliberately NOT chased.** Four surviving mutations narrow the scan's *scope* — a length floor, a dropped section, a swallowed `SyntaxError`, a `break` after the first finding — and none is visible to a test that plants its own fixture. They are **#243**'s subject and are routed there. A fifth deletes the no-print assertion itself: infinite regress, carried here instead. Two residuals stand: the markdown branch's output is not behaviourally proven (it holds no value by construction), and a leak shorter than the planted fixture would pass. Chasing either is the whack-a-mole this epic exists to refuse. -4. **The scan's own docstring carried four registry values**, and pytest prints the failing function's source — so the guard would have published them on exactly the event it exists to catch. The message was clean; the traceback was not. Now it names coordinates. **The wider finding is registered separately as C-97**: 33 standalone values sit in docstrings and comments across nine files, production modules included, and the AST scan excludes docstrings by a deliberate C-57 decision taken before anyone counted them. +4. **The scan's own docstring carried four registry values**, and pytest prints the failing function's source — so the guard would have published them on exactly the event it exists to catch. The message was clean; the traceback was not. Now it names coordinates. **The wider finding is registered separately as C-97**: standalone values sit in docstrings and comments across several files (**25 in 8** as of 2026-08-13 — C-97 owns the number and the counting basis; do not cite it from here), production modules included, and the AST scan excludes docstrings by a deliberate C-57 decision taken before anyone counted them. **Deliberately still open, and moved rather than closed:** the `secret` exemption at `:1042` and the ban-set's package-name collision are the *scope* of the scan, not its reporting, and belong with the matcher rewrite in **#243**. This entry stays open until they land, because closing it now would close a Tier 2 on two-thirds of its content. +**⚠ The stated closing condition IS met, and this entry stayed open anyway — 2026-08-13.** It said it would stay open "until [the `secret` exemption and the ban-set collision] land" with #243. **#243 closed 2026-08-12 and both landed**: the exemption is gone (`scanned_sections` now filters `_TABLE_ROLE` for `CONSUMED` with no subtraction), and the collision cannot arise because the markdown branch is a NAME=VALUE pair matcher. The length floor went too. + +What actually keeps it open is neither of those — it is the two deferrals below, **whose triggers fired while nobody was watching them**: + +- Deferral 1's trigger is *"when #243 finishes touching `tests/test_env_declaration.py`"*. #243 finished. The leak guards are still in that file; `tests/test_redaction_guard.py` is only cross-referenced. +- Deferral 2 was *"routed to #243"* — and #243 closed without it. `test_the_drift_check_would_catch_a_rename` still rebuilds its subject's comparison with its own comprehension rather than driving the checked function. + +Both were unowned, which is worse than deferred — **now filed as #265**, with acceptance criteria and the reason each is not urgent. That issue closing is what closes this entry: its stated condition was already met by #243. *(An earlier draft of this amendment named the problem and left it there, which under ADR-014 §4 converts two compliant deferrals into two non-compliant items. Naming is not rehoming.)* + **Two deferrals, both with triggers (ADR-014 §4).** 1. **The leak guards belong in `tests/test_redaction_guard.py`**, which already owns "what stops us leaking" and which `test_the_environment_refusal_logs_names_and_never_values` already points readers to. The concern is currently split across two files with no shared vocabulary. Not moved here, because moving code while fixing bugs in it is how the next defect arrives. **Trigger: when #243 finishes touching `tests/test_env_declaration.py`.** Owner: this repository. @@ -328,7 +376,7 @@ PR #239 retired the FAO half of the source-reading check on the strength of view views-faoapi#379 binds their *served-name constant* to the registry row. The assertion this repository deleted was `'filters["name"] = self.model_path.model_name'` — that they still *query* on it. So views-faoapi can refactor its selection mechanism, leave its name constant untouched, pass #379, pass our registry comparison, and serve an empty endpoint. The deleted assertion carried that exact sentence: *"the name may still match while the consumer filters on something else entirely — same invisibility, different cause."* Nothing carries it now. -**Verified 2026-08-12, and the state is good — which is why this is a risk and not an incident.** Read at `views-faoapi@origin/development`: `_CONSUMER_DOCUMENT_NAME = "un_fao"` (`src/views_faoapi/managers/api.py:59`) reaches `APIPathManager(...)` at `:1282`, and `managers/prediction/manager.py` still filters on it at `:117` and `:435`. Their D2 test — views-faoapi's `tests/test_seam_contract_binding.py` — imports the constant from the production module rather than re-typing it, which is better than it had to be. **The composition is what nothing asserts**: constant↔registry is checked by them, query↔constant was checked by us and is not any more. Also worth recording: their D2 check is on `development`; their `main` is 22 commits behind at PR #357, so a reader taking "#379 merged" to mean "live on their default branch" is over-reading it. +**Verified 2026-08-12, and the state is good — which is why this is a risk and not an incident.** Read at `views-faoapi@origin/development`: their `CONSUMER_DOCUMENT_NAME` — moved since to `src/views_faoapi/seam_contract.py`, a dedicated module — reaches `APIPathManager(...)` in `managers/api.py`, and `managers/prediction/manager.py` still filters on it at `:117` and `:435`. Their D2 test — views-faoapi's `tests/test_seam_contract_binding.py` — imports the constant from the production module rather than re-typing it, which is better than it had to be. **The composition is what nothing asserts**: constant↔registry is checked by them, query↔constant was checked by us and is not any more. Also worth recording: their D2 check was on `development` while `main` sat 22 commits behind; **as of 2026-08-13 that gap is 2 commits**, so the caveat has all but expired. **C-87 is not this.** C-87 records that we verify our copy against the declaration rather than the consumer's code against it. This is narrower and worse: for one partner we briefly had the second check and gave it up for something that does not cover the same failure. @@ -464,6 +512,8 @@ classified `IGNORED` in `tests/test_env_declaration.py::_TABLE_ROLE` — declare silently unseen. Nothing here reads `obliges_consumers` yet, so the false-alarm rate is still carried by the row-level differential rather than by upstream's own flag. +**⚠ The re-pin deferral below is DISCHARGED, 2026-08-13, by upstream rather than by us.** The registry now publishes `obliges_consumers_since = "1.5.2"` — *"the number a consumer should pin against"* — and both `appwrite_env.py` modules already pin exactly `1.5.2`. So the pin is conformant by upstream's own new rule and the "re-pin is **due**" urgency below is withdrawn. Re-pinning further forward is now hygiene with no safety consequence, which is what the deferral originally said before the trigger fired. + **Deferral, with the trigger ADR-014 §4 requires** — this was carried in a pull-request description, where deferrals go to be forgotten: @@ -595,6 +645,11 @@ Cross-refs: **C-77** (the same field's producer-side half, resolved), ADR-013 § Cross-refs: **C-72** (the re-vendor this would have triggered, and the trigger A2 now rides on), **C-77** (the historical leg whose correctness is what makes the co-delivery premise false), ADR-013 §2.2a, ADR-014 §4 (the deferral's named trigger), #133, views-faoapi ADR-033 and its register C-169. + +**⚠ This entry has no closing condition, and that is the finding — 2026-08-13.** Its Trigger is a habit (*"check what this repository already delivers before writing code"*), it states *"No guard is proposed, deliberately"*, and its Location says *"Not a code defect."* **No evidence in any tree can ever satisfy it**, so it cannot be closed, only carried — which is what a register is not for. + +It is a worked example wearing a risk's clothes. **Give it a real trigger or move it to a lessons artifact**; the post-mortem at `reports/post_mortems/2026-08-12_the_guards_that_did_not_guard.md` is the natural home. Left open here only because relocating a record is itself a change that should be deliberate rather than done in a truth pass (Register Conventions: a relocation is not complete until the destination exists and is cited by number). + --- ### C-84: Every identity this repo delivers under dies on 2026-11-17, within 3h35m of the other @@ -669,7 +724,7 @@ See also C-17 (RESOLVED — implicit column naming between mapper and manager), | Field | Value | |-------|-------| | ID | C-26 | -| Tier | 1 — silent data fabrication with no error signal: absence of evidence becomes evidence of absence in FAO-delivered values | +| Tier | ~~1~~ → **2**, re-tiered 2026-08-13 by this entry's own rule once its Tier-1 gate was answered (see the amendment below). Original rationale, which applied while the gate was open: silent data fabrication with no error signal: absence of evidence becomes evidence of absence in FAO-delivered values | | Source | `expert-code-review` (2026-06-12) | | Trigger | When changing the historical fetch path, or when bumping views-pipeline-core's dataloader — verify whether the **currently active** path (`get_feature_frame`, since #126) zero-fills missing months/cells, and that any fill count is logged rather than silent | | Location | views-pipeline-core `modules/dataloaders/dataloaders.py:1208` (`fillna(0.0)`, legacy pandas fetch); consumed at `views_postprocessing/unfao/managers/unfao.py:125-147` (`_read_historical_data`, legacy branch). Frame-native branch: `:101-124` (`_read_historical_frame` → `get_feature_frame`) | @@ -684,21 +739,17 @@ See also C-25 (same data path, wrong-file variant), C-15 (upload provenance woul **OPEN VERIFICATION QUESTION (review-rr 2026-07-31) — tier held at 1 pending an answer.** `fillna` has **zero occurrences in this repo**; the fabrication site is entirely upstream. Since #126, the historical path run-0 actually used is `get_feature_frame` (`_read_historical_frame`), **not** the pandas `get_data` branch that reaches `dataloaders.py:1208`. It could not be verified from this seat (views-pipeline-core is deliberately absent from test environments, per repo convention). **Question for the pipeline-core seat: does `get_feature_frame` inherit the same unconditional `fillna(0.0)`, or does the frame-native fetch propagate NaN?** If it propagates NaN, this Tier 1 now describes only the legacy branch (retirement is the named post-run-0 follow-up) and should be re-tiered. **Do not downgrade on inspection of this repo alone** — the deliverable ran through the unverified path at global scale on 2026-07-27. ---- -### C-27: Loader construction failures swallowed — surface as remote AttributeError +**⚠ The Tier-1 gate is ANSWERED, and the answer is no — 2026-08-13.** This entry says it is Tier 1 *until* someone establishes whether `get_feature_frame` inherits the `fillna(0.0)`. **views-pipeline-core#366 closed 2026-08-04**, and at the version this repository now installs the loader states the opposite in its own docstring: *"no silent `fillna(0.0)` — NaN policy belongs to the engine boundary."* -| Field | Value | -|-------|-------| -| ID | C-27 | -| Tier | 2 — structural fragility: any dependency or config breakage is converted into a misleading crash far from its cause | -| Source | `expert-code-review` (2026-06-12) | -| Trigger | When bumping views-pipeline-core, or changing this postprocessor's queryset/config — verify a `ViewsDataLoader` construction failure surfaces its real exception rather than a downstream `AttributeError`; today it is caught bare, logged as "No Queryset detected" with `exc_info=False`, and replaced with `self._data_loader = None` | -| Location | views-pipeline-core `managers/model/model.py:883-902`; crash sites `views_postprocessing/unfao/managers/unfao.py:105` (`_read_historical_frame`), `:134` (`_read_historical_data`) | +By this entry's own re-tier rule, Tier 1 is no longer justified. -**Filed upstream 2026-08-01 as views-pipeline-core#367**, cross-referenced to their **#168** (views-pipeline-core C-166, narrow Appwrite exception handling) as the same defect class on a different call path — catch broadly, guess at the cause, discard the evidence — worth deciding once rather than twice. +**Three further claims are now false**, and each would send a reader to the wrong place: +1. The Location points at a legacy branch in `unfao.py` that **no longer exists** — `_read_historical_data` refuses any queryset not declaring `feature_frame` and calls only `_read_historical_frame`. +2. *"There is no fill-count logging"* — false upstream; the fill now logs total and per-column counts before substituting. +3. *"The zarr exposes `last_valid_month_id` … but it is not consulted"* — false here; it is read via `source_metadata.last_valid_month_id`, fabricated months are dropped, and it degrades open with a log line. -`_initialize_data_loader()` catches bare `Exception`, discards the traceback, and nulls the loader. The failure then surfaces as `AttributeError: 'NoneType' object has no attribute 'get_data'` in `_read_historical_data` — the operator debugs the postprocessor while the cause (import error, malformed config, path issue) was erased at construction time. Cost is time-to-diagnosis during exactly the runs where time matters. +**What genuinely survives is upstream and different:** views-datafactory pre-fills its grids with `fill_value=0.0` per its ADR-047, tracked as **views-datafactory#420 (OPEN)**. That is the live risk; the mechanism this entry describes is not. --- @@ -716,6 +767,12 @@ See also C-25 (same data path, wrong-file variant), C-15 (upload provenance woul See also C-13. +**Filed upstream 2026-08-13 as views-pipeline-core#471.** Two years of this entry sitting here with two named fix sites and **no issue filed anywhere** is the finding. The neighbouring entries C-26 and C-27 were filed upstream on 2026-08-01 and **both closed within three days** — so the expected cost of filing was three days and the expected cost of not filing was this entry's whole lifetime. + +Cross-referenced there to their **#248 / #347** (the same defect class on the Appwrite call path, already fixed) and **#168**. The fix sites are in their tree: this repository calls `get_feature_frame` and has no view of the transport. + +**A second argument the entry did not have when filed.** On 2026-08-12 FAO reported empty endpoints, and it took a day to establish the cause was an unannounced migration with no delivery since. A hung run and a run nobody started are indistinguishable from outside — a deadline turns the second into a loud failure. See **C-94**. + --- ### C-30: GAUL-uncovered land cells crash or corrupt global delivery (absorbs C-34: the coverage contract) @@ -768,11 +825,11 @@ This entry's own Tier-2 rationale was that the design *"forces copy-pasting a 27 diff views_postprocessing/unfao/managers/unfao.py \ views_postprocessing/crafd/managers/crafd.py | grep -c '^[<>]' -**32** — sixteen differing lines on each side. Substitute every form of the partner name (case-insensitively, including `un_fao`/`un_crafd` and `faoapi`) and it falls to **2**: one line per side. +**26** — thirteen differing lines on each side (re-run 2026-08-13 with the command above; it read **32**/sixteen when filed, and the drift is itself the entry's point). Substitute every form of the partner name (case-insensitively, including `un_fao`/`un_crafd` and `faoapi`) and it falls to **2**: one line per side. -The sixteen are, by category: one import, one class name, two partner-named method definitions, their two call sites, one refusal-message string, one line that is *both* the `*_ENV` tuple reference and the store label, the four env-name literals, and four lines of prose. +The **thirteen** are, by category *(the breakdown below was written for the sixteen and has not been re-derived — treat the count above as authoritative and re-run the command rather than this list)*: one import, one class name, two partner-named method definitions, their two call sites, one refusal-message string, one line that is *both* the `*_ENV` tuple reference and the store label, the four env-name literals, and four lines of prose. -**None of the difference is behaviour, but "byte-identical" is too strong for one method.** `_read`, `_transform`, `_validate`, `_check_coverage` and `_build_historical_artifact` are byte-identical. `_save_contract` is not: five of the sixteen fall inside it — the datastore call, two comments, and the refusal string. All five are partner-name substitutions; none changes what the method does. +**None of the difference is behaviour, but "byte-identical" is too strong for one method.** `_read`, `_transform`, `_validate`, `_check_coverage` and `_build_historical_artifact` are byte-identical. `_save_contract` is not: **four** of the thirteen fall inside it (lines 356, 367, 368, 371) — two comments, the refusal string, and one call argument. The datastore construction at :355 is **byte-identical**. All four are partner-name substitutions; none changes what the method does. *(An earlier correction updated "sixteen"→"thirteen" without re-checking this sentence, and left "five … the datastore call" — both wrong.)* *(**This paragraph was wrong five times, and how it was wrong is the entry's most useful content.** (1) "roughly ten lines", carried from the review that found it and never measured. (2) A normalised count of 4 and a claim that `_save_contract` was byte-identical, neither checked. (3) A story that #211 "fixed two divergences that already existed" — false: at `9799e87` the second line was **byte-identical in both files**, an inherited inaccuracy rather than a divergence, and rewording CRAF'd's copy is what *created* a divergence there. Only the `:222` pair was real. (4) and (5) An exact list of sixteen line numbers and a line count, invalidated twice within the hour by comment corrections elsewhere in the same file.* @@ -863,7 +920,7 @@ That is a smaller change than it sounds and a bigger one than it looks. Smaller: Tier held at 2: the blast-radius argument is unchanged for what remains, and containment is not removal. -*Did not:* the double inheritance (the `class UNFAOPostProcessorManager(...)` statement — this row cited `unfao.py:80` when written, and that number has moved twice since) stands, and so do consequences (a) — the FAO logic still cannot be instantiated without the framework — and (c)/(d). **This entry remains open on exactly that scope.** Its remaining fix is gated on views-pipeline-core's 3.0.0 (C-44/C-62), which is a release signal rather than engineering work. +*Did not:* the double inheritance (the `class UNFAOPostProcessorManager(...)` statement — this row cited `unfao.py:80` when written, and that number has moved twice since) stands, and so do consequences (a) — the FAO logic still cannot be instantiated without the framework — and (c)/(d). **This entry remains open on exactly that scope.** ~~Its remaining fix is gated on views-pipeline-core's 3.0.0 (C-44/C-62), which is a release signal rather than engineering work.~~ **Refuted by the 2026-08-05 update twenty lines above**, which records that 3.0.0 shipped on 2026-08-03 and nothing became possible — the gate is views-models, not a release. Struck 2026-08-13 because it was the last sentence in the entry and therefore the one a reader leaves with. See also C-07/C-27/C-29 (pipeline-core coupling symptoms), C-39 (the dead-mapper cleanup that precedes any unfao restructuring), **#45** (the delivery-side draw carrier — ship `(N, S)` uncollapsed as a native frame, the producer half of this same problem), and **epic #85** (the migration backlog). @@ -899,14 +956,14 @@ 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 17 fewer tests than local, and nothing requires it to pass +### C-81: What actually gates `main` is weaker than it looks — CI verifies 8 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:** when the Appwrite Seam Contract registry next moves — it moved twice on 2026-08-03 alone — nothing in CI will notice; only a maintainer running the suite locally will. ~~**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 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). | | 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` | @@ -1030,6 +1087,36 @@ See also C-40 (the inheritance/representation coupling this migration unwinds), ## Resolved Concerns +### C-27: Loader construction failures swallowed — surface as remote AttributeError — RESOLVED + +| Field | Value | +|-------|-------| +| ID | C-27 | +| Tier | 2 — structural fragility: any dependency or config breakage is converted into a misleading crash far from its cause | +| Source | `expert-code-review` (2026-06-12) | +| Trigger | When bumping views-pipeline-core, or changing this postprocessor's queryset/config — verify a `ViewsDataLoader` construction failure surfaces its real exception rather than a downstream `AttributeError`; today it is caught bare, logged as "No Queryset detected" with `exc_info=False`, and replaced with `self._data_loader = None` | +| Location | views-pipeline-core `managers/model/model.py` (`_initialize_data_loader`); crash sites `views_postprocessing/unfao/managers/unfao.py::_read_historical_frame` and `::_read_historical_data`. Function names, not line numbers — the cited `:105`/`:134` had already moved to `:183`/`:207`. | + +**RESOLVED 2026-08-13 — fixed upstream, and this repository was installing the version without the fix.** + +views-pipeline-core#367 landed in **3.0.1** (released 2026-08-11): `managers/model/model.py` now logs with `exc_info=True` and **re-raises**, with a comment naming this repository's `AttributeError` as the symptom it produced. + +`pyproject.toml` already allowed it (`>=3.0.0,<4.0.0`), but `poetry.lock` still resolved **3.0.0** — so CI installed the unfixed version for two days after the fix shipped. Bumped with `poetry update views-pipeline-core --lock`: exactly one package moved in the lockfile, and nothing else — `Requires-Dist` is identical between the two releases, so no sub-dependency changed. + +**The local suite does not verify this, and saying it did would be the defect this pass exists to remove.** This machine resolves `views_pipeline_core` to an editable checkout, not to either release, so the 422 passed proves nothing about 3.0.1. **CI is the only real test** — it runs `poetry install` and takes what the lock says. Note also that a patch bump is not a small payload here: 3.0.1 changes ~20 files and splits a module (their #431). Nothing this repository imports moved, which is the check that matters. + +*Verification note, because the obvious check is misleading:* `grep -c "exc_info=True"` returns **6 in both tags**. The fix is identifiable only by the comment naming #367 — present in 3.0.1, absent in 3.0.0. A count that looks decisive and is not. + +**What this leaves.** The trigger stands as written for the next bump: a version constraint that permits a fix is not the same as a lockfile that installs it, and nothing here compares the two. That is a general gap, not this entry's — noted rather than built. + +**Filed upstream 2026-08-01 as views-pipeline-core#367**, cross-referenced to their **#168** (views-pipeline-core C-166, narrow Appwrite exception handling) as the same defect class on a different call path — catch broadly, guess at the cause, discard the evidence — worth deciding once rather than twice. + +`_initialize_data_loader()` catches bare `Exception`, discards the traceback, and nulls the loader. The failure then surfaces as `AttributeError: 'NoneType' object has no attribute 'get_data'` in `_read_historical_data` — the operator debugs the postprocessor while the cause (import error, malformed config, path issue) was erased at construction time. Cost is time-to-diagnosis during exactly the runs where time matters. + +--- + +--- + ### C-91: The git plumbing this arc added turns ordinary developer states into hard errors, bare tracebacks, and one possible hang — RESOLVED | Field | Value | @@ -1063,7 +1150,6 @@ Cross-refs: **C-90** (the same module's untested core), **C-88** (why the module --- - ### C-90: A mutation proof that cannot fail, and the untested function a module was extracted to create — RESOLVED | Field | Value | @@ -1105,7 +1191,6 @@ Cross-refs: **C-86** (whose partial-mitigation paragraph this falsifies), **C-89 --- - ### C-93: A mutation proof written by whoever wrote the guard tests that author's imagination, not the guard — RESOLVED | Field | Value | @@ -1137,7 +1222,6 @@ Cross-refs: **C-57** and **C-89** (the guard this was measured on), **C-90** (a --- - ### C-82: Governance-artifact prose carries numbers and statuses that nothing checks — RESOLVED 2026-08-05 | Field | Value | @@ -1867,7 +1951,7 @@ The right axis was **exact equality on string constants**, not statement shape. **Gated, and honestly so.** The checks need a views-appwrite checkout and skip without one, naming `VIEWS_APPWRITE` and the conventional sibling path so a contributor can run them rather than merely watch them skip. The would-catch-a-rename proof runs in CI with no checkout at all. Resolution helper shared with **C-46** (S7) in `tests/conftest.py` — the second incident, which is this repo's named trigger for extracting. -**Residual — RESOLVED 2026-08-10 (ADR-016).** The gated half did not run in CI, which needed a views-appwrite checkout in the workflow. It has one: that repository went public on 2026-08-08 and the workflow now fetches it, so these checks run on every pull request. That was recorded here and in **C-46** as *"a CI-cost and cross-repo-coupling decision, not a code fix … worth deciding once for both"*, and it sat as a residual on two RESOLVED entries, which is where residuals go to be forgotten. It now has a live entry with a measured cost (17 tests, 9 of them new in this arc), a per-sibling answer, and an owner: **C-81**. views-appwrite is private, so it needs a token — an operator decision. +**Residual — RESOLVED 2026-08-10 (ADR-016).** The gated half did not run in CI, which needed a views-appwrite checkout in the workflow. It has one: that repository went public on 2026-08-08 and the workflow now fetches it, so these checks run on every pull request. That was recorded here and in **C-46** as *"a CI-cost and cross-repo-coupling decision, not a code fix … worth deciding once for both"*, and it sat as a residual on two RESOLVED entries, which is where residuals go to be forgotten. It now has a live entry with a measured cost, a per-sibling answer, and an owner: **C-81**. *(As written this said "17 tests, 9 of them new in this arc" and that views-appwrite is private and needs a token. Both are superseded: views-appwrite went public on 2026-08-08 and is fetched by CI, and the gap is now **8 tests, all views-datafactory** — no credential closes it. See C-81.)* A second, smaller instance of the same shape: these checks parse TOML with `tomllib`, stdlib from Python 3.11, and `pyproject` declares `>=3.11`. CI runs 3.11 and executes them. The maintainer's box runs **3.10**, below the declared floor, so they skip there — the local suite is quietly weaker than a green `pytest -q` suggests. Not a repo defect and not worth its own entry; recorded because "a gate that does not run" is exactly what C-46 is open for, and the CI decision should cover both. | | Tier | 3 | diff --git a/tests/conftest.py b/tests/conftest.py index 1943321..8fdde29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -136,7 +136,7 @@ class Sibling: ci_checkout=True, note=( "PUBLIC. Fetched, and the fetch is load-bearing rather than convenient: it " - "carries the coordinate-registry drift checks, and under ADR-017 it becomes " + "carries the coordinate-registry drift checks, and under vpp_017 (ADR-017) it becomes " "the authority source for the delivery-label check too. Pruning it as an " "unused sibling would disable both and leave a green build — the invisible " "skip ADR-016 §6 exists to prevent." @@ -149,11 +149,11 @@ class Sibling: "PRIVATE — the only one, and that is why it is not fetched. Checking it " "out needs a credential, which is an " "operator decision deferred pending a request to FAO to make the repository " - "public. **Nothing is dark because of this any more** (2026-08-11): ADR-017 " + "public. **Nothing is dark because of this any more** (2026-08-11): vpp_017 " "moved the delivery-label check onto the public coordinate registry, which " "needs no credential, so neither side reads the other. This entry stays " "PRIVATE because the fact is still true, not because anything is blocked on " - "it. See ADR-017 and register C-81." + "it. See vpp_017 and register C-81." ), ), "views-crafdapi": Sibling( @@ -164,7 +164,7 @@ class Sibling: "read this consumer's source for its query filter — and #248 deleted that " "check rather than repairing it: the same read broke twice in 24 hours " "because both consumers refactored a literal into a named constant, which " - "ADR-017 §7 predicted. The declaration stays because CONSUMER_REPO still " + "vpp_017 §7 predicted. The declaration stays because CONSUMER_REPO still " "names this repository; only the fetch is gone. What the source read used to " "cover is register C-92, and views-crafdapi#55 is the ask that would close it " "where the fact lives." @@ -181,7 +181,7 @@ class Sibling: #: #: It exists so this repository records **who receives each delivery**. It used to also #: locate a sibling checkout so the consumer-document-name pin could be read from that -#: consumer's source; #248 deleted that read (ADR-017 §7 — we were never entitled to +#: consumer's source; #248 deleted that read (vpp_017 §7 — we were never entitled to #: depend on another repository's file layout, and two consumers proved it in a day by #: improving theirs). What the map is for now is addressing: it is who register C-92's #: cross-repo asks are sent to. diff --git a/tests/test_env_declaration.py b/tests/test_env_declaration.py index 2f36f7d..9632514 100644 --- a/tests/test_env_declaration.py +++ b/tests/test_env_declaration.py @@ -509,14 +509,14 @@ def test_every_declared_name_is_classified_here(partner): #: **Declared, and asserted against the live registry** — a table appearing upstream that #: nobody here has classified is a change this repository has not looked at. That is not #: hypothetical: `[contract.*]` arrived in v1.5.0 carrying a live obligation (the -#: ADR-017 delivery-label mirror), and the ONLY mechanism here that noticed was a +#: vpp_017 (ADR-017) delivery-label mirror), and the ONLY mechanism here that noticed was a #: version-string comparison that fired for the wrong reason and was deleted with this #: change. Four tables were being ignored silently at the time. _TABLE_ROLE = { "connection": "CONSUMED", # parsed by _declared_classes; coordinates we read "target": "CONSUMED", # parsed by _declared_classes; coordinates we read "secret": "CONSUMED", # parsed by _declared_classes; the operator's slots - "contract": "MIRRORED", # values live in our source by design (ADR-017 §5); + "contract": "MIRRORED", # values live in our source by design (vpp_017 §5); # checked by tests/test_product.py, not by _declared_classes "excluded": "IGNORED", # names the registry records as deliberately NOT coordinates # IGNORED because nothing here READS it, not because it is none of our business — @@ -693,7 +693,7 @@ def test_every_table_in_the_registry_is_classified_here(): But it did have a real job underneath the noise, and this is that job. On 2026-08-10 the registry grew a ``[contract.*]`` table carrying a live obligation for this - repository — the ADR-017 delivery-label mirror — and the edition check was the *only* + repository — the vpp_017 delivery-label mirror — and the edition check was the *only* mechanism here that noticed, because ``_declared_classes`` parses three tables and was silently ignoring four. @@ -1193,7 +1193,7 @@ def test_no_coordinate_value_is_copied_into_this_repo(): # Scoped by the declared partition, NOT by an inline tuple — so a new table cannot # be swept in by a one-word edit, and `contract` cannot be swept in at all. # - # `[contract.*]` values are MIRRORED: ADR-017 §5 requires them to appear in this + # `[contract.*]` values are MIRRORED: vpp_017 §5 requires them to appear in this # package's source, because we write them onto every upload. Banning them here would # forbid the thing the contract obliges. That is not an exception to "never copy a # coordinate" — it is a different class, declared upstream: the registry's own header diff --git a/tests/test_falsify_adr013_s2.py b/tests/test_falsify_adr013_s2.py index 7ce8cc4..42962f4 100644 --- a/tests/test_falsify_adr013_s2.py +++ b/tests/test_falsify_adr013_s2.py @@ -81,46 +81,42 @@ def test_s2_2a_is_not_secretly_in_force(): ) -def test_e3s_claim_about_unreleased_behaviour_is_still_true(): - """Erratum E3 says a pipeline-core fix is in no released version. That expires. - - E3 lifts §2.2's `pipeline_core_version` caveat in two halves. The second — that an - editable install reports ``"unknown"`` rather than a stale number — landed in - pipeline-core on 2026-08-04, a day and a half *after* 3.0.0 was uploaded to PyPI. So - the erratum states plainly that **no released version contains it**, and that it - becomes true of producers at the next release. - - That is a dated claim about someone else's release history, in the document that - punishes those hardest. It stops being true the moment pipeline-core publishes again, - and nothing about this repository would change to signal it. - - So: if the pipeline-core we are running is a **released distribution** (not an - editable checkout) and its version is past 3.0.0, the next release has happened and - E3's wording is stale. CI installs from PyPI, so this is live there even though a - maintainer's editable environment leaves it inert — which is stated rather than - discovered, because a guard that only ever runs in one place is half a guard. +def test_e3_does_not_still_say_the_fix_is_unreleased(): + """E3's expiry has fired and been discharged. This stops it coming back. + + The predecessor of this check was a tripwire: E3 claimed pipeline-core's + editable-install fix was "not yet in any released version", which is a dated claim + about someone else's release history, and it would go stale the moment pipeline-core + published again. It did — **3.0.1, 2026-08-11 13:40 UTC** — and on 2026-08-13 the + tripwire fired in CI and E3 was corrected. Verified at the tag: 3.0.0 returns + ``version("views_pipeline_core")`` with no editable detection, 3.0.1 reads + ``direct_url.json`` and returns ``"unknown"`` when ``dir_info.editable`` is set. + + What replaces it is deliberately smaller, because the thing it guarded is now stable. + "In force for producers running 3.0.1 or later" is not a claim any future release can + falsify, so there is no expiry left to watch and re-pointing the tripwire at 3.0.1 + would be inventing one. The only remaining failure is textual: a revert or a bad merge + restoring the sentence that is now false. + + One thing the tripwire got wrong is worth keeping in view. It observed *our* installed + distribution, so it could not see 3.0.1 until this repository's lockfile moved to it — + it reported the release two days late. A guard on another repo's release history that + watches our own pin is measuring the wrong thing; it caught this because the two + happened to coincide. """ - pytest.importorskip("views_pipeline_core", reason="a declared dependency") - from importlib.metadata import PackageNotFoundError, version as dist_version - - import views_pipeline_core - - source = Path(views_pipeline_core.__file__).resolve() - if "site-packages" not in str(source): - pytest.skip( - "pipeline-core is an editable checkout here, so its recorded version says " - "nothing about what has been released. This check is live in CI, which " - "installs from PyPI." - ) - try: - installed = dist_version("views-pipeline-core") - except PackageNotFoundError: # pragma: no cover - not a distribution at all - pytest.skip("pipeline-core is not installed as a distribution") - - parts = tuple(int(p) for p in installed.split(".")[:3] if p.isdigit()) - assert parts <= (3, 0, 0), ( - f"pipeline-core {installed} is released and past 3.0.0, so Erratum E3's claim " - "that the editable-install fix is 'not yet in any released version' is out of " - "date. Re-read E3 against that release: the second half of the lift is probably " - "now in force, and the sentence saying it is not must go." + # E3's own bullet, not the whole Post-adoption record: "Erratum E3" is also mentioned + # in §5 and "Erratum E2" appears in the header above both, so splitting on the bare + # names spans ~600 lines and would let either assertion be satisfied by unrelated text. + entries = re.split(r"^- \*\*(?=\d{4}-)", _ADR.read_text(), flags=re.M) + e3 = next((e for e in entries if e.startswith("2026-08-10 — Erratum E3")), None) + assert e3 is not None, "Erratum E3's entry is no longer in the Post-adoption record" + assert "not yet in any released version" not in e3, ( + "Erratum E3 has regained the wording that was false from 2026-08-11, when " + "views-pipeline-core 3.0.1 shipped the editable-install fix. E3 was corrected on " + "2026-08-13 to record that release; something has restored the superseded text." + ) + assert "3.0.1" in e3, ( + "Erratum E3 no longer names the release that discharged it. The lift's second " + "half is in force for producers running views-pipeline-core 3.0.1 or later, and " + "E3 is where a consumer looks that up." ) diff --git a/tests/test_product.py b/tests/test_product.py index e4b461d..91409f0 100644 --- a/tests/test_product.py +++ b/tests/test_product.py @@ -43,7 +43,7 @@ #: the delivery would upload, and the document would be invisible (ADR-013 §4.1a). #: #: ``test_the_declared_consumer_name_matches_the_registry`` closes that half against the -#: **public coordinate registry** rather than the consumer's source (ADR-017 §5). It runs +#: **public coordinate registry** rather than the consumer's source (vpp_017 (ADR-017) §5). It runs #: for every partner and needs no credential, because the registry is public even when the #: consumer is not — which is why it reaches the FAO partner and its predecessor could not. #: @@ -210,19 +210,19 @@ def test_every_partner_has_a_declared_contract_row(): @pytest.mark.parametrize("partner", PARTNER_PACKAGES) def test_the_declared_consumer_name_matches_the_registry(partner): - """ADR-017 §5, the producer half: our mirror against the public declaration. + """vpp_017 §5, the producer half: our mirror against the public declaration. **What this replaces, and why.** Until 2026-08-11 this check read the *consumer's source* — a regex over `managers/api.py` looking for the argument to an `APIPathManager(...)` construction. That worked on a laptop and never in CI for the private partner, and it broke on 2026-08-11 when views-faoapi tidied that file into a named constant. Their code got better and our check went looking for something that - had moved. ADR-017 §7: we were never entitled to depend on another repository's file + had moved. vpp_017 §7: we were never entitled to depend on another repository's file layout. Now both sides read one public declaration. The registry lives in views-appwrite, which is public, so this needs no credential even for the private partner — that is - the whole of ADR-017 §5 in one assertion. + the whole of vpp_017 §5 in one assertion. *"Neither reads the other" is now the present state, not just the end state* — #248 deleted the last source read. What that costs is register C-92: nothing here verifies @@ -239,7 +239,7 @@ def test_the_declared_consumer_name_matches_the_registry(partner): row = _CONTRACT_ROW[partner] assert row in contract, ( f"[{partner}] the registry has no `[contract.{row}]` row. That row is the " - "authority this package's CONSUMER_DOCUMENT_NAME mirrors (ADR-017 §5); without " + "authority this package's CONSUMER_DOCUMENT_NAME mirrors (vpp_017 §5); without " "it there is nothing to check the mirror against. Either it was retired upstream " "or this file names the wrong row." ) diff --git a/tests/test_register_integrity.py b/tests/test_register_integrity.py index e8b4491..7a47212 100644 --- a/tests/test_register_integrity.py +++ b/tests/test_register_integrity.py @@ -323,3 +323,37 @@ def test_the_register_is_dated_and_governed(register): "the header must carry an ISO Last Updated date" ) assert "ADR-010" in register, "the register must name its governing ADR" + + +def test_every_entry_is_fenced_off_from_the_one_above_it(register): + """An entry heading follows a horizontal rule, or opens its section. + + Register C-85 and C-26 each grew an amendment that was appended *below* the + entry's terminating ``---`` rather than above it. Markdown does not care, + but a reader does: both amendments rendered as an unheaded preamble to the + *next* entry, so C-85's correction appeared to be part of C-84 and C-26's + re-tiering appeared to be part of C-28. Nothing caught it, because every + other structural check here splits on ``### `` and so cannot see which side + of the rule a paragraph fell on. + + The rule is the only thing that marks where an entry stops. This check is + what makes appending to the wrong entry a test failure rather than a + rendering accident. + """ + lines = register.split("\n") + section_starts = {i for i, line in enumerate(lines) if line.startswith("## ")} + misfenced = [] + for i, line in enumerate(lines): + if not re.match(r"^### [CD]-\d+", line): + continue + # Opening an entry directly under its section heading is the one exception. + if i >= 2 and any(s in section_starts for s in (i - 1, i - 2)): + continue + if not (i >= 2 and lines[i - 1].strip() == "" and lines[i - 2].strip() == "---"): + misfenced.append(f" line {i + 1}: {line[:70]}\n preceded by: {lines[i - 2:i]!r}") + assert not misfenced, ( + "these entries are not fenced off from the entry above them, so anything " + "appended to their predecessor renders as their preamble:\n" + + "\n".join(misfenced) + + "\n\nan entry heading must follow a '---' rule and one blank line." + ) diff --git a/tests/test_store_construction.py b/tests/test_store_construction.py index 1abd151..a7d1895 100644 --- a/tests/test_store_construction.py +++ b/tests/test_store_construction.py @@ -14,8 +14,11 @@ environment. **What this does not test.** That a real store connects, or that the credentials work. -That needs the production Appwrite project, which þing-02 D2 forbids testing against and -which is the only project that exists (A3(h), answered 2026-08-05). What is testable +That needs the production Appwrite project, which is the only one that exists (A3(h), +answered 2026-08-05) and which **þing-01 D2** forbids *integration* tests against — a +prohibition that is conditional ("until the operator creates one") and that explicitly +**permits read-only preflight validation**. Register C-95: this was cited to þing-02 D2 and +stated unconditionally, which is how a permission was read as a prohibition for weeks. What is testable offline is the part that was previously untestable at any price: the refusals, their ordering, and the fact that the environment contract is checked *before* anything is constructed. diff --git a/tests/test_wire_shard.py b/tests/test_wire_shard.py index 99c8410..026b997 100644 --- a/tests/test_wire_shard.py +++ b/tests/test_wire_shard.py @@ -1,6 +1,7 @@ """Hop-B shard writer (wire/shard.py) — byte parity with the fixture arrow shard. -The byte tests assert the pinned toolchain (fixture README: pyarrow 23.0.1) and +The byte tests assert the pinned toolchain (fixture README: pyarrow 16.1.0, matching +`_PINNED_PYARROW` below and the `>=16.1.0,<17.0.0` constraint) and FAIL LOUD on drift — never skip-silent: a quiet skip would read as conformance. """