Skip to content

finding: a stacked PR series has no model in this repo's tooling — three costs measured end-to-end on the #14478 six-card stack #16149

Description

@os-sales

Filed bare for triage by the domain:spec execution seat (session_01G4138K1EG7kQ81FNba5Kp4) after the #14478 stack landed on main at e9fcd6bbd (2026-09-06T03:17Z). Observation, not a graded defect — no fix proposed, no domain:* set, no pm:queue.

The stack was six cards deep (#15676#15682, PRs #15814 · #15837 · #15906 · #15938 · #15988 · #16022), each branched off the one below it, landing on a shared trunk branch and then to main as PR #15626. It worked, but three separate pieces of tooling turned out to have no representation of "this PR is stacked on another PR", and each cost real work. All three are measurements from that campaign, not predictions.


1. check:adr-0087-registrationalready-registered is base-relative, and a stacked card has two bases

The gate's own header states the rule:

already-registered — every named id must resolve at HEAD and already exist at the merge base. If the id is new in this diff the honest disposition is registered, and the two cannot be confused because base decides.

Card 6/6 renamed a key already covered by a conversion card 5/6 registers. So:

base is turso-config-timeout-to-timeout-ms there? honest disposition
card 6/6's own base (card 5/6's head) yes already-registered — exit 0
origin/main no registered — anything else fires R5, severity 3

Neither card is wrong. The two base levels demand contradictory dispositions, and R5 consults only the ids this diff adds, with no cross-reference to a sibling changeset carrying registered in the same eventual merge. Writing registered on card 6/6 would have fired R4 against its own base instead.

The workaround that shipped: land all six cards on the trunk, then edit the disposition to registered on the trunk before it merged to main. Verified there — exit 0, .changeset/driver-turso-config-timeout-ms.md [BREAKING+bang] registered turso-config-timeout-to-timeout-ms (new here: …).

Two traps found while pre-verifying that edit, worth recording whether or not this is ever fixed:

  • The gate reads the changeset from the committed diff, not the working tree. An uncommitted edit returns the identical finding, so it reads as "the fix does not work".
  • Trailing prose on the marker line is parsed as further ids. Keeping the explanatory sentence inline produced "29 of those id(s) do not exist in the ADR-0087 registries at HEAD: —, that, protocol-18, conversion, added, by, …". The not-required (…) form tolerates prose only because the parentheses delimit the ids.

2. Squash-only × stacked ⇒ one repair lap per descendant, cumulative

Merge commits are refused at the API level:

PUT /repos/objectstack-ai/objectstack/pulls/15906/merge  (merge_method: merge)
  → 405 Merge commits are not allowed on this repository

and main carries 0 merge commits in its last 154 (control: the same --merges query does find one on a feature branch, so the zero is a reading).

A squash keeps the content and destroys the ancestry link, so every descendant's merge base rewinds past everything already landed. Measured after card 2/6 landed:

git merge-base --is-ancestor dcd1be0db 43cc4659a   → exit 1   (2/6's head is NOT in the trunk's history)
git merge-base 43cc4659a adad660a1                 → 140e0b266  (before cards 1/6 AND 2/6)

The descendant then re-applies content the trunk already holds and collides. Three repair laps were paid — cards 3/6, 4/6 and 6/6 — each a merge, a hand resolution and a regeneration pass. The conflicts were benign in shape but not free: card 4/6's four conflicts were all one prose correction made after it forked, and its side was a purely inherited stale snapshot (blobs byte-identical to its fork point).

⭐ The generalisation, which is what makes this worth a card: the cost is not per-stack, it is per-descendant, and it recurs on every landing. A six-deep stack pays it five times if nothing changes.

3. Heavy CI never runs on a stacked PR at all

lint.yml and ci.yml both declare:

on:
  pull_request:
    branches:
      - main

A PR based on a feature branch triggers neither, so none of the six required contexts reports on any stacked head. Measured across this whole stack: every card PR showed 8–11 light checks (Auto Label, Check Changeset, PR Size, docs drift, …) and zero of Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Dogfood Regression Gate, Temporal Conformance.

Consequence: a stack's first real signal arrives only at the trunk PR, after every card is written, reviewed and merged. Six cards' worth of work was verified by local runs quoted into PR bodies — which is a discipline that worked here, but it is a discipline, not a gate. On the trunk PR the real run came back 35/35 green on the first try; that is a fact about this stack's care, not about the arrangement's safety.


Prior art in this repo

#12933 is the same family through a different mechanism: GitHub's speculative queue stacking made merge-queue-triage count one deterministic failure as N distinct victim PRs, because the tooling had no notion that the victims were one stack. That card's phrasing applies here too — the count (or the disposition, or the trigger) is systematically wrong in a knowable, mechanically detectable way, and every reader re-derives it by hand.

For triage

Deliberately not split into three cards: the three costs were measured on one campaign and a reader deciding whether this repo should support stacked series needs them together. Splitting is triage's call. Each has a different owner and a different kind of fix — a gate rule, a repository setting or policy, and a workflow trigger — and it is entirely reasonable to grade them differently, including grading some or all of them "won't fix, stacks are rare enough".

⚠️ What this card does not claim: that stacking was the wrong choice for #14478. Six cards over one contract surface, each independently reviewable, is what the maintainer ruled and it produced a clean landing. The finding is only that the tooling around it is silent about the arrangement, and the silence is paid for by hand each time.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions