From d31e3af0d10ecd214c0b3607bbc2b470201687e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 08:26:01 +0000 Subject: [PATCH] docs(spec): measure what the driver-less merge queue does to the two unsharded ADR-0087 projections (#8344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #8344 asked an open question it could not answer from a PR branch: `.gitattributes` routes `packages/spec/spec-changes.json` and `docs/protocol-upgrade-guide.md` through `merge=os-regen`, a LOCAL git driver, while the merge queue rebuilds each PR server-side where no custom driver runs. Stale-but-clean, conflict, or correct? Measured, not reasoned: the real in-flight case (#8325's branch against a `main` already carrying #8324 and #8327) plus four synthetic two-registrations-in-flight pairs, each merged in a clone with no `merge.os-regen.driver` configured. Answer: 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 — byte-identical to the regeneration, all three `check:` gates green on the un-regenerated result — or conflicts. It conflicts only on ADJACENT registry ids; one existing entry between them already merges clean and current. So the card's default direction is measured NOT to work: sharding these two files buys back zero ejections, because every conflicting case also conflicts in `packages/spec/src/migrations/registry.ts` — generated, committed, unsharded and NOT_DRIVER_MANAGED — which every registration touches by construction. No sharding, no CI regenerate-and-diff; the finding and its reproduction are the deliverable. This also corrects `build-spec-changes.ts`'s stated reason for staying a single file ("two PRs append under different majors"): in-flight registrations land in the same current major, so distance in id sort order is what separates them. The conclusion held; the reason did not. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WocN37om5bw81JDoEEMA2e --- .gitattributes | 16 +++++++ packages/spec/scripts/build-spec-changes.ts | 14 ++++-- .../spec/src/migrations/entries/README.md | 46 +++++++++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index e19c90ccfa..8ef510ce03 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/packages/spec/scripts/build-spec-changes.ts b/packages/spec/scripts/build-spec-changes.ts index 3218f4752f..a039ee51ef 100644 --- a/packages/spec/scripts/build-spec-changes.ts +++ b/packages/spec/scripts/build-spec-changes.ts @@ -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'; diff --git a/packages/spec/src/migrations/entries/README.md b/packages/spec/src/migrations/entries/README.md index 2b05c316a8..d8c4358fe4 100644 --- a/packages/spec/src/migrations/entries/README.md +++ b/packages/spec/src/migrations/entries/README.md @@ -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 +```