diff --git a/docs/operations/correction_procedure.md b/docs/operations/correction_procedure.md index 2161402..90fa179 100644 --- a/docs/operations/correction_procedure.md +++ b/docs/operations/correction_procedure.md @@ -80,9 +80,21 @@ defect; fix the producer and rebuild. ## 3. Correct on the wire -**The contract has no retraction primitive. It has supersession**, and that is -deliberate: the manifest-last commit ordering means the way to replace a run is to -publish a *new complete run*, not to mutate an old one. +**The intended policy is WITHDRAWAL. What is implemented is SUPERSESSION.** That gap is +deliberate, recorded, and you need to know about it before you act. + +- **Intended** (operator decision, 2026-08-02): a delivery found to be materially wrong + should be *removed or blocked* so it can no longer be served. Data known to be wrong + should not stay retrievable through the production interface. +- **In force**: supersession. The manifest-last commit ordering means a run is replaced + by publishing a *new complete run*; the old one stays in the bucket and remains + retrievable by a client that asks for it specifically. + +Supersession is in force **because it is what the wire does**, not because it was chosen. +Withdrawal has no mechanism today: it needs an ADR-013 amendment and matching work in +views-faoapi. Put to FAO as **Pre-Release Note 07, Decision Point B.2**, which also asks +whether they have an audit or reproducibility requirement that would argue *against* +withdrawal — that answer decides whether the amendment is worth building. 1. Fix the cause — the lookup, the producer, or the code — and land it. 2. Rebuild any affected artifact. For the lookup: @@ -112,31 +124,37 @@ because it is *correct*. Two consequences: --- -## 4. Telling the UN FAO — **not decided; the operator must answer this** +## 4. Telling the UN FAO -Everything above can be executed by whoever is on the keyboard. This cannot, and it is -the step that matters most to the partner. +**Simon Polichinel von der Maase is responsible for making contact.** Not an automated +alert — a correction needs judgement about scope and impact that an automated message +cannot carry. -**Two questions, in plain language:** +**Recipients and channel are recorded outside this repository.** This repo is **public**, +and publishing named FAO staff members' email addresses in it is not something to do by +default. The contacts are in the FAO-02 project materials +(`brain/2_projects/fao02/…/prerelease_notes/fao_02_pre_release_note_07/`, Decision +Point B.1) and in the operator's address book. -> **1. When a delivery is found to be wrong, who contacts the UN FAO, through what -> channel, and how quickly?** -> Right now nobody has said. There is no named person, no address, and no expectation -> about timing — so in practice the answer would be improvised by whoever noticed, -> under time pressure, which is the worst moment to invent a process. +**Send as soon as the scope of the error is established** — not after a correction has +been prepared. The partner's ability to act is time-sensitive and independent of our +remediation timeline. -> **2. Does the UN FAO expect us to *retract* the bad delivery, or to *supersede* it?** -> These need different behaviour. Supersession is what the contract does today: the old -> run stays in the bucket and a newer one wins. Retraction would mean removing or -> marking the old run so it cannot be served — which the wire has no mechanism for, and -> which would need an ADR-013 amendment and agreement from views-faoapi. -> **This is a question for them, not a decision for us.** +### Still awaiting FAO's answer -Per `CLAUDE.md`, anything touching an external party is the operator's call. Until both -are answered, treat this step as: **stop, and ask Simon.** Do not contact the partner -ad hoc; an inconsistent first message is harder to correct than a slow one. +Two things are proposed but not confirmed, and both are put to FAO in **Pre-Release +Note 07, Topic B**: ---- +1. **Are those the right recipients, and is there an expected notification period?** A + named individual on leave is a single point of failure in exactly the situation where + delay is costly. FAO may prefer a shared address or a rota. +2. **Does FAO expect a correction notice for *every* error, or only one material to + published outputs?** A wrong admin label on a handful of cells and a wrong forecast + across a region are different events; treating them alike either floods them with + immaterial notices or buries a serious one. + +Until FAO answers, follow what is written above. Do not improvise a different channel — +an inconsistent first message to a partner is harder to correct than a slow one. ## 5. Preserve the evidence diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index c11f869..8c60d48 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -580,6 +580,16 @@ See also C-40 (the inheritance/representation coupling this migration unwinds), Pinned by three checks in `tests/test_doc_accuracy.py`: the document exists and names the identification fields; it describes ADR-013 mechanisms and none of the deleted ones; and it still flags its undecided step. +**⚠ UPDATE 2026-08-02, later the same day — the operator answered the PRIO half; the FAO half is now formally asked.** + +- **Who notifies:** **Simon Polichinel von der Maase**, by direct email, as soon as the scope of the error is established. Adopted. +- **Treatment of an affected delivery:** the intended policy is **withdrawal**. What is *implemented* is **supersession**, and the procedure now says so explicitly — supersession is in force because it is what the wire does, not because it was chosen. Withdrawal needs an ADR-013 amendment plus views-faoapi work, and whether that is worth building depends on FAO's answer about audit requirements. +- **Put to FAO** as **Pre-Release Note 07, Topic B** (Decision Points B.1 and B.2), which also records the interim defaults in force as placeholders rather than policy. + +Recipients are deliberately **not in this repository**, which is public; naming a responsible person on our side is one thing, publishing an external organisation's individual email addresses is another. `tests/test_doc_accuracy.py` now refuses partner address strings anywhere in the repo, mutation-proven — and fired on the first draft of this very sentence, which spelled the pattern out. Third time in this epic that a guard has caught the prose explaining it (after S3's retired contract name and S5's ledger-schema docstring); the fix is the same each time — name the thing without spelling it. + +The original residual, kept for the record: + **⚠ RESIDUAL — one step is written but NOT decided, and it is the step that reaches the partner.** Two questions belong to the operator (`CLAUDE.md`: anything touching an external party): 1. **Who contacts the UN FAO when a delivery is found wrong, through what channel, and how fast?** No named person, no address, no timing expectation. In practice it would be improvised by whoever noticed, under time pressure. diff --git a/tests/test_doc_accuracy.py b/tests/test_doc_accuracy.py index 6a81cee..896ba0d 100644 --- a/tests/test_doc_accuracy.py +++ b/tests/test_doc_accuracy.py @@ -343,16 +343,70 @@ def test_the_correction_procedure_describes_the_delivery_that_exists(): ) -def test_the_procedure_states_the_questions_only_the_operator_can_answer(): - """The two external-party decisions must stay visible, not quietly become defaults. +def test_the_procedure_names_who_notifies_and_still_flags_what_fao_has_not_answered(): + """Half of §4 is decided; half is not, and both halves must stay visible. + + **Decided 2026-08-02 (operator):** Simon Polichinel von der Maase makes contact, and + the intended treatment of a bad delivery is *withdrawal*. **Not decided:** whether + FAO agrees the recipients and timing (B.1), and whether they have an audit + requirement arguing against withdrawal (B.2). Both are put to them in Pre-Release + Note 07, Topic B. + + An earlier version of this test asserted the document said "not decided", and its + docstring instructed whoever removed that marker to replace it with the decision. + That is what happened — the guard fired on the operator's answer, which is the + behaviour it was written for rather than a false positive. + + The failure mode it now guards is subtler: a half-answered question quietly becoming + a whole answer, so the outstanding half stops being asked. + """ + text = _CORRECTION.read_text() + assert "Simon Polichinel von der Maase is responsible" in text, ( + "the procedure no longer names who contacts the partner — an unowned step is " + "improvised by whoever notices, under time pressure" + ) + assert "Still awaiting FAO's answer" in text, ( + "the procedure no longer flags what FAO has not answered. If they have " + "answered, record the answer and the date — do not simply drop the question." + ) + assert "Pre-Release Note 07" in text, ( + "the procedure must cite where the outstanding questions were put, or they " + "become questions nobody remembers asking" + ) + + +def test_the_procedure_distinguishes_intended_policy_from_what_is_implemented(): + """The gap that would otherwise be discovered mid-incident. - Who contacts the UN FAO, and whether they expect retraction or supersession. Per - CLAUDE.md both are the operator's; the failure mode is that an undecided step gets - silently improvised the first time it is needed, under time pressure. + Withdrawal is the decision; supersession is what the wire actually does, and it is + in force only because nothing else exists. An operator reading this at 22:00 must + not believe a bad delivery becomes unretrievable when it does not. """ text = _CORRECTION.read_text() - assert "not decided" in text.lower(), ( - "the procedure no longer flags its undecided step — if it has been decided, " - "replace the marker with the decision and say who made it" + assert "intended policy is WITHDRAWAL" in text.replace("**", "") + assert "implemented is SUPERSESSION" in text.replace("**", "") + assert "ADR-013 amendment" in text, ( + "the procedure must say what withdrawal would COST — otherwise the gap reads " + "as an oversight rather than as unbuilt work with a known price" + ) + + +def test_no_partner_contact_details_are_published_in_this_repository(): + """This repo is public. FAO staff email addresses do not belong in it. + + The contacts live in the FAO-02 project materials and the operator's address book. + Naming a responsible person on our side is fine; publishing an external + organisation's individual addresses to a public repository is not something to do + as a side effect of documenting a runbook. + """ + offenders = [] + for doc in (*sorted(_REPO.rglob("*.md")), *sorted(_PKG.rglob("*.py"))): + if ".git" in doc.parts: + continue + for number, line in enumerate(doc.read_text(errors="ignore").splitlines(), 1): + if "@fao.org" in line.lower(): + offenders.append(f"{doc.relative_to(_REPO)}:{number}") + assert not offenders, ( + f"partner contact addresses appear in this public repository: {offenders}. " + "Keep them in the project materials; reference the decision, not the address." ) - assert "UN FAO" in text and "supersede" in text