Sync development → main: the sibling-CI arc, three ADRs, and a drift check that matches on the right thing - #240
Conversation
…on, not a directory Both partners' historical upload passed `name=self._model_path.model_name`. That value comes from the views-models postprocessor DIRECTORY name — a fact in another repository's filesystem. The forecast leg passes the declared `product.CONSUMER_DOCUMENT_NAME`. The two legs upload separately and the consumer selects them separately: forecasts by the newest manifest, historical actuals by category="historical". Both filter on the document name. So if the legs disagree the delivery half-arrives, and the failure mode is an empty endpoint rather than an error -- ADR-013 s4.1a's exact shape, the one that left six orange_ensemble documents stranded in unfao_bucket for months. Delivery-neutral, verified before changing anything: CONSUMER_DOCUMENT_NAME is "un_fao" and the views-models directory is un_fao, so model_name resolved to the same string. No delivered byte changes. What changes is that the agreement is a declaration rather than a coincidence. Fixed now rather than when it broke, because it was about to be sprung. views-models#333 creates CRAF'd's launcher directory this week, and whoever named it would have decided -- without knowing it -- whether CRAF'd's historical artifact was retrievable. The constraint was posted on that issue on 2026-08-04; this removes the need for anyone to honour it. Guard: test_both_delivery_legs_name_the_document_from_the_declaration asserts exactly one forecast leg and one historical leg per partner. Mutation-proven three ways -- revert one leg, add a third, stop declaring on the forecast leg. Its first draft counted with a plain substring and reported three legs where there are two, because `consumer_name=product...` contains `name=product...`; the lookbehind is deliberate. 372 passed / 40 xfailed / 0 failed. ruff clean. Register 83/16/67. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er-name fix(delivery): C-77 — name the historical document from the declaration, not a directory
… client The þing-02 ratification attached a condition to the #146 deferral: record it as an ADR in this repo. That was never done. The reasoning lived only in a GitHub issue and in a deliberation folder outside the repository, which is what the condition existed to prevent -- an issue is a work item, not a place someone checks before deciding whether a rule applies to them. What the ADR records - Why the import stays: a hand-written client here would be the platform's THIRD copy, and the defect that prompted the assembly was common to the two existing copies, not a divergence between them. Upstream declines to export a client surface, deliberately, because exporting the current one would bless a shape its own maintainers have recorded as needing decomposition. There is nothing to unwind to. - What bounds it: an importer allowlist pinned by test, the _ContractStorePort DIP port (four methods, so the wire never sees Appwrite types), and the framework-contract test added with the 3.0.0 bump. Containment, not removal -- C-40 stays open on the residual. - The trigger in plain words: demand (a second incident rooted in duplicated client code) and supply (upstream carving out the auth/config seam). Either means revisit; both mean do it. Explicitly not "this seat's convenience". Two things found while writing it, neither previously recorded here - The supply half has PARTIALLY moved. The verdict was written against a single 3,064-line module; 3.0.0 relocated provisioning and transport out of it and added an audit package, leaving 2,841 lines. Still no exported client, so the trigger has not fired -- but "no movement" would be the wrong thing to believe. - The sibling obligation from the same verdict is DISCHARGED. It required this repo's legacy delivery path to be guarded or retired before 2026-11-30. It was retired in #149; C-63 is resolved; the deadline is moot here. Nobody had recorded that it was met. Also: `C-221` in the verdict is pipeline-core's REGISTER entry, not issue #221 (which is unrelated and closed). Noted in the appendix because this repo namespaces cross-repo identifiers for exactly that reason and the verdict's shorthand does not. Scope note: the deferral was written when one manager held the import; crafd added a second. Two adapters with identical contents now. 372 passed / 40 xfailed / 0 failed. ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e-import-deferral docs(adr): ADR-015 — why this repo imports another project's Appwrite client
…empty one
The lookup builder was the last runtime pandas import in the repository. The sources
are parquet and the output is parquet; pandas was only ever the thing in the middle.
Proven against the real producer, not asserted
Rebuilt land_gaul from the views-datafactory checkout and compared to the committed
artifact:
rows 64,742 -> 64,742
column order identical
schema types identical (including dictionary<string> on the four names)
ALL 10 COLUMNS VALUE-IDENTICAL
adr / region / n_cells / n_dropped_incomplete / lookup_version / source_provenance
byte-identical
That is #90's re-baselined acceptance criterion met exactly: contents identical, and
the flat declared `lookup_version` key reproduced byte-for-byte (C-60).
One deliberate difference: the written file no longer carries a `pandas` schema-metadata
blob. It described an index arrow does not have, the only consumer reads the file as
arrow (`contract.gaul_lookup.load`), and reproducing it would have meant keeping
knowledge of pandas' metadata format in the one script this change exists to remove
pandas from. Stated in the docstring rather than left to be discovered.
The committed artifact is NOT regenerated here. The builder is what #90 asks for;
rewriting a delivered artifact is a separate act with no delivery benefit.
C-76 closed, and the guard failed its own first test
`build()` now refuses a zero-row result instead of writing one and printing `cells=0`
as though that were an outcome. The first draft did not survive its mutation test: on
an empty table `pa.array([True] * 0)` infers NULL type, so `pc.and_` raised
ArrowNotImplementedError in the completeness filter BEFORE the zero-row check could
speak -- the confusing-late-failure C-76 exists to prevent, relocated by one function.
The mask is now explicitly typed.
C-75 decided: RETIRE, and not in this PR
The entry's Owner field required whoever took #90 to make the keep-or-retire call on
GaulLookupEnricher "not to defer a third time". Retire: zero production callers, its
"build/verification path" justification is spent now that this path is arrow-native, it
holds the package's last pandas reference, and its _gather duplicates the shipping
gather in contract/historical.py -- which four test files cover independently. C-45 is
the precedent and was resolved by deleting.
Not executed here: the retirement touches ten files including a CIC, ADR-012 and four
test files. Mixing that with a builder rewrite is what epic #148's S5 explicitly refused
to do. It is the next change.
Epic #85 and tracking #93 stay OPEN until it lands -- their claim only becomes true when
that module is gone.
373 passed / 40 xfailed / 0 failed. ruff clean. Register 83/15/68.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…okup-build feat(build): #90 — build the GAUL lookup arrow-native, and refuse an empty one
…ecomes literal The decision was recorded in #90 as its Owner field required. This executes it. What went - views_postprocessing/contract/enrichment.py (317 lines) - tests/test_enrichment.py (39 tests) - docs/CICs/GaulLookupEnricher.md Why: zero production callers, its "build/verification path" justification spent once #90 made that path arrow-native, and its _gather duplicated contract/historical.py's shipping gather -- which four test files cover independently. C-45 is the precedent and was resolved the same way. No coverage of shipping code was lost The two tests elsewhere that imported the class both asserted only that its lookup_version agreed with gaul_lookup.version() -- two readers of one fact, checked against each other. They now read it through the declared reader the delivery uses, which is the half that was ever load-bearing. One guard deleted rather than kept: test_gaul_lookup_access asserted "GaulLookupEnricher" not in the manager source. With the class gone that cannot fail, and a test that cannot fail is decoration (ADR-014 s2). What it protected -- one lookup read per delivery -- is the first assertion in the same function and still bites. Fifteen files, and the sweep is the point The module, its tests, its CIC, the CIC index, two importing test files, the machinery list, the pandas-importer assertion, README's dependency table and package tree, role_and_seams' tree and contract list, ADR-012's ontology row and pandas claim, and three module docstrings. Every one was a live claim about a class that no longer exists, and NONE of the ADR or CIC references would have been caught by any guard -- which is C-80's whole argument. Two stale claims fixed while in the neighbourhood, both actively false rather than merely dated: frame_extraction.py described itself as the sibling of extraction.py (a module deleted in #151) and said "the pandas readers stay for the still-pandas path"; gaul_lookup.py described the enricher in the present tense. What it makes true `grep -rn "^import pandas\|^from pandas" views_postprocessing/ scripts/` returns nothing -- not a runtime import, not a type-only one. Epic #85's claim is literal now rather than nearly-true, and test_doc_accuracy's assertion changed from "exactly one type-only importer" to "none". 333 passed / 40 xfailed / 0 failed (-40: the enricher's 39, plus one retargeted). ruff clean. Register 83/14/69. Closes #85. Closes #93. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lookup-enricher refactor: C-75 — retire GaulLookupEnricher; epic #85's pandas claim becomes literal
C-79: the store port failed OPEN, and had zero tests The comment beside `_ContractStorePort.upload`'s result check calls it "the whole mechanism". It is: pipeline-core's store, on a metadata failure AFTER the file is uploaded, logs and RETURNS success=False rather than raising, so a caller that discards the result ships a file with no metadata document -- invisible to the consumer. That happened to run-0's historical artifact on 2026-07-27. The check was `if success is False`. A result that was None, or lacked the attribute, or carried a non-bool, sailed through as though the upload had worked. Now `is not True`. The dead to_dict() fallback went with it: an unrecognised result should be refused and named, not adapted to silently. The message reports what it actually received, because success=None (a moved contract) and success=False (a reported failure) send an operator to different places. tests/test_store_port.py -- 16 tests over both partners, where there were none. The standing excuse for source-scanning manager facts is that managers need Appwrite env and a views-models path manager; the port needs neither, so it never applied here. Its trigger fired on 2026-08-03 and nobody noticed. The entry read "the 3.0.0 bump is the next occasion"; the bump landed, C-44 closed on a wheel-level suite verification, and the return contract was never re-read. test_register_integrity cannot catch that -- its checks are structural and none asks whether a named external event has occurred. C-83: a failed import reported as a wrong declaration get_queryset() returns None for ANY exception importing config_queryset.py; declared_data_format(None) defaults to 'dataframe'; the format guard then tells the operator to set data_format: 'feature_frame' in a file that already says exactly that. launch_config.assert_queryset_was_importable now runs FIRST, and both managers read the queryset once and reuse it. The refusal says what happened and steers away from the config file -- "This is NOT a declaration problem: do not edit data_format until the module imports" -- toward the traceback pipeline-core logged. Logs before it raises. Three guards, because order IS the fix: the refusal fires and names the real fault; an importable queryset passes (the format question belongs to the next check); and per partner, get_queryset() is called exactly once with importability checked first. Not fixed here, deliberately: upstream still returns None for any import exception. We stopped passing it into a function whose contract is to default. Raising upstream would be better and is not ours; waiting for it would have left the misleading message on the live FAO path meanwhile. Mutation-proven three ways -- revert the polarity (4 fail), delete the importability check (1 fails), reverse the order (1 fails). 352 passed / 40 xfailed / 0 failed. ruff clean. Register 83/12/71. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sals fix(delivery): C-79 and C-83 — two refusals on the live FAO path
ADRs and CICs were exempt from the deleted-symbol scan. That exemption is right in principle -- an ADR legitimately records superseded designs, and a scan that fires on history gets deleted (ADR-014 s3) -- and far too wide in practice: it is how a CIC came to name a collaborator its class had never called, in six places, for weeks. The design was chosen by measurement, not argument - A path-resolution check over ADRs would have fired 29 times. Inspecting them showed most were correct history, other repos' files, or paths inside URLs. Built the measurement first, then did NOT build the check. - The curated deleted-symbol list was the narrower instrument: 22 hits over ADRs, ZERO over CICs. Fourteen of the 22 were in ADR-011 alone. Three of the hits were real: ADR-013 still cited unfao/historical.py, unfao/wire/ and unfao/wire/source_selection.py, all moved to contract/ by #153. Three genuine defects among five markable ones is the ratio that justified turning the scan on. Two escapes, both declared rather than inferred Line-scoped `legacy-ok` for an isolated mention; a new file-level `legacy-ok-file` for a document whose SUBJECT is a retirement. ADR-011 earned it -- it IS the decision to remove the runtime mapper, so its subject appears fourteen times, correctly. A second guard pins the exemption set to that one document, so a third shows up in a diff. A check nothing else could have made test_a_cic_does_not_name_a_collaborator_its_class_never_calls asserts that a class a CIC names is actually referenced by the class it documents -- the GaulLookupEnricher failure, which no path check and no symbol list would have caught at the time, because the class existed and every path resolved. Exception types are excluded: a collaborator is something the class reaches for, an exception something that passes through, and the first draft flagged three the manager legitimately propagates. The mutation campaign found a miss in the PREVIOUS change Reintroducing the exact GaulLookupEnricher sentence did NOT fail. The collaborator check only sees classes that still exist, and C-75 had deleted that one. The real gap was that the deletion never extended the deleted-symbol list -- which that list's own comment demands in as many words: "A deletion PR that does not extend this regex has not finished." Extended here; the reintroduction now fails. Four further historical mentions written yesterday were flagged by the extension and marked. Four guards, all mutation-proven: stale path in an ADR, the file-level escape used on a live document, a CIC with no declared subject, and the reintroduced collaborator. 356 passed / 40 xfailed / 0 failed. ruff clean. Register 83/11/72. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test(docs): C-80 — scan ADRs and CICs; three stale paths found and fixed
…op the rest C-82 said governance prose carries numbers and statuses nothing checks. Its own count had gone stale while it sat open, which is the argument rather than an irony: a number nothing checks is a claim with a half-life. Two guards where a claim names an artifact — the checkable shape: - test_test_files_named_by_live_entries_exist_or_name_their_repo. Scoped to Open Concerns and Disagreements deliberately; eleven such mentions exist register-wide and nine are resolved entries correctly recording what discharged them, so a blanket check would cry wolf and be gone in a day (ADR-014 §3). - test_a_cic_review_date_is_not_older_than_its_own_content. - test_the_docstring_states_the_same_edition_the_constants_declare, added when the v1.4.4 bump below moved the constants and left both docstrings saying v1.4.1. The first draft of the first guard was the bug it was written to catch. Its foreign-repo exemption reused _FOREIGN_PREFIXES — the identifier-namespacing list, which holds ordinary English like `models` and `pipeline-core` — over a 60-char window. A mutation planting a vanished file in a live entry left it green because a sentence three words earlier said "pinned pipeline-core-free". It had found its two real defects by luck of their neighbours. Rebuilt to require the owning repo immediately abutting the path, re-proven against three mutations including that one. Numbers that could not be guarded were removed in favour of the command that produces them (C-33's precedent), not re-counted. Historical counts inside dated closure records are left alone — they are records, not claims about now. Also disposed of: D-11's self-contradiction; the deleted test_enrichment.py cited as live coverage; views-faoapi's golden-fixture path stated as if it were ours. Seam Contract pin 1.4.1/90fc105 -> 1.4.4/fcf32c9, both partners. The detector demanded it; coordinate names re-verified unchanged, no values copied. New C-84 (Tier 2): the operator console read of 2026-08-05 recorded that both platform keys expire 2026-11-17, 3h35m apart. The UN FAO key is what this delivery authenticates with. No degraded mode, no fallback identity, and the date is known in advance. Registered so it is not discovered by an outage; the fix is a console action, not engineering. C-81 updated to say what that same session did not move. Register 83/11/72 -> 84/11/73. Suite 360 passed / 40 xfailed; ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ygiene docs(register): C-82 — guard the prose claims that can be guarded, drop the rest
…in force #133 asked for three declared fields and said the historical artifact is not co-delivered. Measured before implementing, because implementing it as written meant a contract_version bump — which lives inside the header bytes §10 pins — and so a golden-fixture rebuild and a three-repo re-vendor. Two of the three already ship: - `source` is `provenance.ensemble`, and views-faoapi reads exactly that key. - the schema version is `contract_version`, on the run manifest and every shard header. The third is real but is not ours to stamp. Maturity belongs in `provenance`, which §2.1 closes, so admitting it is an amendment. And Hop-B shard headers are the Hop-A headers re-embedded untouched (contract/wire/sink.py) — this repo forwards provenance, does not compose it, and does not know a run's maturity. Synthesising one here would be a producer guessing at a fact nobody told it. The stamp belongs to views-models; our part is to forward it, which needs no code once the key is admitted. ADR-013 §2.2a records that as Amendment A2: the field, why it is an amendment and not a free addition, what adoption costs, who can stamp it, and a named trigger — adopt when the next fixture re-vendor happens for another reason, C-72/#174 being the one expected. Numbered A2 because A1 was taken (§7a, 2026-07-19). The issue's fourth claim is false: `_save_contract` DOES upload the historical artifact, category="historical", under the same interlock as the forecast. faoapi's C-169 decision has no premise, so there is nothing for the maintainer to decide. test_s2_2a_is_not_secretly_in_force guards the amendment bidirectionally — the risk is not only that A2 ships unrecorded but that it ships while §2.2a still says it has not, leaving three repos believing a re-vendor is still owed. Mutation-proven both ways. New C-85 (Tier 3): a cross-repo ask adopted on its stated terms without measuring current state. Filed in good faith, wrong because it named the run manifest while the consumer reads the shard header — vocabulary is what does not survive the trip between repos. No guard proposed; a checklist would be theatre. Register 84/11/73 -> 85/12/73. Suite 361 passed / 40 xfailed; ruff clean. No delivered byte changes; contract_version stays 1.5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…turity-133 docs(adr-013): #133 — Amendment A2 for run maturity, drafted and not in force
C-40's residual read "gated on views-pipeline-core 3.0.0, which is a release signal rather than engineering work". 3.0.0 shipped 2026-08-03 and nothing became possible, because two different gates had been conflated: - the pandas gate (consequence b) was gated on pipeline-core's DataFrame retirement, an epic, not a version — and it lifted on its own on 2026-07-27. - the de-inheritance was never gated on any pipeline-core release. It is gated on views-models: postprocessors/un_fao/main.py:27 constructs our manager directly and the framework's Template Method drives _read/_transform/_validate/_save. The inheritance IS the integration contract, so removing it is a two-repo change. Nobody had written that down, which is how the entry sat parked reading as unblocked. What was actually available is the inbound half of this entry's own prescribed DIP mitigation. The outbound half landed in July as _ContractStorePort; the inbound half never did. _prod_forecasts_datastore, _<partner>_datastore and _<partner>_appwrite_config were methods constructing AppwriteConfig and DatastoreModule inline. They are now module-level functions taking declared arguments. No delivered byte moves. The gain is C-40's consequence (a): store construction, its refusals and their ordering are testable with no manager instance, no views-models path manager and no Appwrite environment — tests/test_store_construction.py, 14 tests, which could not have been written a day ago. First part of the manager seam reachable without the framework. Two things found while in there, both removed: - self.ensemble_path_manager: assigned in __init__ and in the builder, read once four lines later, and nowhere else in either partner package or in views-models. - loa = "pgm" followed by `if not loa: raise` — an unreachable branch guarding a variable never used again, beside the commented-out block that once computed it. Manager class 351 -> 272 lines (16 -> 14 methods); files 440 against the 450 directory budget. New ratchet test_the_manager_class_itself_stays_thin bounds the class at 300 — the existing budget counts the directory and so cannot see a class re-absorbing logic, which is exactly the shape of this refactor. Both bounds stand; neither was relaxed. Four mutations proven: env assert moved after construction, a builder given back its self, a coordinate value pasted in, the class padded past the ratchet. C-40 re-scoped with the corrected gate and trigger; Location rows on C-33 and C-40 updated; the dated ADR-011 assessment banners the two claims this change invalidated. Suite 377 passed / 40 xfailed; ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…uction-off-self refactor(managers): C-40 — store construction off the manager class
…ecks
A few tests here verify claims this repo makes about OTHER repos — chiefly that the
coordinate-registry edition we pinned is still the one that exists. They need the sibling
on disk, so they skipped in CI and ran only on a laptop.
Which siblings CI could fetch was decided in a COMMENT, and the comment said
views-appwrite was private. It went public on 2026-08-08 (views-appwrite@9d80b75, a
deliberate recorded act) and the comment did not, so seven checks stayed dark for two days
for no reason — including both registry-drift detectors, which had fired twice in one day
the week before. The first draft of ADR-016 proposed issuing a credential to reach them,
written one day after the fact justifying it stopped being true.
So the fact now lives in a declaration that carries the date it was checked:
Sibling(env=..., public=..., public_checked=..., ci_checkout=..., note=...)
`public` is a fact about the world; `ci_checkout` is our decision. Conflating them is what
went wrong. frozen + kw_only deliberately: two adjacent booleans are a one-token slip
between "public, not checked out" and the combination G3 forbids, and a plain dict would
let a missing key read as None and silently exempt a sibling from every rule.
CI now checks out views-appwrite and views-crafdapi, both with `ref: main` — without it
actions/checkout takes the sibling's own default branch, which for views-appwrite is
`development`, while two checks read the working tree and a third demands reachability
from main. Latent today; contradictory the first time a bump lands on development first.
tests/test_ci_sibling_coverage.py: eight rules, each a pure function of
(workflow, siblings) so each runs against a synthetic broken world as well as the real
file. G3 (public => no token) and G5 (no continue-on-error) look like fussiness and are
not: `public` is verified by CI DOING the tokenless checkout, and either key silently
turns it into an unchecked claim. A meta-test refuses a rule added to _RULES with no
mutant proving it bites.
Also fixed, found en route: broken_sibling_overrides treated a variable set to the EMPTY
STRING as unset — which is exactly what a YAML interpolation resolving to nothing
produces. Seven checks would skip on a green build. Path("") .exists() is True, so the
obvious fix makes it worse; the strip is load-bearing.
pyyaml declared and re-locked in the same commit. It already arrived transitively, which
is precisely how this repo lost pytest; a stale lock makes poetry install refuse and takes
the whole suite with it.
test_no_coordinate_value_is_copied_into_this_repo changes character rather than merely
un-skipping: the scan refusing registry VALUES in this public repo's markdown now runs on
every PR rather than only on a maintainer's machine. Its comment said the opposite.
ADR-016 rewritten around the public/private rule; the token guide deleted, since we are
not issuing a token. The credential for views-faoapi — the one private sibling, worth one
test — is deferred with a named trigger, pending a request to FAO to make it public.
C-46's explicit recommendation ("do NOT add sibling checkouts to the per-PR workflow")
is overridden and disposed of in the same change: its stated objection was coupling to a
default branch, which `ref: main` addresses; the residual cost, that an upstream merge to
main can block merges here, is accepted and stated rather than glossed.
Deciders swept across ADRs 000-015: the PRIO MD&D Team no longer exists.
Suite 404 passed / 40 xfailed; ruff clean; lock fresh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI caught this on the first run, which is the point of the change it is part of: the guard could not fail locally, because locally there is only one clock. The dates were stamped from a maintainer's machine in CEST at 01:25 on 2026-08-10. The runner is UTC, where it was 23:25 on 2026-08-09. So a date recorded truthfully today read as tomorrow two hours away, and G8 called it a lie. "Today" is not something a bare ISO date determines — it depends on where the reader is — so comparing one against `date.today()` asks two different questions. A full day of tolerance covers every real offset, and a date more than a day ahead is still exactly what the rule is for: a fact nobody checked. Written down rather than fixed quietly, because the repair a hurried reader reaches for here is deleting the rule (ADR-014 §3). Added test_g8_tolerates_a_timezone_but_not_a_ fiction, pinning both sides of the boundary at 0/+1/+2/+400 days — a tolerance with no test becomes whatever made the last failure go away. Suite green under TZ=UTC too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read off the merge run rather than derived. 398 passed / 9 skipped / 38 xfailed; the 9 are 8 views-datafactory (needs published data, not a credential) and 1 views-faoapi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eclaration feat(ci): declare sibling visibility, and un-dark seven cross-repo checks
An expert-code-review of the design found two fields and two rules justifying each other and nothing else. Measured rather than argued: .public read in exactly one place — rule G3 .public_checked read in exactly one place — rule G8 G3's private-branch vacuous; no private sibling is checked out So `public` existed to be verified by G3, and G3 existed to protect `public`'s verifiability. Deleting both changes no behaviour. `public_checked` was worse than useless: nothing could confirm the check had happened, so it manufactured confidence instead of recording a fact — and G8, its only reader, cried wolf on its first CI run over a timezone, which is ADR-014 §3's own worked example. Visibility now lives in a sibling's `note`, as prose, because nothing here can verify it in any case. What survives without the flag needs no field: a repository CI cannot read fails to download, and G5 keeps that failure loud. guard file 552 -> 448 lines rules 8 -> 6 (G1, G2, G4, G5, G6, G7) Sibling 5 fields -> 3 (env, ci_checkout, note) Every surviving rule has an incident behind it, not a hypothesis: G6 for the 745 lint findings when a sibling landed outside `_siblings/`, G7 for views-appwrite's default branch being `development`. Re-proven against the real workflow after the cut — and the first G7 mutation was wrong rather than the guard, having hit a comment instead of a step. ADR-016 rewritten. The maintainer could not follow the previous version past §4, and the reason was the design rather than the prose: §5-§7 were second-order, rules protecting an argument rather than protecting the code. It now opens with the decision in four sentences, gives each rule the incident behind it, and states plainly in §7 that a broken sibling can block merging here — with the reasoning, which was previously asserted rather than argued. What was removed is recorded, because the removed design is more tempting than it looks and this document once argued for it. Suite 399 passed / 40 xfailed; ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADR-017 decides what this repository does about a fact held in a repository it cannot read. The rule: the fact is declared in the public coordinate registry, each side verifies itself against it, neither reads the other's source. No credential, for any number of private APIs. Written for the category, per the maintainer's brief that private consumer APIs are permanent even if views-faoapi itself goes public. ADR-016 rescoped to say plainly that it only covers repositories CI can download, with the private case handed to ADR-017 rather than deferred. A falsification pass over every section of both documents found seven defects, all fixed: - the heading said 'four sentences' over a body of three, after the rescope removed one - ADR-016 still cited a private register ID in a document meant to stand alone - 'Rejected on §8's arithmetic' pointed at arithmetic the rewrite had removed - 'sibling' was used seventeen times and never defined - ADR-017 makes ADR-016's views-crafdapi download pointless — that fetch serves exactly one test, and ADR-017 replaces it. Neither document said so; both now do - §2's ownership claim is evidenced by ADR-013 for the FAO partner only; CRAF'd is not mentioned there at all. Now admitted, and folded into the registry work - ADR-016 rejects 'copy the facts here' while ADR-017 keeps a copy. Reconciled: what is forbidden is a copy that is ALSO the thing checked against The fix for the fourth broke the first by landing between two sentences; caught on re-run. Suite 399 passed / 40 xfailed; ruff clean. Nothing merged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
New probes, not a re-run of the first pass. Three findings, all fixed: - ADR-016 is Accepted and asserted the outcome of ADR-017, which is Proposed. It said ADR-017 'settles' the private case and that its §7 'replaces' a test. An accepted decision cannot depend on an unaccepted one. Softened to conditional throughout, and the scope line now states plainly that nothing in ADR-016 depends on the proposal being adopted — if it is rejected, the private case returns as an open question. - §5's rule table runs G1, G2, G4, G5, G6, G7. Two numbers are missing and nothing said why. The removed-design section now names G3 and G8 as the removed rules and records that survivors keep their original names rather than being renumbered. - '(see §5.3)' reads as a subsection that does not exist — the ADR's own convention is §N, and §4.1a elsewhere refers to a real ADR-013 subsection. Rewritten as prose. Probes that failed to falsify, recorded because a passed probe is evidence: every factual claim verified (15 of 33 repositories private; the ADR-013 quote is verbatim; the registry does carry consumer fields; CI does read it), and the label/name/CONSUMER_DOCUMENT_NAME terminology is defined on first use and consistent thereafter. All ten findings from both passes re-checked together; no regressions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both ADRs accepted. ADR-017 moves Proposed -> Accepted, and ADR-016's conditional language about it resolves with it: the scope line no longer hedges on whether the proposal is adopted, and §8 now says the decision is accepted but not yet implemented, because it waits on a declaration landing in another repository's registry. /review-diff over the branch found two, both prose, both this repo's named defect class: - a docstring in test_ci_sibling_coverage said "eight rules find nothing to complain about" after the cut left six. Removed the count rather than correcting it — a number nobody checks is a claim with a half-life, which is C-82's whole finding. - ADR-017 §5 stated in the present tense that this repository checks its copy against the registry declaration. It does not: test_product.py still reads the consumer's source and will until the declaration exists. Appendix B and ADR-016 §8 both recorded it as pending, so the documents were honest taken together, but §5 read alone stated unbuilt work as fact. Now says plainly that none of the three steps is in place and that the present tense describes the decided end state. Probes that found nothing, recorded because a passed probe is evidence: no orphaned reference to the deleted `public`, `public_checked`, G3, G8 or the clock-skew constant anywhere in tests, docs or workflows; broken_sibling_overrides verified against unset, empty, whitespace-only, bad-path and valid inputs with the docstring matching each; all six rules carry a mutant and each mutant fires its own rule. Suite 399 passed / 40 xfailed; ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-declaration refactor(tests): drop the circular half of the sibling declaration
Reviews arrived from the views-appwrite seat (#231, #232) and the views-faoapi/crafdapi seat (#233, #234). Both accepted; both found real defects. Two independently found the same one. VERIFIED BEFORE ACTING, since two reviews turned on it: protect_main [active] bypass_actors: NONE branches/main/protection -> 404 Branch not protected So ADR-016 §7's third justification — "the maintainer can merge over a failing check" — was false. A ruleset applies to everyone except the actors it names, and it names none. Withdrawn in place rather than deleted: the section now says there is no escape hatch, that adding a bypass actor is a console change nobody has made, and that such an override would in any case be one person's judgement available only while that person is. The coupling stands on the two reasons that survive. ADR-016: - §7a: the coupling RATE, from views-appwrite's own measurement — five registry editions in four days, four observation-only, each of which would have blocked a release. "The fix is minutes" reads differently at once a quarter than once a day. - §7b: drift tripwires (movement is the signal) versus reachability checks (movement is noise), so a reader knows which coupling they are accepting where. - G7's row is now a dated observation rather than a standing fact about someone else's default branch — a rule that stays right while its explanation rots is this document's own diagnosis. CODE, because one finding was a hole rather than a wording problem: - G4 fired only on ci_checkout=False, so the note recording that the crafdapi fetch is TEMPORARY was enforced by nothing — prose about another repo that nothing can check, which is precisely §3's failure, reintroduced for the one download the ADR calls temporary. G4 now follows the note, not the flag: any note must name a record. - Both fetched siblings gained one. crafdapi's records its temporariness and names ADR-017 §7; appwrite's records that its fetch is load-bearing and that pruning it would silently disable two checks. Mutation-proven both ways. ADR-017: - The transitional window, and the sharpest finding in the set: after the registry row lands and we switch to reading it, but before the consumer-side check exists, the green build proves only that two values this platform authored agree with each other — and the check that consulted real source would by then be deleted. Sequence now constrained: adding the registry check does not remove the source-reading one; that goes only when the consumer-side check lands. - crafd had no third leg filed and its label value is not decided yet. views-crafdapi#39 filed; Appendix B gains it as step 4, records that the CRAF'd registry row is blocked on that partner's data contract, and asks for the FAO half to land independently. - §5 no longer pre-claims views-appwrite's assent to hosting the declaration. - The consumer's check is free because the REGISTRY is public, not because this repository is — the consumer never reads us at all, which is the symmetry the rule is about. - Edition skew stated rather than assumed. - §9 scoped to declarable facts; behavioural agreement needs declared semantics instead, with views-models#327 / D-05 as the live instance. DECLINED: an interim tripwire failing CI if the manual check goes stale. The sequencing fix removes its premise — the source-reading check now survives until the consumer-side one exists — and a dated assertion needing periodic refresh is a guard whose likely first firing is a non-event, which is what G8 did in this very file before being deleted. Suite 399 passed / 40 xfailed; ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…indings docs(adr): address four external reviews of ADR-016 and ADR-017
… leave behind Both were described in the ADRs and tracked nowhere. A decision record states a position; the register is what carries a live risk, and these are live. C-86 — the release path now depends on two other repositories with no way past a red build. ADR-016 accepted that coupling partly on a justification that measurement killed: protect_main lists ZERO bypass actors, so administrator status confers no exemption, and there is no classic branch protection either. The interesting part is that it is LATENT: no status check is required today, so a red build blocks nothing. The moment C-81's enforcement half is fixed — correctly, and it should be — this becomes live and unbypassable in the same change. Two open items that each look sensible alone combine into something neither of them says, so C-81 now says it. Rate is not hypothetical: views-appwrite reports five registry editions in four days, four of them observation-only, each of which would have blocked a release. views-appwrite#76 filed to make the obligation-carrying distinction machine-readable, which is the preferred resolution and which that seat volunteered. C-87 — the delivery label will be verified against a declaration, and nothing will verify the declaration against the consumer's code. ADR-017 §8 states it; the residual belongs here because the second half is work in repositories this project does not control (views-faoapi#379 has an owner, views-crafdapi#39 is blocked on a data contract), and because for a third-party-operated API it may never exist at all. Register 85/12/73 -> 87/14/73. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Blocked on #241. A
Registered as C-89 through C-93. Epic #241, tracking #251, nine stories (#242–#250). Nothing here changes delivered bytes — |
…ecisions it forced Registered before any code moves, so the reasoning survives the fixes. FIVE NEW FROM /code-review max ON THE MERGED #239 (all reproduced here, none taken on trust): C-89 the no-copy guard prints the value it forbids, in one branch of three and proven on one side of two; C-90 the replacement drift checks assert less than their docstrings say, and one mutation proof is a tautology; C-91 the git plumbing turns ordinary developer states into hard errors with the wrong diagnosis; C-92 this repository lost its only check that the FAO consumer still SELECTS by the delivery label; C-93 a mutation proof written by whoever wrote the guard tests that author's imagination — 13 forms proposed by the author, 13 caught; 29 proposed independently, 15 missed. THREE MORE FROM THE EXPERT REVIEW OF THE STANDING DECISIONS: C-94 nothing anywhere detects an invisible delivery at the time it happens — every mechanism aimed at it is a CI-time proxy — deferred with a trigger and an owner rather than omitted; C-95 the integration-test prohibition is cited to þing-02 D2 in three places when it is þing-01 D2, and the real ruling is CONDITIONAL and GRANTS read-only preflight; C-96 the registry table carrying that permission is classified "not about this package". C-95 and C-96 compound into the finding that matters most for planning: a whole class of mechanism was believed closed for weeks because a citation pointed at the wrong verdict and the table stating the permission was declared none of our business. AMENDED: C-57 (my own amendment overclaimed — it cited a backticked example as proof the mid-sentence class was covered; it passes only because of the backticks, and a fourth failure direction was found: removing the length floor admitted this repo's own package names into a security guard's ban-set); C-86 (its partial-mitigation paragraph overstates what shipped — one of the three replacement checks does not match on the facts this repository declares); C-87 (one partner's residual got worse, not better). Header 93/20/73 -> 96/23/73. Epic #241, tracking #251. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C-89's first two thirds. The guard that exists to stop a coordinate value being published was publishing one, into a world-readable CI log, on precisely the event it exists to catch. THE VALUE IS NO LONGER A PARAMETER. Both branches build their finding through `_report_a_copy(where, coordinates)`, which is never handed the value — so a future call site cannot print one however it is written. That is the difference between a rule and a design: the markdown branch was taught not to print on 2026-08-11, the AST branch was not, and the two drifted for a day because each read fine on its own. It names ALL coordinates sharing the value, not one. Measured against the live registry: two pairs share a value (the prod-forecasts bucket and collection share both their id and their name), so a `value -> name` dict would keep the last and name the wrong coordinate half the time — in the one message a maintainer uses to find the copy. THE ROTATION PROOF CHECKED ONE SIDE OF TWO. It asserted the pinned value was absent and said nothing about the freshly rotated one, which is the more damaging half. Mutation- proven: leaking the post-rotation side while keeping the pinned side digested fails now and passed before. AND THE RULE NOW HAS A CHECK, which is the part that matters. `test_every_finding_goes_ through_the_one_reporter` asserts by AST that every `copied.append` in the scan calls the one reporter. Nothing compared the two branches, which is why prose saying "never print a value" coexisted with a branch printing one. Read as source rather than run, because the leaking branch only executes when a coordinate has actually been copied — a state this repository must never be in, so a behavioural test of it would have nothing to observe. Three mutations, each reverted: (1) restore the original AST f-string -> the new guard fails, naming the line; (2) leak the post-rotation side -> the rotation proof fails; (3) inject a real value declared by TWO coordinates into a package module -> caught, both coordinates named, no value in the message. Deliberately NOT in this change: the `secret` exemption and the ban-set's package-name collision. Those are the scan's SCOPE, not its reporting, and belong with the matcher rewrite in #243. C-89 stays open rather than being closed on two thirds of its content. Suite 411 passed / 1 skipped / 40 xfailed, ruff clean. The one pre-existing failure (`test_the_consumer_still_filters_on_the_name_until_it_checks_itself[crafd]`) is #248's, red since views-crafdapi refactored a literal into a named constant on 2026-08-12. Closes #242. Epic #241. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sage instead Five independent reviewers were run against the first attempt. They agreed, and they were right: the guard I wrote was worse than nothing. IT MISSED SIX OF EIGHT MUTATIONS AND FIRED ON A LEGITIMATE REFACTOR. It asserted that every finding was ROUTED through `_report_a_copy` — an AST walk over the scan's own source. But routing is not safety. The value could be smuggled through either of that helper's two parameters, appended with `extend` or `+=`, or reported from a renamed accumulator, and in every case the guard stayed green while a coordinate reached the log. Meanwhile `finding = _report_a_copy(...); copied.append(finding)` — an ordinary refactor that leaks nothing — failed it. A guard that misses the thing and cries wolf on the innocent is C-82's shape and ADR-014 §3's deletion criterion at once, and my docstring's claim that a caller "cannot print one however it is written" was false on the day I wrote it. SO READ THE FINISHED MESSAGE, NOT THE SYNTAX THAT PRODUCED IT. `test_the_scan_reports_ a_copy_without_reprinting_it` plants a value in a fixture tree, runs the real scan through both halves, and asserts the message names the coordinate and never the value. It does not matter how a finding is built or what the final assertion interpolates. All five surviving mutations are now caught — including one that leaked the entire ban-set through the assertion message rather than through any finding. That is also the answer to C-93: not a longer list of mutants I thought of, but a test whose shape makes the class unreachable. AND THE SCAN'S OWN DOCSTRING CARRIED THREE REGISTRY VALUES. pytest prints the failing function's source, so the guard would have published them on precisely the event it exists to catch — message clean, traceback not. Now it names coordinates. The wider measurement is registered as C-97: 33 standalone values in docstrings and comments across nine files, production modules included, excluded from the scan by a deliberate C-57 decision taken before anyone counted them. Three of them cannot be told from ordinary code at all — two are this repo's package names, one is a function name. Also fixed, both found by review and both real: - the markdown half named the coordinate the ASSIGNMENT names, not the one that DECLARES the value. Those differ exactly when a document pastes one coordinate's value beside another's name — the ordinary copy-paste slip — so it asserted a false fact and hid the coordinate actually leaked. - the rotation loop hardcoded the fixture's two literals a dozen lines from where the fixture builds them. Renaming the fixture strings turned the assertion into "two dead strings are absent". Now taken from the fixture's own variables. Register: C-89 amended with what was tried and what failed; C-97 opened; header 96/23 -> 97/24. The earlier "Mitigated" heading corrected to "Partial mitigation", which is one of the two phrasings this register declares — inventing a third passed the integrity guard by evading its string rather than complying, the identical escape ADR-014 §5 records C-15 making. C-89's Location line numbers repointed; the fix had moved all three and the amendment cited the old ones. The file is +112 lines against `development`, which is the wrong direction for this epic. This story is inherently additive — it adds a guarantee's check where there was none. #245 and #248 are deletions and are where the budget comes back. Suite 411 (410 passed / 1 skipped / 40 xfailed), ruff clean. The one failure is #248's, red since views-crafdapi refactored on 2026-08-12. Closes #242. Epic #241. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An adversarial mutation review ran 14 mutations against iteration 2. Twelve survived. Seven were leak channels; all seven are now closed. Five are refused in writing. A MESSAGE TEST ONLY SEES THE MESSAGE. `print(node.value)` beside the append, or a `logging.warning`, left the assertion text clean while pytest published the value under Captured stdout and Captured log call. The behavioural test now reads capsys and caplog as well. A stray debug print is an ordinary accident, not an adversarial one. SUBSTRING ABSENCE IS NOT ABSENCE. A finding carrying `value[:12]` or `value[::-1]` walked straight past `planted not in message`, and a reader recovers both instantly. Replaced by `_carries`: does any eight-character window, forwards or backwards, appear. AND THE DRIFT REPORTER HAD NO TEST AT ALL. `_describe_changes` is exercised only by the rotation proof, through one shape, with fixture values SHORTER THAN EVERY REAL COORDINATE — so a digest leaking only above a length threshold passed, as did a reversing digest, as did the appeared/removed branches a both-sides fixture never reaches. Three leaks, all green. It now has its own test, driven through all three shapes with an over-long opaque value. A HELPER'S PARAMETER IS A PUBLICATION CHANNEL. `_dotenv_use` took file text; pytest renders a frame's arguments and `ast.parse` raises on any malformed module, so a package file that both failed to parse and carried a value would have printed itself in full. It takes a Path now and reads inside. And the fixture values are opaque now because a readable one bit me: the first planted string contained the word "coordinate", which also appears in the check's own failure prose, so the window test fired on the test's own English. My cry-wolf, caught by my own guard, fixed in the fixture rather than by weakening the check. WHAT I AM NOT CHASING, and why it is written down rather than left to be rediscovered. Five mutations survive. Four 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, because the fixture satisfies whatever narrowing is applied. They are #243's subject and are routed there with reproductions; fixing them here would be doing that story badly and twice. The fifth deletes the no-print assertion itself, which is infinite regress: a guard on a guard on a guard has no fixed point, and the register carries that risk instead. The eight-character window is a judgement, not a law — a six-character leak would pass, and chasing it is the whack-a-mole this epic exists to refuse. Register: C-89 amended with all three passes, including what was tried and failed. Suite 412 (411 passed / 1 skipped / 40 xfailed), ruff clean. The one failure is #248's. Closes #242. Epic #241. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The maintainer's verdict on iterations 1-3: "so complicated and heavy with technical
debt that you are not even able to maintain it. Then no one can." He is right, and the
measurement is worse than the impression. Four commits had grown one file by +195 lines
(1145 -> 1340, +17%). +95 of that was PROSE restating the risk register. All 149 lines
of new function bodies were tests of another test in the same file. Meta tests went from
37% to 45% of the file.
This commit deletes 140 of those lines. Every one of the four outstanding review
findings is resolved by subtraction.
TWO OF MY OWN ADDITIONS WERE DEFECTIVE, AND A DESIGN REVIEW CAUGHT BOTH.
`test_the_drift_report_never_carries_a_value_in_any_recoverable_form` claimed to drive
`_describe_changes`'s appeared/removed branches. It did not. Those branch on
`was is None`; my fixtures passed `_ABSENT`, a sentinel OBJECT. The branch is never
entered — it ran one path three times while asserting three, and the register repeated
the claim. That is an ADR-014 §1 defect inside the fix for an ADR-014 §1 defect. The
branches it named return constant strings and cannot leak at all. DELETED.
`_carries`, my eight-character-window "recoverability" predicate, is STRICTLY WORSE THAN
PLAIN `in`. The window degrades to exact match below eight characters, and this package's
two directory names are five-character declared values. Measured:
`_carries("views_postprocessing/unfao/leaky.py …", "unfao")` is True — it condemns a
bare, safe file path while screaming that the scan published a value. Simultaneously
stricter than `in` on opaque fixtures and identical to it on the values that matter.
DELETED; both sites use plain `not in`, which is the vocabulary this file already uses in
`test_the_environment_refusal_logs_names_and_never_values`.
ALSO DELETED: `_report_a_copy` — 2 lines of code under 14 lines of docstring, 11 of them
explaining what it does NOT guarantee. Its safety claim was falsified in iteration 2 and
it survived anyway. Two inline f-strings now, which is what WET-before-DRY prescribes:
the second incident did not have the same shape as the first, and the helper's signature
forced the markdown branch into a detour it had to justify with a six-line comment. The
behavioural guard is what holds the rule, not the formatter.
The output guard SURVIVES, at 19 lines instead of 78. ADR-014 §1 does not permit a
guarantee with no check, and this regression already happened once, silently. But it no
longer monkeypatches four globals, shells out to `git init`, or guards a `print` nobody
has written.
AND THE ONE REAL OPEN LEAK IS NOW CLOSED, AT BOTH SITES. Iteration 3 claimed a malformed
package module could no longer publish itself because `_dotenv_use` takes a Path.
Measured: that changes nothing — the leak is `ast.parse`'s own frame, which pytest
renders with the source as its argument, and `path.read_text()` still hands it there. The
fix is `raise ... from None`, where `from None` is load-bearing because the chained
SyntaxError carries the same text. Applied to BOTH parse sites; the scan itself parses
every package .py, and fixing one and not the other is C-89's shape verbatim.
Proven: a malformed module carrying a real value now fails with "does not parse (line
92)" and the value appears ZERO times, where it appeared before.
Five mutations re-proven: the original C-89 defect fails; a widened assertion fails; a
raw digest fails; a malformed module fails without publishing itself; a legitimate
refactor passes.
Register: C-89's Location is now FUNCTION NAMES — it has cited stale line numbers twice,
because each fix moved them. The false "it takes a Path now" sentence is corrected. The
narration of both deleted additions is removed, including one claim that was never true.
Two deferrals get triggers: consolidating into tests/test_redaction_guard.py when #243
finishes with this file, and a pre-existing proof that re-types its subject inline.
tests/test_env_declaration.py: 1340 -> 1200 (+55 over the pre-story 1145, not +195).
Suite 410 passed / 1 skipped / 40 xfailed, ruff clean. The one failure is #248's.
Closes #242. Epic #241.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-not-print #242: the no-copy guard must not publish what it forbids — and then, delete most of the guard
…her repo's changelog C-86 is that this repository's release path can be blocked by another repository's routine work. PR #239 was written to reduce that and shipped a check that reproduced it. MEASURED, ON THE LIVE REGISTRY. The drift check had two halves. `changed` is filtered to the names each partner declares. `arrived` was not — it ran over every row of every table this package depends on. Each partner reads 13 of those 25 rows. Eight belong to no repository here: three platform key slots, three caller keys for other repos, and two contract rows. So views-appwrite issuing one key for someone else turned both partner parametrizations red and blocked a merge to main, which is the release to FAO. The same test's docstring, seven lines above, said "Silent through: prose edits, [meta] bumps, and rows belonging to anyone else." The prose described the check that was designed; the code implemented a wider one. ITS SELF-DEFENCE WAS FALSE. The failure message claimed `[contract.*]` "arrived exactly this way, and nothing else here would have seen it". `[contract]` is a top-level TABLE, and `test_every_table_in_the_registry_is_classified_here` — thirty lines above, in the same file — catches exactly that. The case it cited as its reason to exist was already covered by its neighbour. MUTATION-PROVEN IN BOTH DIRECTIONS against the real registry: upstream issues an API key for another repo -> silent (was: red build) a third partner's contract row lands -> silent (was: red build) a value this partner reads rotates -> fires a name this partner reads is removed -> fires The second one is not hypothetical: views-productionapi is a forthcoming consumer, and its contract row landing would have reddened both existing partners. AND THE STOPPING RULE IS NOW IN THE FILE, above the check, because this check has been widened once already: it fires on exactly one condition — a row this partner declares differs between the pinned edition and the sibling's main — and any proposal to add a condition must name the delivery failure it prevents and show that failure is not already prevented by assert_env_declared at run time. Deliberately NOT done: adopting `[edition].obliges_consumers` (views-appwrite#76, shipped as registry v1.6.0). Deleting `arrived` fixes the shipping problem; adopting the flag is an addition, and it would move a safety property onto another repository's labelling discipline. `changed` compares bytes and depends on nobody's self-description. Deferred with a trigger in C-86. Register: C-86's "⚠ CORRECTED" note becomes CORRECTED-and-RESOLVED, with the measurement; its rate claim is now accurate, which it was not before. C-90 gets a partial mitigation — the tautological proof and the untested `registry_current` are #246's and it stays open. Suite 410 passed / 1 skipped / 40 xfailed, ruff clean. The one failure is #248's. Closes #245. Epic #241. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rule that read backwards
Two independent reviews of the deletion. The deletion itself is sound; three of my own
claims about it were not, and one of them is the defect this whole epic is about.
I WROTE A MUTATION RESULT IN THE PAST TENSE. The docstring, the commit message and C-86
all said an upstream key "reddened this repository and blocked a release". Measured:
ZERO rows have arrived in the depended-on tables since the pin — the arrival half was
green on every real edition it ever saw, from its introduction on 2026-08-11 to its
deletion a day later. And `protect_main` carries `deletion, non_fast_forward,
pull_request` and no required status check, so a red build blocks nothing today anyway.
What is true: a mutation shows it WOULD fire, and it WOULD block once C-81 lands. Writing
the counterfactual as history, in a change whose subject is prose outrunning code, is the
thing itself.
THE STOPPING RULE READ BACKWARDS. It said a proposed widening must show its failure is
"not already prevented by assert_env_declared at run time". Applied literally that
ADMITS the arrival half — an unadopted upstream coordinate is not prevented by it — and
REJECTS the removal detection this check performs, because a removed coordinate IS caught
at run time when the launcher cannot supply the name. The rule now asks the question that
actually discriminates: state the delivery failure prevented, AND the false-alarm surface
measured against the live registry; if that surface includes rows this package does not
read, it is refused.
"8 BELONG TO NO REPOSITORY HERE" IS SIX. The other two are this package's own delivery
labels — both rows declare `producer = "views-postprocessing"` — and they are covered by
tests/test_product.py, not by this check. C-90's pre-existing text had this right; I
upgraded a careful claim into a false one and propagated it to four places.
AND THE DELETION'S REAL COST WAS RECORDED NOWHERE. A coordinate views-appwrite issues FOR
this package, or a second `[contract.*]` row for a future partner, is now silent until a
human reads the registry — no test, no runtime assert. C-90 had posed exactly this
question ("they cannot both stand") and my mitigation answered only the half that
flattered the change. Now stated in the docstring and decided in C-90.
THE SILENCE DIRECTION NOW HAS A CHECK. The proofs either side of this one are positive —
rotation fires, a rename fires — so a third widening would have met no objection and the
rule forbidding it would have been prose with a guard on the other half only. Mutation-
proven by re-adding an arrival half and watching it fail.
C-86: two of three, not one. "Every top-level table must be classified" still fires
regardless of obligation, and HAS — `[edition]` at v1.6.0, an edition the registry itself
calls "additive and opt-in; obliges nobody", reddened this repo. Recorded as the entry's
remaining rate risk with the `obliges_consumers` deferral as its fix.
ADR-014 §5: ADR-016 §1 and §7b both asserted the deleted behaviour as current, and §7b is
the section C-86 cross-references. Both corrected with an erratum naming the cost.
C-90's Trigger, Location, title and present-tense body all repointed at the two defects
that remain.
Suite 412 passed / 1 skipped / 40 xfailed, ruff clean. The one failure is #248's.
Epic #241.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he check it named /review-diff on the remediation, and it found the remediation's own defect. Same class, one level up, in the change that was fixing it. THE SILENCE GUARD DID NOT GUARD THE CHECK. Its docstring said "the stopping rule above, as a check rather than a paragraph" and claimed a third widening "would have met no objection" — implying it now would. It would not. The test called `_describe_changes` directly, the helper UNDERNEATH the check, and the deleted arrival half never lived in that helper. Proven: re-adding `arrived` to the real check, exactly as it was before #245, left the whole suite green at 41 passed. AND THE MUTATION PROOF I OFFERED FOR IT WAS INVALID. I mutated the TEST — adding arrival logic inside the test body — and watched it fail. That proves the assertion works on input I hand it. It proves nothing about the code the test is supposed to be watching. A proof has to mutate the subject, not the instrument. It now drives the real check through monkeypatched registry readers, the pattern #242 arrived at for the same reason. Re-proven properly: re-widening the real check fails it, naming the offending row. Also fixed while in there: `mine = next(iter(names))` picked an arbitrary set element, so the fixture varied per process. `sorted(...)[0]` instead — declared, not incidental. And one near-miss worth recording: my first attempt to drop an unused local used a `sed` pattern that matched three OTHER tests and silently deleted their `module = ...` binding. Ruff caught it as seven F821s, restored from a `cp` backup. The lesson is the standing one — anchor edits on unique text, not on a pattern that happens to be unique in the region you are looking at. Register: C-89's sibling lesson recorded on C-86's mitigation — a guard must be pointed at the thing it claims to guard, which is the sentence this arc keeps re-learning. Suite 412 passed / 1 skipped / 40 xfailed, ruff clean. The one failure is #248's. Epic #241. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Iteration 2's review, and it found the sharpest thing on this branch. THE DRIFT CHECK'S ENTIRE ASSERTION COULD BE DELETED WITH A GREEN SUITE. Measured: neuter `assert not changed` and the suite stays at 412 passed. My silence guard was the only test driving the real check, and it is purely negative — it asserts the check does NOT raise. Nothing asserted it does. AND MY DOCSTRING VOUCHED FOR THE NEIGHBOURS. It said "the proofs either side of this one are positive — rotation fires, a rename fires". Neither is about this check. The rotation proof calls `_describe_changes`, the helper underneath it — which is precisely the defect I diagnosed and fixed for the silence guard one commit earlier. The rename proof calls `_declared_classes` and re-types the CLASS check's comparison inline; it belongs to a different check entirely, under this one's name. So I applied "a guard must be pointed at the thing it claims to guard" to one of the three tests in the cluster, then used that fix as a docstring warrant for the other two. C-90's rewritten Trigger says "Both remaining halves are proof defects" and enumerates two; this was a third of the same class, in the same file, which made the word "Both" falsifiable by a one-line mutation. Fixed by using the harness I had already built for only the direction that cannot fail: `test_the_drift_check_fires_when_a_row_this_partner_reads_rotates` drives the same real check with a rotated row and `pytest.raises`. Both directions now mutation-proven THROUGH THE CHECK: neuter the assertion -> the twin fails; re-add the arrival half -> the silence guard fails. The vouching for the neighbours is deleted rather than repaired; they are still worth having, they are simply not about this. TWO REGISTER CORRECTIONS, one of which is the same mistake inside its own correction. "6 belong to OTHER REPOSITORIES ENTIRELY" — no. Three are caller keys for other consumers; three are platform slots still marked status="planned" that belong to no repository at all, and one of those names "un_fao delivery" among the identities it would be issued for. C-90's original "belong to no repository here" was the careful wording. I had upgraded it, corrected the upgrade, and upgraded it again two sentences from the correction. And a quotation that was never written: the text pointed "further down this entry" for the partition "earning its keep". That phrase appears nowhere in the repository. The real record is 81 lines ABOVE, and says something narrower. Now quoted verbatim, direction fixed. Also: the silence fixture filed a `secret`-class name under `[target]` — harmless, since both sides carry the identical row, but its two neighbours each assert fixture honesty and this one did not. It now picks a genuinely `target`-class name and asserts it. Suite 414 passed / 1 skipped / 40 xfailed, ruff clean. The one failure is #248's. Epic #241. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d a tautology
Two findings, both mine, both classes I had just finished correcting elsewhere in the
same commit.
THE FIXTURE-HONESTY ASSERTION COULD NOT FAIL. I wrote:
mine = sorted(n for n, cls in expected_class.items() if cls == "target")[0]
assert expected_class[mine] == "target", ...
`mine` is SELECTED by the condition it is then asserted to satisfy. True by construction,
for every possible input. That is C-90's own first defect — `base = {name: {} for name in
_TABLE_ROLE}` then `assert not _unclassified_tables(base)` — reproduced by me in the
commit whose whole subject was "nothing proves this check fires".
Replaced with the thing that can actually go wrong: assert the selection found anything.
If a partner stops declaring a `target` coordinate the fixture has nothing to file and
would prove nothing, and now it says so. Proven by pointing the selection at a class that
does not exist and watching it fail.
AND "THE ROTATION PROOF BELOW" IS ABOVE. It is at :929; the twin is at :1022 — 93 lines
below it, not above. Same directional error as the "recorded further down this entry"
pointer I fixed in the same commit, which pointed 81 lines the wrong way. Two in one
change is a pattern rather than a slip: I describe a file's layout from memory instead of
reading it.
Suite 414 passed / 1 skipped / 40 xfailed, ruff clean. The one failure is #248's.
Epic #241.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l-check #245: delete the arrival check — it subscribed us to another repo's changelog
… wrong
THE SUITE IS FULLY GREEN AGAIN. 413 passed, zero failures. The failure that has been red
on `development` since 2026-08-12 is gone, and it is gone by deleting the check rather
than repairing it.
TWO BREAKAGES IN TWENTY-FOUR HOURS, BOTH BECAUSE THEIR CODE GOT BETTER. views-faoapi on
11 August, views-crafdapi on the 12th, each refactoring `APIPathManager("literal")` into
`APIPathManager(CONSUMER_DOCUMENT_NAME)` — an improvement in both repositories, and a red
build here both times. The regex was measuring their code quality and reporting it as our
failure. ADR-017 §7 said we were never entitled to depend on another repository's file
layout; repairing it a third time would have been repairing the wrong thing.
WHAT WENT: `_CONSUMER_PATH_MANAGER`, `_CONSUMER_FILTER`, `_CONSUMER_SELF_CHECK_PENDING`,
`_consumer_package`, both tests that used them, and the views-crafdapi checkout from CI —
measured first, that fetch had exactly zero remaining consumers. The SIBLINGS entry stays
with `ci_checkout=False` and a note naming the record, because CONSUMER_REPO still names
that repository; only the fetch is gone.
THE SEQUENCING CONSTRAINT DISSOLVED RATHER THAN BEING SATISFIED, and that is the part
worth writing down. ADR-017 §5 said a partner's source read is removed only once that
partner's consumer-side check lands. views-crafdapi#53 has not landed. The read went
anyway, because the constraint was guarding a window in a mechanism that turned out to be
the wrong mechanism. §5 now carries an erratum saying so, and a plainer rule: a check that
reads another repository's source is not a check this repository builds.
THE GAP IS NOW PERMANENT AND UNGUARDED HERE, BY CHOICE. Nothing verifies that a consumer's
query uses the name it declares. C-92 states the chain link by link and says plainly that
a delivery is verified by two values this platform authored agreeing with each other, plus
the consumer's own word. Filed where the fact lives: views-faoapi#390 (under their
seam-verification epic #383, whose flagship this is) and views-crafdapi#55 — both with
file:line evidence, both noting that `manager.py:117` applies the filter conditionally so
a falsy name broadens the query rather than failing, both explicitly not prescribing their
internals.
Also, while in the file: the module docstring and a comment block each carried a real
coordinate value, and pytest prints a module docstring whenever anything in the file
fails. Now named, not quoted (C-97).
tests/test_product.py 348 -> 253. Suite 413 passed / 1 skipped / 40 xfailed, ruff clean.
Closes #248. Unblocks PR #240. Epic #241.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ther repository
The deletion is right and the review confirmed it: nothing stranded in code, the workflow
edit is clean, all six CI rules still bite. The defect is in the prose, and it is the
worst kind — permanent governance text resting on something untrue.
VIEWS-CRAFDAPI#53 CLOSED AT 12:15. MY COMMIT WAS 18:18 THE SAME DAY. My ADR-017 §5
erratum says "neither partner's step 3 is what removed them" and my commit message says
"views-crafdapi#53 has not landed". Both false, by six hours. Both partners' consumer-side
checks HAD landed — the constraint was satisfied, not dissolved.
Worse, and more interesting: the crafd commit that satisfied step 3 is THE SAME COMMIT
that broke our regex. While binding its served name to the registry, it tidied
`APIPathManager("literal")` into `APIPathManager(CONSUMER_DOCUMENT_NAME)`. The improvement
and the breakage were one edit, in both repositories, a day apart. The event I narrated as
"their code got better and our test went red" IS the sequencing constraint being met.
So the erratum is rewritten on honest ground. The outcome stands — the source read had to
go — but on §7's argument (we were never entitled to depend on another repository's file
layout), not on a false claim that the ordering was bypassed. The constraint is
DISCHARGED, not repealed. Writing a repeal into ADR-017 on an untrue claim about another
repository is §3's failure inside §5's text, and it is corrected visibly rather than
quietly.
NINE MORE STALE CLAIMS, all named by the same review, none caught by any test:
ADR-017 — §5's body still said "today the check still reads the consumer's source and
still skips in CI", seven lines below the erratum saying otherwise. Appendix B still said
views-crafdapi#53 was open, that step 4 was "not yet filed", and closed the document with
"so no window exists in which the label is verified only against our own typing" — which
this change makes permanently false, deliberately.
ADR-016 — the §4 table row said the crafdapi fetch "retires when views-crafdapi#53 lands,
not before"; §5 explained which download was temporary; the 2026-08-11 update said that
partner's source-read and fetch stay until #53 lands. All three now false.
Register — C-92's own Location cited three symbols this change deleted and two line
numbers past the end of a file that is now 253 lines; its title and Trigger scoped to FAO
while its body covers both partners; its "second structural half" is entirely about
deleted code. C-87's trigger named #53 as live. C-81 said crafdapi is "now checked out in
CI". C-97's headline number was measured at 33 and is now 25 across 8 files — recounted,
with a note to re-measure rather than cite it.
AND CONSUMER_REPO WAS GUARDING A PRECONDITION OF NOTHING. Its test's docstring said "the
gated check below iterates this map"; there is no gated check below. Its failure message
described a `require_sibling()` call site that no longer exists. Reframed to what the map
is actually for now: addressing — it records who receives each delivery, and it is who
C-92's cross-repo asks are sent to.
Suite 413 passed / 1 skipped / 40 xfailed, ruff clean.
Epic #241.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/review-diff on the remediation. The erratum asserted that in BOTH repositories the commit satisfying step 3 was the same commit that broke our source read. I had verified that for views-crafdapi and asserted it for views-faoapi — the exact overstatement this arc keeps producing, in the paragraph correcting an overstatement. Verified now, and it holds: `8615574` (faoapi, #379, closed 11 August) and `0c493ae` (crafd, #53, closed 12:15 on the 12th) each introduce the named constant AND add that repository's registry-binding test. One edit, both effects, both repositories. Both shas are now in the ADR so the next reader can check it in one command rather than trusting the sentence. Also: #379 is an issue and issues close, they do not merge. Cross-document scan for residual contradictions across ADR-016 and ADR-017 — "source-read stays", "fetch stays", "#53 open", "#53 lands", "temporarily", "no window exists" — all zero. Suite 413 passed / 1 skipped / 40 xfailed, ruff clean. Epic #241. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er-source-reads #248: delete both consumer source-reads — the category is wrong
|
Unblocked and green. Gate 1 of #241 is complete — #242, #245, #248 all merged. The three defects that would have shipped in this release are fixed:
Not merging this myself — it is the release to FAO. Yours when you want it. Suggested order after: cut the tag (views-models#364 and views-crafdapi#43 are waiting on one), then make |
… scan is finished THE PARSE IS GONE. The scan no longer reads a line, captures what it thinks the value is, and compares. It knows both halves before it starts, so it matches NAME=VALUE directly, with findall rather than one match per line. Every blindness C-57 records came from that parse — the swallowed comment, the missing terminator, the six-character lookbehind, the second assignment on a table row. None is expressible now. The two halves need not correspond: `APPWRITE_X=<value declared for APPWRITE_Y>` is the ordinary copy-paste slip, and it names the coordinate that DECLARES the value rather than the one the line assigns. THE SECRET EXEMPTION IS GONE. An inline name literal, sitting under a comment saying the scope came from the declared partition and not from an inline list. Measured, [secret] carries no values today, so removing it changes nothing now and closes the hole the day one gains a value. THE STOPPING RULE IS A TEST. It enumerates every line in this repository's own tracked markdown that assigns a declared coordinate and fails if the matcher cannot read one. So the form list comes from the corpus: a form no document here uses is not a gap, and a new form is taught in the same change as the document that introduces it. That is what stops a fifth widening, which is what this story was for. ALL FOUR SCOPE MUTATIONS ROUTED FROM #242 ARE CLOSED, by one richer fixture rather than four tests — two copies, two sections, one short, one in markdown. Each element defeats a specific narrowing: stopping at the first finding, reading only `target`, reinstating a length floor, skipping the markdown branch. Measured surviving before, caught after. Plus one small test for a package module that does not parse. AND AN EXCLUSION LIST WAS BUILT AND THEN DELETED, which is the part worth keeping. The plan called for excluding values spelled like this repository's own code. Measured against what the AST branch actually sees — non-docstring string constants — THE COLLISION DOES NOT EXIST: the only registry values appearing that way are the two contract rows, and contract is MIRRORED rather than CONSUMED, so it is never in the scanned set. I had built a narrowing of a security scan for a problem this branch does not have. Deleted; the latent version is recorded in C-97. Register: C-57 gets the mitigation, in one place, and the docstrings point at it rather than restating it — the one-home rule, applied for the first time. tests/*.py 9200 -> 9308. The budget is still the wrong way and I am not going to pretend otherwise; see the story comment. Suite 415 passed / 1 skipped / 40 xfailed, ruff clean. Closes #243. Epic #241. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-stopping-rule #243: match the pair, and let the corpus decide when the scan is finished
Eight days of work on
development, 15 merged pull requests, 43 commits, 54 files.This merge is the release to FAO. Everything below is currently invisible in production.
The safety claim first
No delivered byte changes.
CONTRACT_VERSION1.5onmain,1.5here — unchangedtests/fixtures/The package changes are a store-construction refactor (C-40), a deleted enrichment module that nothing called, and two pinned constants. What FAO receives is what FAO received.
What this actually buys
Three new ADRs, and the second and third exist because a real thing broke.
run_pytest.ymlasserting a fact about another repository that nothing checked, and which had been false for two days.CI got materially stronger. It now checks out
views-appwriteandviews-crafdapiwith no credential, so the cross-repository checks run on every pull request instead of on a maintainer's laptop.tests/test_ci_sibling_coverage.pyholds six rules keeping the workflow and the declaration honest with each other, each proven against a deliberately broken world.The registry drift check learned to match on the right thing. It compared edition label strings, so five upstream editions in four days each reddened this repository and blocked a release — for edits that carried no obligation for anyone. It now matches on the rows this package actually reads. Measured across the window that caused the problem: green, where the old check was red four times.
That check has already earned it. Mid-work it went red on live data nobody warned us about: views-appwrite had shipped a new
[edition]table marking each edition as obliging consumers or not. We learned it existed because a guard objected.Three consumer-facing correctness fixes — C-77, C-79, C-83 — plus the delivery-label check for both partners now reading the public registry rather than scraping the consumer's source code. The old check broke when views-faoapi tidied their own file: their code got better and our test went red, which is a check we were never entitled to have.
The register
mainFive new concerns, seven closed. The five new ones are mostly things this work discovered about its own CI coupling rather than about delivery.
What this does NOT fix, stated plainly
mainhas no required status check.protect_main's ruleset is active but its ref-name include-list is empty, so it matches nothing — a red build does not block a merge here today. That is C-81, and it is a console action.And read C-86 before you fix it.
protect_mainlists zero bypass actors, and a GitHub ruleset applies to everyone except the actors it names — so admin status confers no exemption. The momenttestbecomes required, this repository's releases depend on two other repositories'mainbeing reachable, with no way past a red build. Both changes are right. Doing them without knowing they combine is not.Suggested order: merge this, cut the tag, then make the check required — so the guards are live before they are blocking.
Verification
Green on
developmentatf7451e6, and all four checks green on the head commit.🤖 Generated with Claude Code