From 9420f5a481bf34cf5a3e38c4e5b8da8671b88235 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Aug 2026 01:22:30 +0200 Subject: [PATCH 1/3] feat(ci): declare sibling visibility, and un-dark seven cross-repo checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/run_pytest.yml | 50 +- docs/ADRs/000_use_of_adrs.md | 2 +- .../001_ontology_of_views_postprocessing.md | 2 +- .../ADRs/002_topology_and_dependency_rules.md | 2 +- ...uthority_of_declarations_over_inference.md | 2 +- ...ng_as_mandatory_critical_infrastructure.md | 2 +- ...ntent_contracts_for_non_trivial_classes.md | 2 +- ..._based_agents_as_untrusted_contributors.md | 2 +- .../008_observability_and_explicit_failure.md | 2 +- ..._contracts_and_configuration_validation.md | 2 +- docs/ADRs/010_technical_risk_register.md | 2 +- ..._runtime_mapper_with_precomputed_lookup.md | 2 +- docs/ADRs/012_revised_ontology.md | 2 +- .../013_sampled_forecast_wire_contract.md | 2 +- ...4_claims_and_the_guards_that_carry_them.md | 2 +- .../015_the_pipeline_core_appwrite_import.md | 2 +- .../016_ci_read_access_to_private_siblings.md | 206 +++++++ docs/ADRs/README.md | 16 + poetry.lock | 4 +- pyproject.toml | 8 + reports/technical_risk_register.md | 26 +- tests/conftest.py | 122 ++++- tests/test_ci_sibling_coverage.py | 511 ++++++++++++++++++ tests/test_env_declaration.py | 12 +- tests/test_product.py | 6 +- 25 files changed, 929 insertions(+), 62 deletions(-) create mode 100644 docs/ADRs/016_ci_read_access_to_private_siblings.md create mode 100644 tests/test_ci_sibling_coverage.py diff --git a/.github/workflows/run_pytest.yml b/.github/workflows/run_pytest.yml index 1e2d99f..b548505 100644 --- a/.github/workflows/run_pytest.yml +++ b/.github/workflows/run_pytest.yml @@ -27,35 +27,46 @@ jobs: # is normal. The consequence was that CI verified strictly LESS than a developer's # laptop, and precisely on the guards that matter most across a repo boundary. # - # Measured 2026-08-03 (402 collected in all three runs, so the delta is entirely - # skips): - # local, all siblings present 362 passed / 40 xfailed / 0 skipped - # CI, bare checkout 347 passed / 38 xfailed / 17 skipped - # CI, with the checkout below 348 passed / 38 xfailed / 16 skipped + # WHICH siblings are checked out is NOT decided here. It is declared in + # `tests/conftest.py::SIBLINGS`, and `tests/test_ci_sibling_coverage.py` fails if + # this file and that declaration disagree — in either direction. ADR-016. # - # So this recovers exactly ONE test — the cross-seam consumer-name check for CRAF'd. - # That is worth having (it is the §4.1a pin, whose failure mode is a delivery nobody - # can find) but it is one, not seventeen. The other sixteen are below. + # The prose that used to live here said views-appwrite was PRIVATE. It went public + # on 2026-08-08 and the comment did not, so seven checks stayed dark for no reason. + # That is why the fact now lives in a declaration carrying the date it was checked, + # rather than in a paragraph. # - # Only views-crafdapi is checked out, and the choice is deliberate: + # `ref: main` is deliberate on every sibling and is not a default. Without it + # `actions/checkout` takes the sibling's OWN default branch — which for + # views-appwrite is `development` — while ADR-014 §3 says the authority for a claim + # about another repository is that repository's `main`. Two checks in + # test_env_declaration read the working tree and a third demands reachability from + # `main`; pointed at `development` they would eventually demand contradictory things. # - # views-crafdapi PUBLIC, and its check reads source text -> enabled here. - # views-datafactory PUBLIC, but its 8 tests need the producer's raw GAUL - # parquets (`data/raw/gaul_admin/*.parquet`), which are NOT in - # its git repository. Checking it out converts an honest skip - # into a FileNotFoundError. Measured, not assumed — it was tried. - # views-appwrite PRIVATE. The registry-drift detectors live here and are the - # views-faoapi most valuable of the lot; enabling them needs a token, which - # is an operator decision. Register C-46 and C-57 both carry - # that as their standing residual, and it is still open. # `actions/checkout` refuses a path outside $GITHUB_WORKSPACE, so the siblings go # inside it and are pointed at by the declared env vars instead of the conventional # `../` location. `sibling_repo` checks $VIEWS_ first, precisely so the - # location can be declared rather than assumed. + # location can be declared rather than assumed. `_siblings/` specifically: it is + # excluded from ruff in pyproject.toml, and the Lint step reported 745 findings in + # someone else's test suite the first time a sibling landed outside it. + # + # No `token:` on any step below, and that is load-bearing rather than an omission: + # the default GITHUB_TOKEN is scoped to THIS repository, so a tokenless checkout of + # a repo declared public FAILS if it is actually private. That is what verifies the + # `public` field — no test does, because nothing in this suite touches the network. + - name: Checkout views-appwrite (sibling) + uses: actions/checkout@v3 + with: + repository: views-platform/views-appwrite + ref: main + path: _siblings/views-appwrite + fetch-depth: 0 + - name: Checkout views-crafdapi (sibling) uses: actions/checkout@v3 with: repository: views-platform/views-crafdapi + ref: main path: _siblings/views-crafdapi fetch-depth: 0 @@ -80,6 +91,7 @@ jobs: - name: Run tests env: + VIEWS_APPWRITE: ${{ github.workspace }}/_siblings/views-appwrite VIEWS_CRAFDAPI: ${{ github.workspace }}/_siblings/views-crafdapi run: | set -e diff --git a/docs/ADRs/000_use_of_adrs.md b/docs/ADRs/000_use_of_adrs.md index ee9b9b3..e30bd47 100644 --- a/docs/ADRs/000_use_of_adrs.md +++ b/docs/ADRs/000_use_of_adrs.md @@ -3,7 +3,7 @@ **Status:** Accepted **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase **Informed:** All contributors --- diff --git a/docs/ADRs/001_ontology_of_views_postprocessing.md b/docs/ADRs/001_ontology_of_views_postprocessing.md index 0a32b2e..df9a73d 100644 --- a/docs/ADRs/001_ontology_of_views_postprocessing.md +++ b/docs/ADRs/001_ontology_of_views_postprocessing.md @@ -3,7 +3,7 @@ **Status:** Superseded by [ADR-012](012_revised_ontology.md) (2026-06-27) **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase --- diff --git a/docs/ADRs/002_topology_and_dependency_rules.md b/docs/ADRs/002_topology_and_dependency_rules.md index ebf9a7a..83e4ff3 100644 --- a/docs/ADRs/002_topology_and_dependency_rules.md +++ b/docs/ADRs/002_topology_and_dependency_rules.md @@ -2,7 +2,7 @@ **Status:** Accepted — amended 2026-06-27 (see [Amendment](#amendment-2026-06-27)) **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase --- diff --git a/docs/ADRs/003_authority_of_declarations_over_inference.md b/docs/ADRs/003_authority_of_declarations_over_inference.md index cd2e868..ac664c3 100644 --- a/docs/ADRs/003_authority_of_declarations_over_inference.md +++ b/docs/ADRs/003_authority_of_declarations_over_inference.md @@ -3,7 +3,7 @@ **Status:** Accepted **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase --- diff --git a/docs/ADRs/005_testing_as_mandatory_critical_infrastructure.md b/docs/ADRs/005_testing_as_mandatory_critical_infrastructure.md index 534673e..0bb32c9 100644 --- a/docs/ADRs/005_testing_as_mandatory_critical_infrastructure.md +++ b/docs/ADRs/005_testing_as_mandatory_critical_infrastructure.md @@ -3,7 +3,7 @@ **Status:** Accepted **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase --- diff --git a/docs/ADRs/006_intent_contracts_for_non_trivial_classes.md b/docs/ADRs/006_intent_contracts_for_non_trivial_classes.md index 46827bc..5d11083 100644 --- a/docs/ADRs/006_intent_contracts_for_non_trivial_classes.md +++ b/docs/ADRs/006_intent_contracts_for_non_trivial_classes.md @@ -3,7 +3,7 @@ **Status:** Accepted **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase --- diff --git a/docs/ADRs/007_silicon_based_agents_as_untrusted_contributors.md b/docs/ADRs/007_silicon_based_agents_as_untrusted_contributors.md index bbf45ab..1703917 100644 --- a/docs/ADRs/007_silicon_based_agents_as_untrusted_contributors.md +++ b/docs/ADRs/007_silicon_based_agents_as_untrusted_contributors.md @@ -3,7 +3,7 @@ **Status:** Accepted **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase --- diff --git a/docs/ADRs/008_observability_and_explicit_failure.md b/docs/ADRs/008_observability_and_explicit_failure.md index c48583a..9d22e06 100644 --- a/docs/ADRs/008_observability_and_explicit_failure.md +++ b/docs/ADRs/008_observability_and_explicit_failure.md @@ -3,7 +3,7 @@ **Status:** Accepted **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase --- diff --git a/docs/ADRs/009_boundary_contracts_and_configuration_validation.md b/docs/ADRs/009_boundary_contracts_and_configuration_validation.md index 6e7c0d7..3668251 100644 --- a/docs/ADRs/009_boundary_contracts_and_configuration_validation.md +++ b/docs/ADRs/009_boundary_contracts_and_configuration_validation.md @@ -2,7 +2,7 @@ **Status:** Accepted **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase --- diff --git a/docs/ADRs/010_technical_risk_register.md b/docs/ADRs/010_technical_risk_register.md index 56d7bdf..993f9f3 100644 --- a/docs/ADRs/010_technical_risk_register.md +++ b/docs/ADRs/010_technical_risk_register.md @@ -2,7 +2,7 @@ **Status:** Accepted **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase --- diff --git a/docs/ADRs/011_replace_runtime_mapper_with_precomputed_lookup.md b/docs/ADRs/011_replace_runtime_mapper_with_precomputed_lookup.md index e2cc9a7..ed789c5 100644 --- a/docs/ADRs/011_replace_runtime_mapper_with_precomputed_lookup.md +++ b/docs/ADRs/011_replace_runtime_mapper_with_precomputed_lookup.md @@ -4,7 +4,7 @@ **Status:** Accepted **Date:** 2026-06-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase **Consulted:** FAO-FSFC (via Pre-release Note 02 and Release Note 02 confirmation) **Informed:** All contributors, UN FAO operational team diff --git a/docs/ADRs/012_revised_ontology.md b/docs/ADRs/012_revised_ontology.md index 9b05a14..846292f 100644 --- a/docs/ADRs/012_revised_ontology.md +++ b/docs/ADRs/012_revised_ontology.md @@ -2,7 +2,7 @@ **Status:** Accepted **Date:** 2026-06-27 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase **Supersedes:** [ADR-001](001_ontology_of_views_postprocessing.md) --- diff --git a/docs/ADRs/013_sampled_forecast_wire_contract.md b/docs/ADRs/013_sampled_forecast_wire_contract.md index 09e097d..6510bc8 100644 --- a/docs/ADRs/013_sampled_forecast_wire_contract.md +++ b/docs/ADRs/013_sampled_forecast_wire_contract.md @@ -2,7 +2,7 @@ **Status:** Accepted **Date:** 2026-07-15 -**Deciders:** Project maintainer (PRIO MD&D Team) — explicit sign-off, views-models#149 +**Decider:** Simon Polichinel von der Maase — explicit sign-off, views-models#149 **Supersedes:** the "platform ADR-046" that platform issues cited as the format authority — no such *format* document ever existed (a pipeline-core ADR-046 exists but covers storage infrastructure only; see Erratum E2 in the Post-adoption record) — and the v1 proposal comment on views-models#149 (2026-07-02). --- diff --git a/docs/ADRs/014_claims_and_the_guards_that_carry_them.md b/docs/ADRs/014_claims_and_the_guards_that_carry_them.md index 547c9f3..afddd36 100644 --- a/docs/ADRs/014_claims_and_the_guards_that_carry_them.md +++ b/docs/ADRs/014_claims_and_the_guards_that_carry_them.md @@ -2,7 +2,7 @@ **Status:** Accepted **Date:** 2026-08-02 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase **Arises from:** epic [#181](https://github.com/views-platform/views-postprocessing/issues/181) **Related:** [ADR-003](003_authority_of_declarations_over_inference.md) (declarations over inference), [ADR-005](005_testing_as_mandatory_critical_infrastructure.md) (testing as diff --git a/docs/ADRs/015_the_pipeline_core_appwrite_import.md b/docs/ADRs/015_the_pipeline_core_appwrite_import.md index 27728c5..9648272 100644 --- a/docs/ADRs/015_the_pipeline_core_appwrite_import.md +++ b/docs/ADRs/015_the_pipeline_core_appwrite_import.md @@ -2,7 +2,7 @@ **Status:** Accepted **Date:** 2026-08-04 -**Deciders:** Project maintainers (PRIO MD&D Team) +**Decider:** Simon Polichinel von der Maase **Arises from:** issue [#146](https://github.com/views-platform/views-postprocessing/issues/146), recorded at the request of the þing-02 ratification **Related:** [ADR-002](002_topology_and_dependency_rules.md) (dependency direction), diff --git a/docs/ADRs/016_ci_read_access_to_private_siblings.md b/docs/ADRs/016_ci_read_access_to_private_siblings.md new file mode 100644 index 0000000..e45d947 --- /dev/null +++ b/docs/ADRs/016_ci_read_access_to_private_siblings.md @@ -0,0 +1,206 @@ +# ADR-016: Cross-repository checks in CI, and how siblings declare their visibility + +**Status:** Accepted +**Date:** 2026-08-10 +**Decider:** Simon Polichinel von der Maase +**Arises from:** register **C-46**, **C-57**, **C-81**, which all carried the same standing +residual: the cross-repository drift detectors ran only on a maintainer's laptop +**Related:** [ADR-003](003_authority_of_declarations_over_inference.md) (declarations over +inference), [ADR-014](014_claims_and_the_guards_that_carry_them.md) §2 (a guard nobody has +watched fail is decoration) and §4 (a deferral names a trigger and an owner) + +--- + +## Context + +### §1 Some of this repository's tests are about other repositories + +Most tests here check our own code. A few check something else: whether **claims this code +makes about other repositories are still true**. + +The clearest case is the shared Appwrite coordinate registry. Each partner's +`appwrite_env.py` declares, in two constants, which edition of that registry it was written +against. That is a claim about a *different repository*. A test opens that repository, reads +the real edition, and compares. + +This is not theoretical maintenance. On 2026-08-05 the check failed: the registry had moved +from v1.4.1 to v1.4.4 while nobody here was looking. Two days earlier, the same family of +checks fired **twice in one day**. A claim about another repository goes stale at *that* +repository's pace, and nothing in this repository's own code can notice. + +### §2 They only run where the other repository is on disk + +`tests/conftest.py::sibling_repo` resolves a sibling by a declared environment variable and +falls back to the conventional `../` directory. When neither resolves it returns +`None` and the test **skips** — correctly, because on a laptop a missing sibling is normal. + +A GitHub Actions job gets a checkout of **one** repository. So in CI they skipped, and +**continuous integration verified strictly less than a laptop did, precisely on the checks +that cross a repository boundary**. + +### §3 Why they stayed dark, which is the more interesting failure + +Four siblings, and until now the decision about which ones CI could fetch lived in a +**comment** in the workflow file. That comment said `views-appwrite` was private. + +It is not. It was made public on **2026-08-08** — a deliberate, recorded act in that +repository (`views-appwrite@9d80b75`, *"docs: record going public"*). The comment did not +change with it, so seven checks went on skipping in CI for no reason whatsoever. + +The first draft of this very document, written on 2026-08-09, proposed issuing a credential +to reach those seven — **one day after the fact that justified it had ceased to be true**. +That is not an embarrassing footnote to the decision below; it *is* the argument for it. A +fact about another repository, recorded in prose, with no date and nothing able to check it, +will be wrong and nobody will find out. + +--- + +## Decision + +### §4 Every sibling is declared, and the declaration separates fact from decision + +`tests/conftest.py` declares each sibling repository with four fields: + +| field | kind | meaning | +|---|---|---| +| `env` | declaration | the environment variable that overrides its location | +| `public` | **fact** | its visibility on GitHub — not ours to decide | +| `public_checked` | **fact** | the ISO date that visibility was last verified | +| `ci_checkout` | **decision** | whether CI fetches it | +| `note` | reasoning | why not, required whenever `ci_checkout` is false | + +Separating `public` from `ci_checkout` is the point. One is a fact about the world, the +other is a choice we make; §3 is what happens when a single sentence tries to be both. And +`public_checked` is not decoration — every measured claim in this repository carries a date, +and a bare boolean is a fact with no expiry. + +### §5 CI fetches exactly what the declaration says, and a test enforces the agreement + +`.github/workflows/run_pytest.yml` checks out every sibling declared `ci_checkout=True`, into +`_siblings/`, pointed at by the declared environment variable. + +`tests/test_ci_sibling_coverage.py` fails when the workflow and the declaration disagree **in +either direction** — a declared checkout that is missing, or a checkout nobody declared. Each +rule is a pure function of `(workflow, siblings)` so it can be run against a synthetic broken +world; a rule that can only be demonstrated by editing CI is one nobody ever watches fail. + +Today that means `views-appwrite` and `views-crafdapi` are fetched. `views-datafactory` is +public but is **not** fetched: its checks need raw GAUL parquet files that are not in its git +repository, so checking it out replaces an honest skip with a crash — measured, tried and +reverted. `views-faoapi` is the one genuinely private sibling. + +### §6 A missing sibling that CI declared must turn the build red + +Skipping is right on a laptop and wrong in CI once we have declared the repository should be +there. If a checkout silently fails, the checks skip, the build stays green, and we are back +to §2's blindness while looking fixed. + +The mechanism already existed: +`tests/test_env_declaration.py::test_no_sibling_override_points_at_a_missing_path` fails when +a declared variable is set but resolves to nothing. Because the workflow sets those variables +unconditionally, a failed checkout is a **red build**. Naming it here so nobody deletes it +believing it to be tidiness. + +That guard had a hole, closed in the same change: it treated a variable set to the **empty +string** as unset, which is exactly what a YAML interpolation resolving to nothing produces — +so the likeliest CI misconfiguration was the one case it could not see. (`Path("").exists()` +is `True`, so the obvious fix makes it worse.) + +### §7 `public` is verified by CI doing it, not by a test — and two rules keep that true + +No test in this suite touches the network. That is a deliberate convention, not an oversight: +what these tests locate is a working copy on disk. + +So `public` is verified by **the checkout itself**. The default `GITHUB_TOKEN` is scoped to +this repository, so a tokenless fetch of a sibling declared public *fails the build* if it is +actually private. + +Two rules exist solely to keep that argument load-bearing, and both look like fussiness until +you see what they protect: + +- a checkout of a **public** sibling must **not** pass a credential — add one to dodge a rate + limit and the field silently becomes an unchecked claim; +- no sibling checkout may carry `continue-on-error` — one key and a failed fetch stops + failing the build. + +**The reverse case is not detected.** A private sibling that quietly becomes public will go on +being declared private, and nobody here will notice. That is stated rather than papered over; +it costs a stale `note` and an unnecessary skip, not a wrong result. + +### §8 The credential for the one private sibling is deferred + +`views-faoapi` is private, and reaching it needs a credential. That is **not** done here. + +The cost/benefit is thin: it is **one** test. It is, admittedly, the most valuable single one +— it checks that our delivery is filed under the name the consumer actually filters for, and +that failure produces *no error anywhere*: the upload succeeds, storage is paid for, and the +consumer's endpoint is simply empty. But one test does not justify a credential tied to one +person, with an expiry somebody must track, while a better answer is pending. + +**The better answer being pursued:** asking FAO to consent to that repository being made +public, which removes the need entirely. An audit of its full history found no credentials of +any kind and no partner staff email addresses; it already carries an MIT licence. Two items +remain open — internal storage identifiers appear in 21 tracked files, and whether GAUL 2024's +terms permit redistribution. + +**Named trigger (ADR-014 §4).** Issue the credential when **either**: FAO declines, or a +**second** private sibling appears. **Owner:** the maintainer. Should it ever be issued, the +scope floor is one repository, read-only, with a deliberately chosen expiry — the default of +30 days would put this repository back in the dark within a month, with a green build +throughout. + +--- + +## Consequences + +**What this buys.** Seven cross-repository checks move from *"run when a maintainer happens to +run them"* to *"run on every change"*. Two of them fired in anger in the week before this was +written. One test also changes character: the scan refusing registry **values** in this public +repository's markdown now runs on every pull request rather than only on a maintainer's +machine — worth having, since README.md carried four such values once already. + +**What it costs.** CI now depends on two other repositories being fetchable, so an outage or a +visibility change there turns this repository's build red. That is the intended trade: a red +build is the honest signal, and §6 exists to make sure it is what happens. + +**Where this will go wrong first.** Somebody debugging a red CI adds `continue-on-error` to a +sibling checkout, or a `token:` to make a rate limit go away. Either quietly dismantles §7. +Both are rules in `test_ci_sibling_coverage.py` for that reason. + +**Second most likely.** A sibling checkout is added to a different workflow file and escapes a +rule scoped to `run_pytest.yml`. The declaration check scans every workflow, not one. + +--- + +## Alternatives considered + +**Leave it, and rely on running the suite by hand.** The status quo, and what registers C-46 +and C-57 carried as an open residual for weeks. It works exactly as well as one person's +habits, which is not a property a safety check should have. + +**Vendor the facts instead of reading them.** Copy the registry edition into this repository +and check the copy. Rejected outright: copies of that registry were the platform's original +failure, and the standing rule is that it is referenced by pinned URL and never copied. A test +reading a local copy compares a thing to itself. + +**Check `public` against the GitHub API from a test.** Rejected. It would be the only network +call in the suite, would need a credential to answer for private repositories — the very thing +under discussion — and would be flaky in exactly the conditions where a green build matters. +Verification-by-doing (§7) is weaker but honest, and its limits are written down. + +**Issue the credential now and fetch all four siblings.** Rejected on the arithmetic in §8: it +buys one test, and a decision that may make it unnecessary is outstanding. + +--- + +## Appendix — reproducing the measurement + +Counts drift as tests are added; the command does not. From the repository root: + +``` +pytest -q +VIEWS_APPWRITE=/nonexistent pytest -q -rs +``` + +The first, with the sibling repositories present, is the full suite. The second is what CI +would see without the checkout — and it must **fail**, not merely skip. That is §6. diff --git a/docs/ADRs/README.md b/docs/ADRs/README.md index 48d7ab1..d866caf 100644 --- a/docs/ADRs/README.md +++ b/docs/ADRs/README.md @@ -4,6 +4,12 @@ This repository uses Architectural Decision Records (ADRs) to govern structural, semantic, and operational behavior. +> **On the `Deciders` field (2026-08-10).** ADRs 000–015 originally recorded +> *"Project maintainers (PRIO MD&D Team)"*. That team no longer exists, and a decider +> field naming a body that cannot be asked anything is worse than useless to a future +> reader. All of them now name the sole decider, Simon Polichinel von der Maase. +> ADR-013's line keeps its original sign-off reference alongside the name. + ADRs are divided into two categories: 1. **Constitutional ADRs (000–009)** @@ -88,6 +94,16 @@ These ADRs form the architectural constitution of the repository. demand and supply — under which it is revisited. Arises from #146 and the þing-02 ratification, which asked that the reasoning live here rather than in an issue. +- **ADR-016** — Cross-Repository Checks in CI, and How Siblings Declare Their Visibility + A handful of tests here verify claims this repo makes about *other* repos — chiefly that the + coordinate-registry edition we pinned is the one that exists. They need the sibling on disk, + so they skipped in CI and ran only on a laptop. Each sibling is now declared with its + visibility, the date that was checked, and whether CI fetches it; a test fails when the + workflow and the declaration disagree either way. Written after the workflow's comment + asserted a repository was private for two days after it went public — which is the argument + for a dated declaration rather than prose. The credential for the one genuinely private + sibling is deferred with a named trigger. + ADRs numbered 010 and above define: - Domain-specific decisions diff --git a/poetry.lock b/poetry.lock index 42a6e0f..bbc9180 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2641,7 +2641,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -3785,4 +3785,4 @@ viz = ["matplotlib", "nc-time-axis", "seaborn"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.15" -content-hash = "a51d9be49b3be1984501750e17be52d4efb46d4cb72e2f1decaaf3bf999b9bf2" +content-hash = "302cbc3aa750f7dacffe848c6141e6ba542ce30f2f646d99a859409acfd23b49" diff --git a/pyproject.toml b/pyproject.toml index 19b8f99..d76c633 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,14 @@ pyarrow = ">=16.1.0,<17.0.0" pytest = ">=8.0,<10.0" # the suite runs on 9.x today; 8.x still works ruff = ">=0.6,<1.0" +# Declared for the same reason the pytest comment above exists. `tests/test_ci_sibling_ +# coverage.py` parses `.github/workflows/*.yml` to check that CI checks out exactly the +# siblings `tests/conftest.py::SIBLINGS` declares. pyyaml already arrives transitively +# (it is in the lock as a main-group dependency), and relying on that is precisely how +# this repository lost pytest. The alternative — `pytest.importorskip("yaml")` — would +# make the guard skip itself whenever the parser is absent, which is decoration. +pyyaml = ">=6.0,<7.0" + # Ruff's default rule selection is NOT stable across ruff versions, and inheriting a # moving default is the inference ADR-003 forbids applied to tooling. Measured # 2026-08-03: ruff 0.14.13 reported 0 findings on this tree; 0.16.1 reported **815**, diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index 0dcfc30..05a6236 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -4,7 +4,7 @@ |-------------------|--------------------------------------| | Project | views-postprocessing | | Owner | Dylan Pinheiro / PRIO MD&D Team | -| Last Updated | 2026-08-05 | +| Last Updated | 2026-08-10 | | Total Concerns | 85 | | Open Concerns | 12 | | Resolved Concerns | 73 | @@ -114,7 +114,7 @@ Plus, outside the register: #158's rename finished and the **broken URL it creat **What the epic did NOT do, stated because a closeout that reports only successes is the defect this epic exists to fix:** 1. **Cluster M is untouched and correctly so.** Six entries, two of them Tier 2, all resolving on one upstream publish. No engineering here moves them. -2. **The CI question is decided in writing but not implemented.** Three gated cross-repo checks run nowhere automatic. C-46's residual carries the argued recommendation — *do not couple per-PR CI to another repo's default branch; if wanted, a weekly scheduled check that opens an issue on divergence* — with a named trigger. **It is a decision awaiting an owner, not a task awaiting effort.** +2. **The CI question is decided in writing but not implemented.** Three gated cross-repo checks run nowhere automatic. *(Largely implemented 2026-08-10 — ADR-016. Two of the three groups now run in CI; the datafactory group still cannot, because it needs raw data absent from that repository's git, which no CI change can supply.)* C-46's residual carries the argued recommendation — *do not couple per-PR CI to another repo's default branch; if wanted, a weekly scheduled check that opens an issue on divergence* — with a named trigger. **It is a decision awaiting an owner, not a task awaiting effort.** 3. **Withdrawal of a bad delivery is the chosen policy and is not built.** Supersession is in force because it is what the wire does. Deliberately not started: it needs an ADR-013 amendment plus views-faoapi work, and FAO's answer on audit requirements (Pre-Release Note 07, B.2) decides whether it is wanted at all. 4. **Two questions are with the UN FAO**, not with us — recipients and notification timing (B.1), withdrawal versus supersession (B.2). 5. **`test_datafactory_deploy_readiness`'s `xfail` tuning was left alone**, deliberately: S7 fixed how the checkout is found, not what the gate asserts. If it needs re-pinning now that datafactory has moved past `v1.4.0`, that is a separate judgement. @@ -519,13 +519,27 @@ Nine of the seventeen are **new in this arc**, including both registry-drift det Where each sibling stands, after trying them: - **views-crafdapi** — public, its check reads source text. **Now checked out in CI**, recovering **one** test: the cross-seam consumer-document-name pin for CRAF'd. - **views-datafactory** — public, but its eight tests need the producer's raw GAUL parquets, which are **not in its git repository**. Checking it out converts an honest skip into a `FileNotFoundError`; tried and reverted. -- **views-appwrite**, **views-faoapi** — **private**. The most valuable checks live here. Closing this needs a token in CI. +- **views-appwrite** — was private when this was written; **made public 2026-08-08** (`views-appwrite@9d80b75`) and **now checked out in CI**, recovering **seven** tests including both registry-drift detectors. No credential was needed for any of them, and none was ever the obstacle after 2026-08-08 — the obstacle was that this line went on saying "private" for two days after it stopped being true. +- **views-faoapi** — **private**, and the only one. Its single check is dark. Closing it needs either a credential or FAO's consent to make that repository public; the second is being pursued, and ADR-016 §8 carries the trigger for falling back to the first. **Enforcement.** `main` is **not branch-protected**: `gh api .../branches/main/protection` returns `404 Branch not protected`, and `gh api .../rules/branches/main` returns `[]`. The `protect_main` ruleset exists and is `active`, but its `ref_name` include-list is **empty**, so it matches nothing — and it declares no `required_status_checks` rule in any case. **A red `Run Pytest` would not block a merge to `main`.** This repository's own `tests/test_falsification_campaign_4_1.py` carries the question as an unverifiable xfail probe; it is verifiable through the API, and the answer is no. The two compound: a suite that checks less than you think, and no requirement that even that much passes. Neither is caused by this sync — both are pre-existing — but this sync is the first time `main` receives an epic whose value is largely the guards themselves. Cross-refs: **C-46** and **C-57** (both RESOLVED; this is the residual each recorded as *"a CI-cost and cross-repo-coupling decision"* and *"worth deciding once for both"* — it now has a live home and a concrete answer per sibling), **C-80** (the other verification gap found in the same audit), #188. +**Partial mitigation 2026-08-10 (ADR-016) — the coverage half is mostly closed; the enforcement half is untouched.** + +The coverage half rested on a claim nobody could check. This entry, and the workflow comment it drew on, said `views-appwrite` was **private**, so its seven checks needed a credential. It went public on **2026-08-08** (`views-appwrite@9d80b75`, a deliberate and recorded act), and the claim here went on being made for two days afterwards. No credential was required, and none had been the obstacle since that date. + +CI now checks that repository out and those seven run on every pull request — including both registry-drift detectors, which is what this entry called *"the most valuable of the lot"*. One test also changes character rather than merely un-skipping: the scan refusing registry **values** in this public repository's markdown now runs on the merge rather than only on a maintainer's machine. + +**What remains, and it is two different things:** + +1. **One dark check.** `views-faoapi` is genuinely private — the consumer-name pin is still laptop-only. That is one test, not seven, and it is the one whose failure mode is invisible rather than loud. ADR-016 §8 defers the credential and names the trigger: FAO declining the request to make that repository public, or a second private sibling appearing. +2. **The enforcement half is entirely untouched.** `protect_main`'s ref-name include-list was empty; it now targets the default branch, but **no status check is required**, so a pull request with a red CI can still be merged to `main`. Since merging to `main` *is* the production release, this is the half that matters most and the half that has not moved. + +**The lesson this entry should carry.** The blocker was not a missing credential. It was a fact about another repository recorded in prose, with no date, that nothing could check — and it survived a console session, an ADR draft and a register entry, all of which repeated it. ADR-016 replaces the prose with a declaration carrying the date it was verified, and a test that fails when CI and the declaration disagree. + **Update 2026-08-05 — the operator session happened, and neither half of this entry moved.** Simon read the Appwrite console that morning (views-appwrite v1.4.4). It answered two *other* þing-02 questions definitively — there is **no non-production project**, and both platform keys expire 2026-11-17 (now **C-84**) — but the console read is a different action from issuing a token and a different console from GitHub's. So both halves stand: the two private siblings still have no CI credential, and `protect_main`'s ref-name include-list is still empty. Recorded rather than left implicit because "the operator did a console session" is exactly the kind of adjacent fact that gets mistaken for progress on this entry. It is not. What it does establish is that the session is a thing that happens, and these two items are small enough to ride along with the next one. @@ -1289,6 +1303,10 @@ Verified 2026-08-02: `grep -rn "/home/" tests/ scripts/ views_postprocessing/ -- | Trigger | When treating `test_datafactory_deploy_readiness` as a release gate (it never runs in CI), or when a contributor's local `pytest` fails on it — re-promote / re-pin the strict-xfail now that views-datafactory has advanced to `1.5.0`-dev past its `v1.4.0` tag | | Location | `tests/test_datafactory_deploy_readiness.py` (`_DF = Path("/home/simon/.../views-datafactory")`, `skipif(not _DF.exists())`) | +**Overridden 2026-08-10 (ADR-016), and the objection was designed around rather than dismissed.** Sibling checkouts *were* added to the per-PR workflow. The recommendation's argument was specific — *"it couples this repo's CI to another repo's **default branch**, so an unrelated upstream commit turns this repo red"* — and every sibling checkout declares **`ref: main`** for exactly that reason. A commit on someone's feature branch, or on a default branch that is not `main` (views-appwrite's default is `development`), cannot reach us. `test_ci_sibling_coverage.py` makes `ref: main` a rule rather than a habit. + +**What is genuinely accepted, and should not be glossed:** a change merged to a sibling's `main` — a registry edition bump, say — *can* turn this repository red and block merges here until someone re-pins. That is not a defect being tolerated; it is the drift detector working, and the alternative is the state this entry was open about, where the drift was noticed only when a maintainer happened to run the suite. The cost is real and the trade is deliberate. + The cross-repo deploy-readiness gates introduced under C-36 are guarded by `skipif` on a **hardcoded local datafactory checkout path**, so they are **skipped in CI** and only ever execute on one developer's machine. There, `test_version_bumped_past_latest_tag` is currently **failing**: it is an `xfail(strict)` that flipped to XPASS because datafactory moved to `1.5.0`-dev past its `v1.4.0` tag — exactly the auto-flip C-36's resolution anticipated, but because of the hardcoded path the flip surfaces as a **local red** rather than a CI signal, and breaks local `pytest` runs (the suite is run with this test deselected). No correctness/reliability impact on the delivery → **Tier 4** (test hygiene). C-36 (resolved) converted these gates to strict-xfail but did not capture the local-path / CI-skip dimension. See also C-36 (the resolved strict-xfail conversion this extends), C-44 (the datafactory version-state coupling). @@ -1326,7 +1344,7 @@ The right axis was **exact equality on string constants**, not statement shape. **Gated, and honestly so.** The checks need a views-appwrite checkout and skip without one, naming `VIEWS_APPWRITE` and the conventional sibling path so a contributor can run them rather than merely watch them skip. The would-catch-a-rename proof runs in CI with no checkout at all. Resolution helper shared with **C-46** (S7) in `tests/conftest.py` — the second incident, which is this repo's named trigger for extracting. -**Residual — now tracked as C-81.** The gated half does not run in CI, which needs a views-appwrite checkout in the workflow. That was recorded here and in **C-46** as *"a CI-cost and cross-repo-coupling decision, not a code fix … worth deciding once for both"*, and it sat as a residual on two RESOLVED entries, which is where residuals go to be forgotten. It now has a live entry with a measured cost (17 tests, 9 of them new in this arc), a per-sibling answer, and an owner: **C-81**. views-appwrite is private, so it needs a token — an operator decision. +**Residual — RESOLVED 2026-08-10 (ADR-016).** The gated half did not run in CI, which needed a views-appwrite checkout in the workflow. It has one: that repository went public on 2026-08-08 and the workflow now fetches it, so these checks run on every pull request. That was recorded here and in **C-46** as *"a CI-cost and cross-repo-coupling decision, not a code fix … worth deciding once for both"*, and it sat as a residual on two RESOLVED entries, which is where residuals go to be forgotten. It now has a live entry with a measured cost (17 tests, 9 of them new in this arc), a per-sibling answer, and an owner: **C-81**. views-appwrite is private, so it needs a token — an operator decision. A second, smaller instance of the same shape: these checks parse TOML with `tomllib`, stdlib from Python 3.11, and `pyproject` declares `>=3.11`. CI runs 3.11 and executes them. The maintainer's box runs **3.10**, below the declared floor, so they skip there — the local suite is quietly weaker than a green `pytest -q` suggests. Not a repo defect and not worth its own entry; recorded because "a gate that does not run" is exactly what C-46 is open for, and the CI decision should cover both. | | Tier | 3 | diff --git a/tests/conftest.py b/tests/conftest.py index 78493ac..f9ec51b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,6 +26,7 @@ import os import subprocess +from dataclasses import dataclass from pathlib import Path import pytest @@ -60,15 +61,94 @@ #: either a partner or machinery, and the same test refuses to let it be neither. MACHINERY_PACKAGES = ("contract", "delivery") -#: repo name -> the environment variable that overrides its location. -#: Declared, never derived from the name: ``views-datafactory`` → ``VIEWS_DATAFACTORY`` -#: happens to be mechanical, but a future sibling need not follow the pattern and -#: guessing it would be the inference ADR-003 forbids. -SIBLING_ENV = { - "views-datafactory": "VIEWS_DATAFACTORY", - "views-appwrite": "VIEWS_APPWRITE", - "views-faoapi": "VIEWS_FAOAPI", - "views-crafdapi": "VIEWS_CRAFDAPI", +@dataclass(frozen=True, kw_only=True) +class Sibling: + """What this repository declares about one views-platform sibling (ADR-016). + + Two of these fields are different kinds of thing and the distinction is the point. + ``public`` is a **fact about the world** that this repository does not control. + ``ci_checkout`` is a **decision** this repository makes. Conflating them is how the + workflow ended up asserting, in a comment, that ``views-appwrite`` was private for + days after it went public — and how seven checks stayed dark in CI for no reason. + + ``public_checked`` is the date the fact was last verified, and it is not decoration: + every measured claim in this repository carries one. A bare boolean is a fact with no + expiry, which is precisely what went wrong. + + **Keyword-only and frozen, deliberately.** Two adjacent booleans are a one-token slip + between "public, not checked out" and "private, checked out" — the second being the + combination rule G3 exists to forbid. A plain dict would let a missing ``ci_checkout`` + read as ``None``, silently exempting that sibling from every guard; that is the + failure mode this repository has registered more often than any other (C-47, C-57, + #211). Here the omission is a ``TypeError`` at import. + + **No validation in ``__post_init__``.** See ``broken_sibling_overrides`` below for + what raising at import time costs: one typo became three collection errors and zero + tests run. The rules live in ``tests/test_ci_sibling_coverage.py``, where a violation + is one clean failure and the other four hundred tests still report. + """ + + #: The environment variable that overrides this sibling's location. Declared, never + #: derived: ``views-datafactory`` → ``VIEWS_DATAFACTORY`` happens to be mechanical, + #: but a future sibling need not follow the pattern and guessing it would be the + #: inference ADR-003 forbids. + env: str + #: Visibility on GitHub — a fact about the world, not a decision of ours. + public: bool + #: ISO date ``public`` was last verified. See the class docstring. + public_checked: str + #: Whether CI checks this sibling out. A decision, and the reason for it belongs in + #: ``note`` whenever the answer is no. + ci_checkout: bool + #: Why this sibling is not checked out. Required when ``ci_checkout`` is False, and + #: must name a record, so the non-coverage has an owner rather than a shrug. + note: str = "" + + +#: The views-platform repositories whose current state this repository's tests read. +#: +#: `views-appwrite` was private until **2026-08-08**, when it was deliberately made +#: public (`views-appwrite@9d80b75`, "docs: record going public"). The workflow comment +#: here went on saying PRIVATE afterwards, which is the whole argument for declaring the +#: fact with a date instead of narrating it in prose. +SIBLINGS = { + "views-datafactory": Sibling( + env="VIEWS_DATAFACTORY", + public=True, + public_checked="2026-08-10", + ci_checkout=False, + note=( + "public, but its checks need the producer's raw GAUL parquets " + "(data/raw/gaul_admin/*.parquet), which are NOT in its git repository. " + "Checking it out converts an honest skip into a FileNotFoundError — measured " + "2026-08-03, tried and reverted. Closing this needs the data published " + "somewhere fetchable, not an access grant. Register C-46." + ), + ), + "views-appwrite": Sibling( + env="VIEWS_APPWRITE", + public=True, + public_checked="2026-08-10", + ci_checkout=True, + ), + "views-faoapi": Sibling( + env="VIEWS_FAOAPI", + public=False, + public_checked="2026-08-10", + ci_checkout=False, + note=( + "the only private sibling. Checking it out needs a credential, which is an " + "operator decision deferred pending a request to FAO to make the repository " + "public. One check is dark meanwhile — the consumer-name pin, whose failure " + "mode is a delivery nobody can find. See ADR-016 and register C-81." + ), + ), + "views-crafdapi": Sibling( + env="VIEWS_CRAFDAPI", + public=True, + public_checked="2026-08-10", + ci_checkout=True, + ), } #: partner package -> the repository that CONSUMES its delivery. @@ -97,12 +177,12 @@ def sibling_repo(name: str) -> Path | None: Returns ``None`` rather than raising so callers can skip; a missing sibling is a normal condition in CI, where only this repo is checked out. """ - if name not in SIBLING_ENV: + if name not in SIBLINGS: raise KeyError( f"no environment variable declared for sibling {name!r}; add it to " - f"SIBLING_ENV rather than guessing one from the name" + f"SIBLINGS rather than guessing one from the name" ) - override = os.environ.get(SIBLING_ENV[name]) + override = os.environ.get(SIBLINGS[name].env) candidate = Path(override) if override else _REPO.parent / name return candidate if candidate.exists() else None @@ -122,12 +202,24 @@ def broken_sibling_overrides() -> dict[str, str]: ``Interrupted: 3 errors during collection`` and **zero tests run** — trading silent under-coverage for total loss of the suite. One clean failure says the same thing and lets the other 360 tests report. + + **An EMPTY variable is the case this missed, and it is the one CI produces.** The + filter was ``if value and ...``, so ``VIEWS_APPWRITE=""`` read as unset: + ``sibling_repo`` fell through to the conventional ``../views-appwrite``, which does + not exist in a CI workspace, and seven checks skipped on a green build. A YAML + interpolation that resolves to nothing — ``${{ env.TYPO }}`` — produces exactly that + empty string, so this is the *likely* misconfiguration in CI, not an exotic one. + + ``.strip()`` and not merely ``if value is not None`` because **``Path("").exists()`` + is ``True``** — it resolves to the current directory. Dropping the truthiness test + without the strip would report an empty override as a perfectly good checkout, which + is worse than the bug being fixed. """ return { var: value - for var in SIBLING_ENV.values() + for var in (s.env for s in SIBLINGS.values()) for value in [os.environ.get(var)] - if value and not Path(value).exists() + if value is not None and (not value.strip() or not Path(value).exists()) } @@ -141,7 +233,7 @@ def require_sibling(name: str) -> Path: path = sibling_repo(name) if path is None: pytest.skip( - f"{name} checkout not found — set {SIBLING_ENV[name]}=/path/to/{name}, " + f"{name} checkout not found — set {SIBLINGS[name].env}=/path/to/{name}, " f"or place it alongside this repo at {(_REPO.parent / name)}" ) return path diff --git a/tests/test_ci_sibling_coverage.py b/tests/test_ci_sibling_coverage.py new file mode 100644 index 0000000..489e72f --- /dev/null +++ b/tests/test_ci_sibling_coverage.py @@ -0,0 +1,511 @@ +"""The workflow and the sibling declaration must agree — ADR-016. + +**The failure this exists to prevent has already happened.** A comment in +`run_pytest.yml` stated that `views-appwrite` was private. It went public on 2026-08-08 +and the comment did not, so seven cross-repository checks stayed dark in CI for no reason +at all — and the document written to justify a credential for them was drafted the next +day, against a fact that had already changed. + +The fix is not a better comment. It is that **which siblings CI checks out is declared +once**, in `tests/conftest.py::SIBLINGS`, and this file fails when the workflow and that +declaration disagree — in either direction. + +**What verifies the `public` field, since no test here does.** Nothing in this suite +touches the network; that is a hard convention (`conftest`'s docstring: what is located +is a working copy on disk). `public` is verified by CI *doing it*: the default +`GITHUB_TOKEN` is scoped to this repository, so a tokenless checkout of a sibling +declared public fails the build if it is actually private. Rules G3 and G5 exist to keep +that true — one `token:` or one `continue-on-error:` and the claim silently stops being +checked. The reverse case (a private sibling quietly becoming public) is **not** detected +here, and ADR-016 §7 says so rather than implying coverage it does not have. + +**Every rule is a pure function of (workflow, siblings)**, so each can be run against a +synthetic mutant rather than only against the real file. A guard that can only be +demonstrated by editing CI is a guard nobody ever watches fail (ADR-014 §2) — this repo +already answers that three times over (`test_the_drift_check_would_catch_a_rename`, +`test_the_guard_would_actually_catch_a_violation`, `test_the_ban_actually_fires_on_a_living_doc`). +""" + +from __future__ import annotations + +import datetime as dt +from pathlib import Path + +import pytest +import yaml + +from tests.conftest import SIBLINGS, Sibling + +_WORKFLOWS = Path(__file__).resolve().parent.parent / ".github" / "workflows" +_TEST_WORKFLOW = _WORKFLOWS / "run_pytest.yml" + +#: Where a sibling checkout must land. Not tidiness: `pyproject.toml` excludes +#: `_siblings` from ruff, and the Lint step reported 745 findings in views-crafdapi's own +#: test suite the first time a sibling was checked out anywhere else. +_SIBLING_PATH_PREFIX = "_siblings/" + +#: The org every sibling belongs to. A `repository:` outside it is not a sibling. +_ORG = "views-platform/" + + +# ── parsing ────────────────────────────────────────────────────────────────── + + +def _load(path: Path) -> dict: + return yaml.safe_load(path.read_text()) or {} + + +def _steps(workflow: dict) -> list[dict]: + """Every step of every job, flattened.""" + return [ + step + for job in (workflow.get("jobs") or {}).values() + for step in (job.get("steps") or []) + if isinstance(step, dict) + ] + + +def _sibling_steps(workflow: dict) -> list[dict]: + """Checkout steps that fetch ANOTHER repository. + + Keyed on the presence of ``repository:``, not on ``uses: actions/checkout``. This + repo's own checkout is an `actions/checkout` step with no ``repository:``, so a rule + scoped to the action would fire on line one and be deleted the same day (ADR-014 §3). + """ + return [ + step + for step in _steps(workflow) + if str(step.get("uses", "")).startswith("actions/checkout") + and isinstance(step.get("with"), dict) + and step["with"].get("repository") + ] + + +def _repo_name(step: dict) -> str: + """``views-platform/views-appwrite`` -> ``views-appwrite``.""" + return str(step["with"]["repository"]).removeprefix(_ORG) + + +def _pytest_step(workflow: dict) -> dict | None: + """The step that actually runs the suite — where the env vars must land. + + Found by what it runs, not by its name. An `env:` block on the Lint step satisfies a + naive "the variable is set somewhere" check while the tests see nothing. + """ + for step in _steps(workflow): + if "pytest" in str(step.get("run", "")): + return step + return None + + +# ── rules: pure functions, each returning the violations it found ──────────── + + +def _g1_declared_checkouts_are_present_and_pointed_at( + workflow: dict, siblings: dict[str, Sibling] +) -> list[str]: + """Every ``ci_checkout=True`` sibling is checked out AND its env var points at it.""" + by_name = {_repo_name(s): s for s in _sibling_steps(workflow)} + pytest_step = _pytest_step(workflow) + env = (pytest_step or {}).get("env") or {} + + problems = [] + for name, sibling in siblings.items(): + if not sibling.ci_checkout: + continue + step = by_name.get(name) + if step is None: + problems.append(f"{name}: declared ci_checkout=True but no checkout step") + continue + expected = "${{ github.workspace }}/" + str(step["with"].get("path", "")) + actual = env.get(sibling.env) + if actual is None: + problems.append( + f"{name}: checked out, but {sibling.env} is not set on the step that " + "runs pytest — the tests would look in the conventional location, find " + "nothing, and skip" + ) + elif str(actual).strip() != expected: + problems.append( + f"{name}: {sibling.env}={actual!r} does not match the checkout path " + f"({expected!r})" + ) + return problems + + +def _g2_every_checkout_is_declared( + workflow: dict, siblings: dict[str, Sibling] +) -> list[str]: + """No sibling is fetched in CI without being declared with ``ci_checkout=True``.""" + problems = [] + for step in _sibling_steps(workflow): + name = _repo_name(step) + sibling = siblings.get(name) + if sibling is None: + problems.append( + f"{name}: checked out in CI but absent from SIBLINGS. A sibling nobody " + "declared is one no rule below covers." + ) + elif not sibling.ci_checkout: + problems.append( + f"{name}: checked out in CI but declared ci_checkout=False. One of the " + "two is wrong, and the declaration is what the reader believes." + ) + return problems + + +def _g3_tokens_match_visibility( + workflow: dict, siblings: dict[str, Sibling] +) -> list[str]: + """Private ⇒ must pass a token. Public ⇒ must NOT. + + The second half is the one that matters and the one that looks pointless. It is what + keeps ``public`` verified-by-doing: a tokenless checkout of a private repo fails the + build. Add a token to a public sibling — to dodge a rate limit, say — and the field + becomes an unchecked claim with nothing anywhere to catch it. + """ + problems = [] + for step in _sibling_steps(workflow): + name = _repo_name(step) + sibling = siblings.get(name) + if sibling is None: + continue # G2's business + has_token = "token" in step["with"] or "ssh-key" in step["with"] + if not sibling.public and not has_token: + problems.append( + f"{name}: declared private but checked out with no credential — the " + "step will fail. Either it is public now (update the declaration and its " + "date) or it needs a token." + ) + if sibling.public and has_token: + problems.append( + f"{name}: declared public but the checkout passes a credential. That " + "removes the only thing verifying the `public` field — a tokenless " + "checkout failing when a repo is private. Drop the token, or the claim " + "is unverified." + ) + return problems + + +def _g4_non_coverage_is_explained( + workflow: dict, siblings: dict[str, Sibling] +) -> list[str]: + """Every sibling NOT checked out says why, and names a record. + + A deferral needs a trigger and an owner (ADR-014 §4). Requiring the note to cite a + `C-nn` or an `ADR-nnn` is what makes the citation non-optional, so declared + non-coverage stays attached to something a reader can follow. + """ + problems = [] + for name, sibling in siblings.items(): + if sibling.ci_checkout: + continue + if not sibling.note.strip(): + problems.append( + f"{name}: not checked out in CI and no note says why. Silent " + "non-coverage reads as 'nothing to see here'." + ) + elif not any( + token in sibling.note for token in ("C-", "ADR-") + ): + problems.append( + f"{name}: its note explains the non-coverage but names no record. Cite " + "the register entry or ADR that owns it, so the deferral has an owner." + ) + return problems + + +def _g5_no_step_swallows_its_own_failure( + workflow: dict, siblings: dict[str, Sibling] +) -> list[str]: + """No sibling checkout may carry ``continue-on-error``. + + One key, and the entire verification-by-doing argument becomes false: the checkout of + a repo that turned private fails, the build stays green, and the checks it enables + skip. It is also exactly the key someone reaches for while debugging a red CI. + """ + return [ + f"{_repo_name(step)}: sibling checkout carries continue-on-error, so a failed " + "fetch would not fail the build — and the checks it enables would skip silently" + for step in _sibling_steps(workflow) + if step.get("continue-on-error") + ] + + +def _g6_siblings_land_under_the_excluded_path( + workflow: dict, siblings: dict[str, Sibling] +) -> list[str]: + """Sibling checkouts go under ``_siblings/`` — a real coupling to the ruff config.""" + return [ + f"{_repo_name(step)}: checked out to {step['with'].get('path')!r}, outside " + f"{_SIBLING_PATH_PREFIX!r}. pyproject.toml excludes only that prefix from ruff, " + "so the Lint step would start reporting another repository's code (it once " + "reported 745 findings that way)." + for step in _sibling_steps(workflow) + if not str(step["with"].get("path", "")).startswith(_SIBLING_PATH_PREFIX) + ] + + +def _g7_siblings_are_taken_from_main( + workflow: dict, siblings: dict[str, Sibling] +) -> list[str]: + """Every sibling checkout declares ``ref: main``. + + Without it `actions/checkout` takes the sibling's own default branch — which for + views-appwrite is `development`. ADR-014 §3 makes `main` the authority for a claim + about another repository, and `test_the_pinned_commit_is_reachable_from_the_contract_repos_main` + already enforces that. Reading the working tree from `development` while demanding + reachability from `main` is two guards in one file asking for different things. + """ + return [ + f"{_repo_name(step)}: checkout does not declare `ref: main`, so it takes that " + "repository's default branch — someone else's setting, and not the branch this " + "repo treats as authoritative." + for step in _sibling_steps(workflow) + if str(step["with"].get("ref", "")) != "main" + ] + + +def _g8_the_visibility_fact_carries_a_usable_date( + workflow: dict, siblings: dict[str, Sibling] +) -> list[str]: + """``public_checked`` parses, and is not in the future.""" + problems = [] + today = dt.date.today() + for name, sibling in siblings.items(): + try: + checked = dt.date.fromisoformat(sibling.public_checked) + except ValueError: + problems.append( + f"{name}: public_checked={sibling.public_checked!r} is not an ISO date. " + "A fact with an unreadable date is a fact with no date." + ) + continue + if checked > today: + problems.append( + f"{name}: public_checked={sibling.public_checked} is in the future." + ) + return problems + + +_RULES = { + "G1 declared checkouts are present and pointed at": _g1_declared_checkouts_are_present_and_pointed_at, + "G2 every checkout is declared": _g2_every_checkout_is_declared, + "G3 tokens match visibility": _g3_tokens_match_visibility, + "G4 non-coverage is explained": _g4_non_coverage_is_explained, + "G5 no step swallows its own failure": _g5_no_step_swallows_its_own_failure, + "G6 siblings land under the excluded path": _g6_siblings_land_under_the_excluded_path, + "G7 siblings are taken from main": _g7_siblings_are_taken_from_main, + "G8 the visibility fact carries a usable date": _g8_the_visibility_fact_carries_a_usable_date, +} + + +# ── the inputs are real (ADR-014 §2) ───────────────────────────────────────── + + +def test_the_workflow_this_file_scans_actually_parses(): + """A YAML restructure would empty every scan below and report success. + + This is by far the most likely way this file becomes decoration: rename the job, nest + `steps` differently, move the workflow, and eight rules find nothing to complain + about. So assert there is something to scan before trusting that there was nothing + wrong. + """ + assert _TEST_WORKFLOW.exists(), f"no workflow at {_TEST_WORKFLOW}" + workflow = _load(_TEST_WORKFLOW) + assert _steps(workflow), ( + "no steps parsed out of run_pytest.yml — the job or step structure moved, and " + "every rule in this file is now scanning an empty list." + ) + assert _pytest_step(workflow) is not None, ( + "no step in run_pytest.yml runs pytest. G1 checks the env vars on that step; " + "without it, it checks nothing." + ) + found = {_repo_name(s) for s in _sibling_steps(workflow)} + assert found, ( + "no sibling checkout steps found. Either they were all removed — in which case " + "the cross-repo checks are dark again — or the detection predicate no longer " + "matches how they are written." + ) + + +def test_every_workflow_file_is_scanned_for_smuggled_siblings(): + """G2 must see ALL workflows, not just this one. + + A sibling checkout added to `codeql.yml` would be invisible to a rule scoped to + `run_pytest.yml`. Nothing forbids one being added there; this makes it visible. + """ + workflows = sorted(_WORKFLOWS.glob("*.yml")) + assert workflows, ( + f"no workflow files under {_WORKFLOWS} — a glob over a moved directory finds " + "nothing and reports success (register C-74)." + ) + stray = [] + for path in workflows: + stray += [ + f"{path.name}: {_repo_name(step)}" + for step in _sibling_steps(_load(path)) + if _repo_name(step) not in SIBLINGS + ] + assert not stray, ( + f"sibling checkouts in workflow files that SIBLINGS does not declare: {stray}." + ) + + +def test_the_declared_siblings_are_the_ones_the_suite_resolves(): + """`SIBLINGS` is what every gated check resolves through; assert it is not empty.""" + assert SIBLINGS, "SIBLINGS is empty — every cross-repo check now skips permanently." + assert all(s.env.startswith("VIEWS_") for s in SIBLINGS.values()), ( + "a declared override variable does not follow the VIEWS_* convention; " + f"{ {n: s.env for n, s in SIBLINGS.items()} }" + ) + + +# ── the rules, against the real workflow ───────────────────────────────────── + + +@pytest.mark.parametrize("label", sorted(_RULES)) +def test_the_workflow_agrees_with_the_declaration(label): + """Each rule, run against the file that actually ships.""" + violations = _RULES[label](_load(_TEST_WORKFLOW), SIBLINGS) + assert not violations, f"[{label}]\n " + "\n ".join(violations) + + +# ── the rules, against synthetic mutants (ADR-014 §2) ──────────────────────── +# +# A rule that has only ever been run against a correct file has never been watched fail. +# Each case below is a minimal broken world; the rule must object AND name the sibling, +# because a violation nobody can locate is barely better than none. + + +def _workflow(**step_overrides) -> dict: + """A minimal well-formed workflow with one sibling checkout, then mutated.""" + with_block = { + "repository": "views-platform/views-appwrite", + "ref": "main", + "path": "_siblings/views-appwrite", + } + with_block.update(step_overrides.pop("with", {})) + step = {"uses": "actions/checkout@v3", "with": with_block} + step.update(step_overrides) + return { + "jobs": { + "test": { + "steps": [ + {"uses": "actions/checkout@v3"}, # this repo's own — no `repository:` + step, + { + "run": "poetry run pytest tests/", + "env": { + "VIEWS_APPWRITE": "${{ github.workspace }}/_siblings/views-appwrite" + }, + }, + ] + } + } + } + + +_ONE = {"views-appwrite": SIBLINGS["views-appwrite"]} + + +def _replace(name: str, **changes) -> dict[str, Sibling]: + from dataclasses import replace + + return {name: replace(SIBLINGS[name], **changes)} + + +_MUTANTS = [ + ("G1 declared checkouts are present and pointed at", + {"jobs": {"test": {"steps": [{"run": "poetry run pytest tests/", "env": {}}]}}}, + _ONE, "declared ci_checkout=True but the checkout step is gone"), + ("G2 every checkout is declared", _workflow(), {}, "checked out but undeclared"), + ("G3 tokens match visibility", + _workflow(**{"with": {"token": "${{ secrets.X }}"}}), _ONE, + "public sibling fetched with a credential — kills verification-by-doing"), + ("G4 non-coverage is explained", + _workflow(), _replace("views-appwrite", ci_checkout=False, note=""), + "not checked out and no note"), + ("G4 non-coverage is explained", + _workflow(), _replace("views-appwrite", ci_checkout=False, note="because reasons"), + "note explains but names no record"), + ("G5 no step swallows its own failure", + _workflow(**{"continue-on-error": True}), _ONE, + "a failed fetch would not fail the build"), + ("G6 siblings land under the excluded path", + _workflow(**{"with": {"path": "vendor/views-appwrite"}}), _ONE, + "outside _siblings/, so ruff would lint it"), + ("G7 siblings are taken from main", + _workflow(**{"with": {"ref": "development"}}), _ONE, + "takes a branch that is not the authority"), + ("G8 the visibility fact carries a usable date", + _workflow(), _replace("views-appwrite", public_checked="2099-01-01"), + "the fact was checked in the future"), + ("G8 the visibility fact carries a usable date", + _workflow(), _replace("views-appwrite", public_checked="last tuesday"), + "unparseable date"), +] + + +def test_every_rule_has_a_mutant_that_proves_it_bites(): + """ADR-014 §2, applied to this file's own contents. + + Adding a rule to `_RULES` without a broken world to try it against is adding a rule + nobody has watched fail. That is the definition of decoration, and it is easy to do + by accident — the rule reads fine, the real workflow satisfies it, and the suite goes + green forever. + + This is a cheap structural check, not a clever one: it cannot tell whether a mutant + is a *good* one. It only refuses the case where there is none at all. + """ + proven = {label for label, *_ in _MUTANTS} + unproven = sorted(set(_RULES) - proven) + assert not unproven, ( + f"rules with no mutant in _MUTANTS: {unproven}. Add a broken world that each one " + "must object to, or the rule is untested against anything but a correct file." + ) + + +@pytest.mark.parametrize("label, workflow, siblings, why", _MUTANTS) +def test_each_rule_bites_on_a_broken_world(label, workflow, siblings, why): + violations = _RULES[label](workflow, siblings) + assert violations, f"[{label}] did not object to: {why}" + assert any("views-appwrite" in v for v in violations), ( + f"[{label}] objected but did not name the offending sibling: {violations}" + ) + + +@pytest.mark.parametrize( + "env, why", + [ + ({}, "the env var is not set at all"), + ({"VIEWS_APPWRITE": ""}, "set to empty — the CI misconfiguration that skips silently"), + ({"VIEWS_APPWRITE": "${{ github.workspace }}/_siblings/appwrite"}, "points at the wrong path"), + ], +) +def test_g1_bites_on_every_way_the_env_var_can_be_wrong(env, why): + """The three shapes of "checked out, but the tests cannot see it".""" + workflow = _workflow() + workflow["jobs"]["test"]["steps"][2]["env"] = env + violations = _g1_declared_checkouts_are_present_and_pointed_at(workflow, _ONE) + assert violations, f"G1 did not object to: {why}" + + +def test_g1_looks_at_the_step_that_runs_pytest_and_not_merely_any_step(): + """Env on the Lint step satisfies "the variable is set somewhere" and nothing else.""" + workflow = _workflow() + workflow["jobs"]["test"]["steps"][2]["env"] = {} + workflow["jobs"]["test"]["steps"].insert( + 2, {"run": "poetry run ruff check .", + "env": {"VIEWS_APPWRITE": "${{ github.workspace }}/_siblings/views-appwrite"}} + ) + assert _g1_declared_checkouts_are_present_and_pointed_at(workflow, _ONE), ( + "G1 accepted an env var set on the Lint step — the tests would never see it" + ) + + +def test_the_repos_own_checkout_is_not_mistaken_for_a_sibling(): + """G2 must not fire on line one of every workflow ever written (ADR-014 §3).""" + own_checkout_only = {"jobs": {"test": {"steps": [{"uses": "actions/checkout@v3"}]}}} + assert not _g2_every_checkout_is_declared(own_checkout_only, SIBLINGS) + assert not _sibling_steps(own_checkout_only) diff --git a/tests/test_env_declaration.py b/tests/test_env_declaration.py index 4f75f3f..74d3267 100644 --- a/tests/test_env_declaration.py +++ b/tests/test_env_declaration.py @@ -720,10 +720,14 @@ def test_no_coordinate_value_is_copied_into_this_repo(): # sibling repositories out into the workspace, and their documents are not this # repo's to police; an rglob would scan them and fail on someone else's prose. # - # **This half cannot run in CI**, and that is worth stating rather than discovering. - # The registry values come from the views-appwrite checkout, which is private and - # deliberately not checked out — so the whole test skips there. It guards a - # maintainer's commit, not the merge. Closing that is C-81's token decision. + # **This half runs in CI as of 2026-08-10** (ADR-016), and the change is worth + # noting because it inverts what this comment said for weeks. The registry values + # come from the views-appwrite checkout, which was private and deliberately absent — + # so the whole test skipped there, guarding a maintainer's commit but not the merge. + # views-appwrite went public on 2026-08-08 and CI now checks it out, so this scan + # runs on every pull request. It is the one test here whose CI behaviour went from + # "skip" to "runs a security-adjacent scan", and it is worth having: README.md + # carried four real coordinate values once already. tracked = subprocess.run( ["git", "-C", str(_REPO), "ls-files", "-z", "*.md"], capture_output=True, text=True, check=False, timeout=30, diff --git a/tests/test_product.py b/tests/test_product.py index 152fc0c..1fd606f 100644 --- a/tests/test_product.py +++ b/tests/test_product.py @@ -25,7 +25,7 @@ import pytest -from tests.conftest import CONSUMER_REPO, PARTNER_PACKAGES, SIBLING_ENV, require_sibling +from tests.conftest import CONSUMER_REPO, PARTNER_PACKAGES, SIBLINGS, require_sibling #: partner -> the document name its consumer filters on, DECLARED here rather than #: read back from the module under test. @@ -170,9 +170,9 @@ def test_every_partner_has_a_declared_consumer_repository(): f"{sorted(set(PARTNER_PACKAGES) - set(CONSUMER_REPO))}. Without one, that " "partner's consumer-name pin is never checked against the consumer." ) - undeclared = sorted(r for r in CONSUMER_REPO.values() if r not in SIBLING_ENV) + undeclared = sorted(r for r in CONSUMER_REPO.values() if r not in SIBLINGS) assert not undeclared, ( - f"consumer repositories with no SIBLING_ENV entry: {undeclared}. " + f"consumer repositories with no SIBLINGS entry: {undeclared}. " "require_sibling() raises KeyError rather than skipping for those, so the " "check would fail confusingly instead of skipping cleanly." ) From 124cf4789dffccfba12a22e09263908a6a9a2dbc Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Aug 2026 01:26:14 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(tests):=20G8=20compared=20two=20differe?= =?UTF-8?q?nt=20"today"s=20=E2=80=94=20one=20day=20of=20clock=20slack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- tests/test_ci_sibling_coverage.py | 51 ++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/tests/test_ci_sibling_coverage.py b/tests/test_ci_sibling_coverage.py index 489e72f..ce8c31c 100644 --- a/tests/test_ci_sibling_coverage.py +++ b/tests/test_ci_sibling_coverage.py @@ -266,12 +266,32 @@ def _g7_siblings_are_taken_from_main( ] +#: How far ahead of "today" a check date may sit before it is an error rather than a +#: clock difference. See `_g8_the_visibility_fact_carries_a_usable_date`. +_CLOCK_SKEW = dt.timedelta(days=1) + + def _g8_the_visibility_fact_carries_a_usable_date( workflow: dict, siblings: dict[str, Sibling] ) -> list[str]: - """``public_checked`` parses, and is not in the future.""" + """``public_checked`` parses, and is not meaningfully in the future. + + **One day of slack, and it is not laziness — this rule failed CI on its first run.** + The dates were stamped from a maintainer's machine in CEST at 01:25 on 2026-08-10; + the runner was in UTC, where it was still 23:25 on 2026-08-09. A date recorded + truthfully today read as tomorrow two hours away, and the guard called it a lie. + + "Today" is not a fact a date alone determines — it depends on where the reader is — + so comparing a bare ISO date against `date.today()` is comparing two different + questions. A full day of tolerance covers every real timezone offset, and a date more + than a day ahead is still what this rule is for: a fact nobody actually checked. + + Recorded here rather than fixed silently, because the alternative repair a hurried + reader would reach for is deleting the rule (ADR-014 §3: when a guard cries wolf, + check the matching before the scope). + """ problems = [] - today = dt.date.today() + horizon = dt.date.today() + _CLOCK_SKEW for name, sibling in siblings.items(): try: checked = dt.date.fromisoformat(sibling.public_checked) @@ -281,9 +301,10 @@ def _g8_the_visibility_fact_carries_a_usable_date( "A fact with an unreadable date is a fact with no date." ) continue - if checked > today: + if checked > horizon: problems.append( - f"{name}: public_checked={sibling.public_checked} is in the future." + f"{name}: public_checked={sibling.public_checked} is more than a day " + "ahead of today — that is not clock skew, it is a date nobody checked." ) return problems @@ -440,7 +461,7 @@ def _replace(name: str, **changes) -> dict[str, Sibling]: "takes a branch that is not the authority"), ("G8 the visibility fact carries a usable date", _workflow(), _replace("views-appwrite", public_checked="2099-01-01"), - "the fact was checked in the future"), + "the fact was checked far in the future"), ("G8 the visibility fact carries a usable date", _workflow(), _replace("views-appwrite", public_checked="last tuesday"), "unparseable date"), @@ -509,3 +530,23 @@ def test_the_repos_own_checkout_is_not_mistaken_for_a_sibling(): own_checkout_only = {"jobs": {"test": {"steps": [{"uses": "actions/checkout@v3"}]}}} assert not _g2_every_checkout_is_declared(own_checkout_only, SIBLINGS) assert not _sibling_steps(own_checkout_only) + + +def test_g8_tolerates_a_timezone_but_not_a_fiction(): + """The boundary either side of `_CLOCK_SKEW`, pinned. + + A rule with a tolerance needs its tolerance tested, or the number drifts into being + whatever made the last failure go away. Tomorrow is a clock difference; the day after + is a claim about a check that has not happened. + """ + today = dt.date.today() + for offset, should_object in ((0, False), (1, False), (2, True), (400, True)): + siblings = _replace( + "views-appwrite", + public_checked=(today + dt.timedelta(days=offset)).isoformat(), + ) + violations = _g8_the_visibility_fact_carries_a_usable_date(_workflow(), siblings) + assert bool(violations) is should_object, ( + f"a check date {offset} day(s) ahead of today: expected " + f"{'an objection' if should_object else 'no objection'}, got {violations}" + ) From e902c1fc82cc1289a8805e13ad9a796aa5136075 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Aug 2026 01:28:21 +0200 Subject: [PATCH 3/3] =?UTF-8?q?docs(register):=20C-81=20=E2=80=94=20the=20?= =?UTF-8?q?measured=20CI=20figure,=2016=20skips=20to=209?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- reports/technical_risk_register.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index 05a6236..c97871e 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -531,7 +531,7 @@ Cross-refs: **C-46** and **C-57** (both RESOLVED; this is the residual each reco The coverage half rested on a claim nobody could check. This entry, and the workflow comment it drew on, said `views-appwrite` was **private**, so its seven checks needed a credential. It went public on **2026-08-08** (`views-appwrite@9d80b75`, a deliberate and recorded act), and the claim here went on being made for two days afterwards. No credential was required, and none had been the obstacle since that date. -CI now checks that repository out and those seven run on every pull request — including both registry-drift detectors, which is what this entry called *"the most valuable of the lot"*. One test also changes character rather than merely un-skipping: the scan refusing registry **values** in this public repository's markdown now runs on the merge rather than only on a maintainer's machine. +CI now checks that repository out and those seven run on every pull request — including both registry-drift detectors, which is what this entry called *"the most valuable of the lot"*. **Measured on the merge run, not derived:** CI went from 16 skips to **9** (`398 passed / 9 skipped / 38 xfailed`, PR #229), the remainder being 8 views-datafactory and 1 views-faoapi. One test also changes character rather than merely un-skipping: the scan refusing registry **values** in this public repository's markdown now runs on the merge rather than only on a maintainer's machine. **What remains, and it is two different things:**