From 745b41d8c646151a7aa5026cd4fe46f02839e2bf Mon Sep 17 00:00:00 2001 From: Polichinl Date: Sun, 2 Aug 2026 12:50:11 +0200 Subject: [PATCH] =?UTF-8?q?docs(adr):=20S9=20#190=20=E2=80=94=20epic=20#18?= =?UTF-8?q?1=20closeout,=20and=20ADR-014=20for=20the=20rules=20no=20test?= =?UTF-8?q?=20carries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten register entries closed, one cluster closed, open count 24 -> 15. Every remaining entry is classified: six blocked on the pipeline-core 3.0.0 publish, five owned by another repo, four deliberately deferred. None unexamined. ADR-014 records what the epic learned. The argument for writing nothing was genuinely strong for most of it: a contributor who violates the register's closing-condition convention already gets a failure naming the exact strings and the fix, at the moment of violation, which teaches better than an ADR nobody opens. What that argument leaves unrecorded is the two clauses no test carries — a deferral needs a trigger and an owner, and a change that names a record disposes of that record in the same change. An ADR for the mechanisable rules would be ceremony; one for the un-mechanisable ones is the only vehicle there is. §5 says explicitly that no test enforces it and none is proposed, and records the attempt so the next person does not spend the same afternoon on it. The evidence that it lapses is the epic's own: C-71 fixed in the morning and filed open all day while eight stories shipped; the CIC lagging three consecutive stories, caught by review each time and by nothing mechanical. The closeout states what was NOT done, at length, because a closeout reporting only successes is the defect this epic exists to fix: Cluster M untouched and correctly so; the CI question decided in writing but unowned; withdrawal of a bad delivery chosen but deliberately unbuilt pending FAO's answer; two questions with the partner; the deploy gate's xfail tuning left alone on purpose; and a local Python below the declared floor that skips three checks. The sharpest thing the epic produced is smaller than any of its stories: existence is not reachability. Co-Authored-By: Claude Opus 5 (1M context) --- ...4_claims_and_the_guards_that_carry_them.md | 154 ++++++++++++++++++ docs/ADRs/README.md | 7 + reports/technical_risk_register.md | 34 ++++ 3 files changed, 195 insertions(+) create mode 100644 docs/ADRs/014_claims_and_the_guards_that_carry_them.md diff --git a/docs/ADRs/014_claims_and_the_guards_that_carry_them.md b/docs/ADRs/014_claims_and_the_guards_that_carry_them.md new file mode 100644 index 0000000..13292df --- /dev/null +++ b/docs/ADRs/014_claims_and_the_guards_that_carry_them.md @@ -0,0 +1,154 @@ +# ADR-014: Claims, and the guards that carry them + +**Status:** Accepted +**Date:** 2026-08-02 +**Deciders:** Project maintainers (PRIO MD&D Team) +**Arises from:** epic [#181](https://github.com/views-platform/views-postprocessing/issues/181) +**Related:** [ADR-003](003_authority_of_declarations_over_inference.md) (declarations over +inference), [ADR-005](005_testing_as_mandatory_critical_infrastructure.md) (testing as +infrastructure), [ADR-010](010_technical_risk_register.md) (the register) + +--- + +## Context + +Over two days in August 2026, this repository found **seven** places where it said one +thing and did another. None was a bug in the delivery. All were claims that had outlived +what they described: + +- three register entries filed as open with their own stated closing conditions met + (C-43, C-59, C-61) — one of them saying, in its own body, *"closes when + `tests/test_gaul_lookup_fidelity.py` is committed and green"*, with the file committed + and green; +- two issues closed with their work partly undone (#154, #158) — #158's rename also + broke a URL by find-and-replacing a filename inside it; +- a test file asserting *"the logic tested matches `unfao.py:_validate()` exactly"* about + a method it had stopped resembling seven weeks earlier, while running 43 cases against + a function it defined itself; +- a security-adjacent guard scanning **6 files where it declared 17**, because four of + its five declared roots had been moved and `rglob` on a vanished directory returns + silence rather than an error. + +The pattern is not carelessness. Each was written accurately and became false when +something else moved. What they share is that **nothing was attached to them that would +notice**. + +ADR-003 already forbids inferring what should be declared. This ADR is that rule applied +one level up: **a declaration that nothing validates is an inference with better +grammar.** + +--- + +## Decision + +### §1 A guarantee is attached to a check, or it is not a guarantee + +When this repository asserts something a reader would rely on — in code, in a docstring, +in an ADR, in the register — the assertion is accompanied by something that fails when it +stops being true. If no such check is possible, §4 applies. + +This extends to the repository's own governance artefacts. The register is checked by +`tests/test_register_integrity.py`; the living docs by `tests/test_doc_accuracy.py`; the +package boundary by `tests/test_clone_readiness.py`. **A governance artefact is not exempt +from the rule it exists to enforce.** + +### §2 A guard is mutation-proven, or it is decoration + +A new guard is demonstrated to **fail on the defect it was written for**, and that +demonstration is recorded in the pull request. A guard that has never been watched fail is +a guard whose shape nobody knows. + +Two failure modes, both observed here, and both must be considered: + +- **too narrow** — the þing-01 redaction guard passed while covering a third of its + declared surface; +- **too broad** — a first draft of the coordinate-copy check flagged a *function name* in + this repository as a leaked value. + +Where a guard's inputs are declared (a path list, a module list, a set of names), **assert +that the inputs are real**. `rglob` on a nonexistent directory yields nothing rather than +raising; a list of paths that no longer exist does not fail a scan, it empties it. + +### §3 Prefer a false negative to a false alarm + +A guard that cries wolf gets deleted, and then the rule it carried is unguarded — which is +strictly worse than the narrow guard that would have caught most cases. When a guard fires +on something legitimate, **the first question is whether the matching is wrong, not +whether the scope is too wide.** In the one instance where scope was narrowed instead, the +result missed the shape the defect would actually take. + +Corollary, learned expensively: **existence is not reachability.** A cross-repo pin was +verified to exist, its files were verified to exist at it, and every check written at the +time passed — but it sat on an unmerged branch, declared a version never ratified, and was +withdrawn. Where a claim is about what another repository *says*, check reachability from +that repository's `main`. + +### §4 A deferral is attached to a trigger and an owner, or it is not a deferral + +Deliberate duplication, an unbuilt fix, an unanswered question: each is recorded with the +**named event** that should reopen it. Not "later", not "when convenient" — a thing that +will observably happen. + +Worked examples now in force: the two entry-validation modules stay duplicated until *a +third one is written*; sidecar version-stamping waits for *the next ADR-013 version bump*; +scheduled cross-repo checks wait until *an upstream change reaches the partner through +this repo without anyone noticing first*. + +The counter-example is why this clause exists. Epic #148's retired code path was kept +"until run 0 proves the contract path live" — a real condition, with no owner and no +trigger. Run 0 happened on 2026-07-27 and nobody opened the box; the cleanup took a +subsequent epic. + +### §5 A change that names a record disposes of that record in the same change + +If a pull request cites a register entry, an issue, a CIC or an ADR as the thing it +addresses, it updates that artefact **in the same pull request**. Not in a follow-up, not +at closeout, not from memory. + +**No test enforces this and none is proposed.** It was attempted: no expression reliably +separates *"this entry describes work that is done"* from prose, and a guard that guesses +is one that gets deleted (§3). This clause is therefore a habit, and it is written down +precisely because it is the one rule here that cannot be mechanised — which makes it the +one most likely to lapse. + +The evidence that it does lapse is this epic's own record. C-71 was fixed on the morning of +2026-08-02 and sat filed open for the rest of the day while eight further stories shipped. +The CIC lagged the change in **three consecutive stories** and was corrected by review each +time, never by anything mechanical. + +--- + +## Consequences + +**Cost.** Every guard now carries a mutation demonstration, which is roughly a third again +of the work of writing it. Every deferral needs an argued trigger, which is harder than +writing "later" and is meant to be: if no trigger can be named, the deferral is probably a +decision being avoided. + +**What this does not license.** Not every statement needs a test. §1 is about assertions a +reader would *rely on* — a contract, a boundary, a count, a closing condition. Prose that +explains reasoning is not a claim in this sense, and three times during this epic a guard +fired on the very sentence describing it. The fix each time was to name the thing without +spelling it, **never to weaken the guard** — three weakenings would have left nothing. + +**Where it will be violated first.** §5, because it is the only clause a contributor can +break while every test passes. + +--- + +## Alternatives considered + +**Write nothing; the guards teach themselves.** Genuinely tempting for §1–§3: a +contributor who violates the register's closing-condition convention gets a failure naming +the exact strings and the fix, at the moment of violation — better teaching than an ADR +nobody opens. Rejected because it leaves §4 and §5 unrecorded, and those are the two +clauses no test carries. An ADR that exists for the mechanisable rules would be ceremony; +one that exists for the *un*-mechanisable ones is the only vehicle available. + +**A checklist in the PR template instead.** Cheaper and closer to the moment of work. +Rejected as a replacement, not as a supplement: a checklist records what to do and not why, +and the reasoning is what survives a contributor deciding the rule does not apply to them. +Worth adding later, pointing here. + +**Widen `test_register_integrity` to catch §5.** Attempted and abandoned — see §5. Recorded +so the next person does not spend the same afternoon on it. diff --git a/docs/ADRs/README.md b/docs/ADRs/README.md index 77f4045..9e8ae78 100644 --- a/docs/ADRs/README.md +++ b/docs/ADRs/README.md @@ -72,6 +72,13 @@ These ADRs form the architectural constitution of the repository. sidecar, and the no-collapse boundary (delivery/draws.py). Supersedes the phantom "platform ADR-046". Adopted 2026-07-15 via views-models#149. +- **ADR-014** — Claims, and the Guards That Carry Them + ADR-003 applied to the repository's own statements: a guarantee is attached to a check, + a guard is mutation-proven or it is decoration, a false negative beats a false alarm, a + deferral names a trigger and an owner, and a change that names a record disposes of that + record in the same change. Arises from epic #181, which found seven places where this + repo said one thing and did another. + ADRs numbered 010 and above define: - Domain-specific decisions diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index 8c60d48..7230a3f 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -90,6 +90,40 @@ covered a single open entry (see Historical clusters below). **The lesson worth carrying, and it is D-11's:** WET-before-DRY was applied *correctly* — the pandas and frame seams ran as deliberate siblings through the migration, and a premature abstraction would have outlived the implementation it existed to unify. What went wrong was not the duplication; it was that the removal condition (*"until run 0 proves the contract path live"*) was written down without a **named trigger to act on**, so the box expired on 2026-07-27 and nobody opened it. D-11 predicted exactly this. **A deferral needs an owner and a trigger, not just a reason.** +### Epic #181 closeout — "every claim checkable" (2026-08-02) + +**Eleven stories, ten register entries closed, one cluster closed, one ADR written.** Recorded here rather than only in the issue tracker, because an epic that ends in a closed issue ends nowhere a future reader looks. + +| closed | by | proven by | +|---|---|---| +| **C-71** ADR-008 in the entry validator | S1 | a check parametrised over **both** validators, so the pair cannot drift again | +| **C-43, C-59, C-61** Cluster K's build-time guarantees | S2 | `tests/test_gaul_lookup_fidelity.py`, 26 tests | +| **C-03** the `_validate` replica | S4 | 43 self-referential tests replaced by 14 against the real gate | +| **C-60** the lookup's declared version | S5 | the artifact rebuilt, values byte-identical, stamp unchanged | +| **C-57** coordinate-registry drift | S6 | four checks, each mutation-proven | +| **C-46** the machine-specific path | S7 | `grep -rn "/home/"` over the repo → 0 | +| **C-74** the þing-01 redaction guard | S10 | scan coverage 6 → 17 files | +| **C-22** the correction procedure | S8 | written for the delivery that exists; partner-facing step decided 2026-08-02 | + +Plus, outside the register: #158's rename finished and the **broken URL it created** in ADR-013 §7d repaired (Erratum E2); #154's undone half completed across four living documents; #15 superseded; a cross-repo pin re-taken after #196 showed it sat on an unmerged branch. + +**Open count 24 → 15.** Every remaining entry is classified: **six** blocked on the views-pipeline-core 3.0.0 publish (Cluster M), **five** owned by another repo (C-13, C-24, C-26, C-27, C-28), **four** deliberately deferred (C-15, C-30, C-33, C-40). None is unexamined. + +**What the epic did NOT do, stated because a closeout that reports only successes is the defect this epic exists to fix:** + +1. **Cluster M is untouched and correctly so.** Six entries, two of them Tier 2, all resolving on one upstream publish. No engineering here moves them. +2. **The CI question is decided in writing but not implemented.** Three gated cross-repo checks run nowhere automatic. C-46's residual carries the argued recommendation — *do not couple per-PR CI to another repo's default branch; if wanted, a weekly scheduled check that opens an issue on divergence* — with a named trigger. **It is a decision awaiting an owner, not a task awaiting effort.** +3. **Withdrawal of a bad delivery is the chosen policy and is not built.** Supersession is in force because it is what the wire does. Deliberately not started: it needs an ADR-013 amendment plus views-faoapi work, and FAO's answer on audit requirements (Pre-Release Note 07, B.2) decides whether it is wanted at all. +4. **Two questions are with the UN FAO**, not with us — recipients and notification timing (B.1), withdrawal versus supersession (B.2). +5. **`test_datafactory_deploy_readiness`'s `xfail` tuning was left alone**, deliberately: S7 fixed how the checkout is found, not what the gate asserts. If it needs re-pinning now that datafactory has moved past `v1.4.0`, that is a separate judgement. +6. **The local Python is 3.10 while `pyproject` declares `>=3.11`**, so three checks skip on the maintainer's machine and run in CI. Not a repo defect; recorded because "a gate that does not run" is the shape C-46 was open for. + +**The lessons are in [ADR-014](../docs/ADRs/014_claims_and_the_guards_that_carry_them.md)** — including §5, the one rule no test can carry, which is written down *because* it cannot be mechanised. The attempt to mechanise it is recorded there too, so the next person does not repeat it. + +**The sharpest thing the epic produced** is smaller than any of its stories: **existence is not reachability.** A cross-repo pin existed, its files existed at it, every check written at the time passed — and it had never reached `main`. + +--- + ### Cluster M: Six open concerns, one upstream publish **Root cause:** this repo pins `views-pipeline-core >=2.1.3,<3.0.0`, which resolves 2.3.0 from PyPI. Every fix and every removal below exists **only** on pipeline-core's unreleased 3.0.0. None is engineering work here; all five arrive together with one pin bump, and none can be taken before that bump. **Entries:** **C-44** (the bump itself, deliberately held), **C-62** (the transitive drag — 3.4 GB venv, 31 of 32 Dependabot alerts), **C-72** (the pyarrow CVE whose fix our ceiling excludes), **C-73** (the Tier-2 stale-run selection defect, fixed upstream in their #341), **C-58** (the Tier-2 auto-provision-instead-of-raise, fixed upstream in their #322/#331/#332), **C-07** (the undeclared `appwrite` dependency, whose transitive path their #345 withdraws).