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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/run_pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,14 @@ jobs:
# 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.
# the default GITHUB_TOKEN is scoped to THIS repository, so a tokenless checkout of a
# private repository simply fails, loudly, at this step.
#
# An earlier version of this comment said that "verifies the `public` field". There is
# no such field: `public` was deleted from `SIBLINGS` on 2026-08-10 because nothing
# could verify it, and ADR-016 §4 now says so explicitly. A comment in THIS file
# asserting a fact about another repository that nothing checks is the founding
# incident of ADR-016 §3, and it had reappeared here.
- name: Checkout views-appwrite (sibling)
uses: actions/checkout@v3
with:
Expand Down
97 changes: 94 additions & 3 deletions docs/ADRs/016_ci_read_access_to_private_siblings.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ the boundary and says so, rather than implying a coverage it does not have.
Most tests here check our own code. A few check something different: whether a **claim
this repository makes about a different repository** is still true.

The clearest example. Each partner's `appwrite_env.py` says, in effect, *"we were written
against edition 1.4.4 of the shared configuration registry."* That registry lives in
another repository. A test opens that repository and checks the edition is still 1.4.4.
The clearest example. Each partner's `appwrite_env.py` records which edition of the
shared configuration registry it was verified against, by naming a commit in another
repository. Tests open that repository at that commit and ask whether anything this
package reads has moved since — a rename, a reclassification, a rotated value, or a new
coordinate that has arrived. *(Until 2026-08-11 they asked a cruder question — whether the
edition **label** still matched — which fired on every upstream edit. See §7b's erratum.)*

This is not hypothetical housekeeping. On 2026-08-05 that test failed — the registry had
moved to a new edition while nobody here was looking. Two days earlier, the same family of
Expand Down Expand Up @@ -86,6 +89,41 @@ nothing able to check it, will eventually be wrong and nobody will find out.

That is all. It replaces a comment with something a test can read.

**Where that code lives, and why the question is worth asking.** The declaration sits in
`tests/conftest.py`, because that is where this repository already keeps facts several
test modules share. It is not obviously the right home: `conftest.py` is pytest's fixture
file, and none of these declarations is a fixture — they are statements about the platform
that tests happen to read.

The cost is already visible. `scripts/build_gaul_lookup.py` needs the same
sibling-location fact and cannot import it, because a script importing from `tests/` is
the dependency direction backwards. So it declares the variable name a second time. That
duplication is defensible on its own merits — the two contracts genuinely differ — but it
was not a free choice, and a reader should know that a structural constraint and a design
decision happened to agree.

**Named trigger for moving it (ADR-014 §4):** a second non-test consumer needing one of
these declarations, or a fifth declaration arriving in that file. The shape it would move
to is a small module owning the platform declarations, importable by tests and scripts
alike. Not committed to here, because the second incident is what tells you whether that
shape is right. Tracked as register **C-88**.

Who is who, once and in one table:

| repository | public? | fetched by CI? | why not, if not |
|---|---|---|---|
| **views-appwrite** | yes | **yes** | — carries the registry drift checks |
| **views-crafdapi** | yes | **yes**, temporarily | — retires when views-crafdapi#53 lands, not before (ADR-017 §5 is per partner) |
| **views-datafactory** | yes | no | its checks need raw data that is not in its git repository; fetching it turns an honest skip into a crash |
| **views-faoapi** | **no** | no | private — no amount of workflow configuration reaches it. That case is [ADR-017](017_facts_across_a_private_boundary.md) |

**What of this table is enforced, and what is not.** `ci_checkout` is: the workflow and
`SIBLINGS` are compared by `tests/test_ci_sibling_coverage.py`, and disagreement fails.
The **`public?` column is not, and cannot be** — which is why `public` was deleted from the
`Sibling` record on 2026-08-10, after a review found nothing could verify it. It is here
because a reader needs it to follow the argument. Nothing reads this table; if it drifts
from `SIBLINGS`, only a human will notice.

### §5 CI downloads exactly what that list says, and a test enforces it

The workflow downloads every sibling marked `ci_checkout=True`.
Expand Down Expand Up @@ -190,6 +228,53 @@ branch. Both live here, and they answer different questions:
G7 makes both read `main` rather than a default branch. It does not make either of them
track the tip for its own sake.

**Erratum, 2026-08-11 — there is a third kind, and §7a's "not adopted here" is stale.**

§7a recorded views-appwrite's suggestion — pin against the contract *version* and treat
observation-only bumps as non-blocking — and marked it *not adopted here, because it needs
the upstream side first*. That framing was wrong in a way worth correcting rather than
quietly editing: it treated the noise as something only upstream could fix.

The registry-edition check was matching on `meta.version`, a label meaning *"anything at
all changed"*. What this repository actually depends on is a set of rows. Matching on the
rows instead removes the false alarms **without needing anything from upstream** — which
is ADR-014 §3 applied properly: when a guard fires on something legitimate, the first
question is whether the matching is wrong, not whether the scope is too wide. The matching
was wrong.

So the two categories above become three:

- **Drift tripwires** read the sibling's `main` because movement is the signal.
- **Reachability checks** read a pinned commit; movement is noise.
- **Differential tripwires** read **both** — a pinned edition as the baseline, the
sibling's `main` as the comparison — and fire when something *we declare* differs between
them, or when a coordinate arrives in a table we read. This is the shape the registry
check now has. It catches a rotation, which is invisible to the other two, and it is
silent through prose edits and version bumps.

*(Corrected twice on 2026-08-11, and the second time is the instructive one. The first
implementation compared against the sibling's **working tree** — so a developer whose clone
sat on a feature branch graded this repository against unreviewed content, which is #196's
shape. The first correction moved **one** check onto `origin/main` and this paragraph then
claimed all of them did; three others were still reading the working tree. All four read
`origin/main` now. A sentence written to describe a fix, one fix ahead of the code, is the
same defect §3 diagnoses.)*

The third kind exists because the no-copy rule forbids writing expected coordinate values
into this repository. A pinned edition is the only lawful place to keep a baseline for
comparing them.

views-appwrite#76 has since been done. It shipped on 2026-08-11 as registry version 1.6.0,
which adds a table naming each edition and saying whether that edition obliges the
repositories that read the registry. This repository does not read it yet, and does not have
to: the third kind of check above already tells us whether anything we depend on moved. What
the new table would add is upstream's own answer to the same question, which is cheaper and
does not require a pinned baseline at all. That is worth adopting, and it is not urgent.

It is worth saying how we learned it had shipped. Nobody told us. The check that requires
every table upstream to be classified here went red the first time it met the new one, which
is what that check is for.

---

This section overrides an earlier internal recommendation not to couple per-pull-request CI
Expand All @@ -216,6 +301,12 @@ Until that declaration exists, the affected check runs on a maintainer's machine
in CI. That is stated in the sibling's `note`, which rule G4 requires and which must name
the record that owns it.

**Update 2026-08-11.** The declaration now exists (views-appwrite#75), views-faoapi
checks itself against it (their #379), and this repository's check reads the registry
rather than that repository's source. So the FAO half is no longer laptop-only, and no
credential was issued. What remains is CRAF'd's consumer-side check (views-crafdapi#53);
until it lands, that partner's source-read stays, and with it that partner's fetch.

---

## Consequences
Expand Down
59 changes: 49 additions & 10 deletions docs/ADRs/017_facts_across_a_private_boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,33 @@ must decide for the category, not for today's instance.
> surface. Each side verifies itself against that declaration. Neither side reads the
> other's source code.**

The same thing as a picture:

```
views-appwrite
the registry (public)
"un_fao"

▲ ▲
│ │
views-postprocessing views-faoapi
writes the name reads the name

these two never read each other
```

**The arrow that is missing is the point.** Neither repository reads the other, so it does
not matter that one of them is private, and no credential is needed by anyone. Both arrows
point at a third place that is public to everybody.

Who does what, once and in one table:

| repository | its role | what it checks | needs access to |
|---|---|---|---|
| **views-appwrite** | holds the fact | nothing — it is the authority | — |
| **views-postprocessing** (here) | writes the name onto every upload | its own copy against the registry | the registry only |
| **views-faoapi** (private) | opens files with that name | its own served name against the registry | the registry only |

Concretely, for the delivery label:

1. The value is declared once, in the platform's public coordinate registry
Expand Down Expand Up @@ -330,17 +357,29 @@ visibility can be inferred for future APIs.
1. **views-appwrite** — declare the label for each partner in the coordinate registry.
Filed as views-appwrite#75. Nothing here can proceed before it.

**The two partners are not equally ready, and the halves should land separately.** The
FAO label is fully specified — `un_fao`, in force, evidenced in ADR-013 §4.1a. The
CRAF'd label is **not a value anyone has decided yet**: that partner serves a different
dataset whose targets, columns and entity model are still an open data-contract question
on the consumer side. Bundling both into one edit stalls the ready half behind the
blocked one, so **the FAO row should land on its own** and the CRAF'd row should follow
its data contract.
**Superseded 2026-08-11 — both rows now exist and both are in force.** This step used
to say the CRAF'd label was "not a value anyone has decided yet" and should follow its
data contract. views-appwrite declared both rows in v1.5.0, and the CRAF'd row states
the correction itself: *"Both sides already use this value in code today — this row
DECLARES a standing fact, it does not decide a new one."* It cites both sides' source.

So step 1 is **done for both partners**, and step 2 covers both. What remains split is
step 3, which is per-partner and is what step 2's retirement half waits on.
2. **views-postprocessing** — switch the check to read the registry rather than the
consumer's source, for both partners. Ours, blocked on step 1. **In the same change**,
stop fetching `views-crafdapi` in CI: §7 shows that fetch then serves nothing, and
ADR-016's rules require a sibling that is not fetched to say why.
consumer's source, for both partners. Ours, blocked on step 1.

**Erratum, 2026-08-11 — this step used to say "in the same change, stop fetching
`views-crafdapi` in CI", and that contradicted §5.** §5 constrains the order: a
partner's source-reading check is removed only once *that partner's* consumer-side
check exists. The crafd source-read is the sole consumer of the crafd fetch, so
retiring the fetch retires the check — before views-crafdapi#53 has landed. The
instruction and the constraint could not both be followed.

Corrected: **retirement is per partner, and follows that partner's step 3.** For FAO
that is now met (views-faoapi#379 merged 2026-08-11), so the FAO source-read goes. For
CRAF'd it is not (views-crafdapi#53 open), so the crafd source-read stays — and with
it the crafd fetch, whose `note` in `tests/conftest.py` already records that it lives
or dies with that one check.
3. **views-faoapi** — verify its own served label against the declaration; §8's other half.
Filed as views-faoapi#379, and the consumer-side maintainer has accepted it.

Expand Down
Loading
Loading