Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions docs/ADRs/014_claims_and_the_guards_that_carry_them.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions docs/ADRs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions reports/technical_risk_register.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading