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
16 changes: 16 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@
# stay routed here as directories: the driver still owns a same-category
# collision, which is the residue sharding cannot remove.
#
# Two entries below are still SINGLE files — spec-changes.json and
# docs/protocol-upgrade-guide.md — and #8344 asked what the driver-less queue does
# to them. MEASURED (2026-08-13, the real in-flight case plus four synthetic pairs,
# each merged in a clone with no merge.os-regen.driver): it never leaves them
# stale-but-clean. Both are sorted unions and an ADR-0087 registration is
# insertion-only, so the queue's text merge either takes both sides — byte-identical
# to the regeneration, gates green — or conflicts outright. It conflicts only when
# the two in-flight entries are ADJACENT in registry sort order; one existing entry
# between them is already enough to merge clean AND current.
#
# Sharding them would not buy back an ejection, which is why they are still single
# files: every conflicting case also conflicts in
# packages/spec/src/migrations/registry.ts — generated, committed, unsharded,
# NOT_DRIVER_MANAGED — and every registration touches it by construction. The table
# and the reproduction are in packages/spec/src/migrations/entries/README.md.
#
# `merge=os-regen` hands those paths to `scripts/git-merge-regen.mjs`, which does
# NOT text-merge them. See that file for why it also does not regenerate them
# in place (git runs merge drivers BEFORE the sources are merged, so anything
Expand Down
14 changes: 11 additions & 3 deletions packages/spec/scripts/build-spec-changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,17 @@
* both is not consumer leniency — a published tarball is immutable, so there is
* no producer to fix. This repo's OWN surface is always the directory.
*
* `spec-changes.json` itself stays a single file, deliberately (#5837): it is
* keyed by version, so two PRs append under different majors and it has never
* been a conflict surface worth splitting.
* `spec-changes.json` itself stays a single file, deliberately (#5837), and #8344
* re-measured that call rather than inheriting it. The original reason — "two PRs
* append under different majors" — is not what actually holds: in-flight
* registrations land in the SAME (current) major, so what separates them is their
* distance in the registry's id sort order, not the major. What holds is the
* conclusion. This file is a sorted union of an insertion-only registration, so a
* driver-less server-side merge either takes both sides (byte-identical to the
* regeneration) or conflicts; it is never stale-but-clean, it conflicts only on
* ADJACENT ids, and in that case `src/migrations/registry.ts` — unsharded and
* outside the merge driver — conflicts too, so splitting this file would not save
* the PR. Measurement table: `../src/migrations/entries/README.md`.
*/

import { readFileSync, writeFileSync, existsSync } from 'node:fs';
Expand Down
46 changes: 46 additions & 0 deletions packages/spec/src/migrations/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,49 @@ The regeneration lap. `spec-changes.json` and `docs/protocol-upgrade-guide.md` a
projections of this registry and still have to be regenerated and committed when an
entry lands. #6957's ruling kept them in version control on purpose — the review diff
is worth the laps it costs — so a retirement card is not faster, only harder to lose.

### What the merge queue does with those projections — measured (#8344)

`.gitattributes` routes both through `merge=os-regen`, and that driver is a **local**
git facility: the GitHub merge queue rebuilds each PR server-side, where no custom
driver runs. #8344 asked what the queue therefore produces for them when two ADR-0087
registrations are in flight — stale-but-clean, a conflict, or something correct.

Measured 2026-08-13, on the real in-flight case that raised the question (#8325's
branch against a `main` already carrying #8324 and #8327) plus four synthetic pairs,
each merged in a clone with **no `merge.os-regen.driver` configured** — which is
exactly the queue's situation:

| two registrations in flight, distance in registry sort order | driver-less merge | the un-regenerated result |
| --- | --- | --- |
| the real case — #8324 + #8327 against #8325 | clean | **byte-identical** to the regeneration |
| ids far apart | clean | `check:spec-changes`, `check:upgrade-guide`, `check:migration-registry` all pass |
| exactly one existing entry between them | clean | all three gates pass |
| **adjacent — nothing between them** | **conflict** | — (the PR is ejected) |
| **both the first entry of a new major** | **conflict** | — (the PR is ejected) |

So the answer is **no, never stale-but-clean**. Both files are sorted unions and a
registration is insertion-only, so the queue's text merge either takes both sides —
which *is* the regeneration, byte for byte — or refuses. There is no third outcome,
and the `--check` gates are what prove the clean rows current rather than merely
conflict-free.

The residue is that conflict, and **sharding those two files would not buy back a
single ejection**: every conflicting case above also conflicts in `registry.ts`, which
is generated, committed, unsharded and deliberately outside the driver
(`NOT_DRIVER_MANAGED` in `scripts/regen-artifacts.mjs`) — and which every registration
touches by construction. Sharding this directory removed the collision at the
**source**; it does not reach the generated file the sources are concatenated into.

⚠️ Locally the driver hides two thirds of that signal: the adjacent-id merge reports
three conflicts server-side and one — `registry.ts` — in a clone with the driver
registered, because the driver defers the two projections. The one it leaves standing
is enough to see the collision coming, which is what makes this a narrow hazard rather
than a silent one.

Reproduce any row with a driver-less clone and git's own server-side merge:

```
git clone --shared --no-local . /tmp/driverless # a fresh clone has no merge.os-regen.driver
git -C /tmp/driverless merge-tree --write-tree --messages BRANCH_A BRANCH_B
```
Loading