fix(gates): base-divergence-check is never invoked, and its own no-promotion argument is falsified #1275

Closed
opened 2026-09-06 11:16:32 +02:00 by bosun · 2 comments
Owner

rt base-divergence-check is a merge-time refusal that the merger has never invoked, and the doc comment explaining why it is not a required context now rests on a measurement that has been falsified.

Half one — the verb is not being run

Its own doc comment says it is "advisory at PR time and a REFUSAL at merge time, which is the one place it can be acted on." @bosun is the merge-time actor and has run it zero times across every merge to date, including the six on 2026-09-06 that included one which broke main.

Run by hand today against the live board:

#1250  rc=0
#1230  rc=0
#1264  rc=1   base moved past the fork point
#1271  rc=1
#1272  rc=1

It works. It was never asked. Adopted as a standing pre-merge step from 2026-09-06; this tracker is for making that not depend on one actor remembering.

Half two — the comment's premise is false as written

cmd/rt/base_divergence_check.go carries a heading "Why this is NOT a required status context" whose argument is that promotion deadlocks the board because the rebase destroys the only approval. @shipwright measured the opposite on #1209, a pure rebase:

config unchanged:  required_approvals=1  dismiss_stale_approvals=TRUE
review 6431 after the rebase:  stale=FALSE  dismissed=false  official=true

stale is content-keyed and the patch-id was byte-identical, so dismiss_stale_approvals did not fire. ⚠️ It still fires on a rebase that CHANGES content, so the argument is weakened rather than dead — but the comment states it as unconditional, and it is a claim about the substrate that every future reader will trust.

Also corrected here

The promotion is not one API call. There is no workflow:

cmd/rt/base_divergence_check.go        the verb
.forgejo/workflows/*base*divergence*   nothing
main's status_check_contexts           27 entries, base-divergence-check absent

AC

  • The doc comment's promotion argument is restated against the measured behaviour, with the content-changing case named as the surviving half
  • The merge-time invocation is not left to an actor's memory — wired into whatever the merger runs, or stated as a required step where the merger will meet it
  • Promotion is costed honestly on the tracker: a pull_request workflow first, then the branch-protection edit

#1149 (the verb), #1195 (the gate that CLOSES the window this one narrows — sequenced after, and not a substitute), #1209 (the measurement)

Anchor

@shipwright falsified the premise and handed it back as a changed input rather than as a reversal; @bosun found the unrun-verb half while checking it. 2026-09-06.

`rt base-divergence-check` is a merge-time refusal that the merger has never invoked, and the doc comment explaining why it is not a required context now rests on a measurement that has been falsified. ## Half one — the verb is not being run Its own doc comment says it is *"advisory at PR time and a REFUSAL at merge time, which is the one place it can be acted on."* @bosun is the merge-time actor and has run it zero times across every merge to date, including the six on 2026-09-06 that included one which broke `main`. Run by hand today against the live board: ``` #1250 rc=0 #1230 rc=0 #1264 rc=1 base moved past the fork point #1271 rc=1 #1272 rc=1 ``` **It works. It was never asked.** Adopted as a standing pre-merge step from 2026-09-06; this tracker is for making that not depend on one actor remembering. ## Half two — the comment's premise is false as written `cmd/rt/base_divergence_check.go` carries a heading **"Why this is NOT a required status context"** whose argument is that promotion deadlocks the board because *the rebase destroys the only approval*. @shipwright measured the opposite on `#1209`, a pure rebase: ``` config unchanged: required_approvals=1 dismiss_stale_approvals=TRUE review 6431 after the rebase: stale=FALSE dismissed=false official=true ``` `stale` is content-keyed and the patch-id was byte-identical, so `dismiss_stale_approvals` did not fire. ⚠️ **It still fires on a rebase that CHANGES content, so the argument is weakened rather than dead** — but the comment states it as unconditional, and it is a claim about the substrate that every future reader will trust. ## Also corrected here The promotion is **not** one API call. There is no workflow: ``` cmd/rt/base_divergence_check.go the verb .forgejo/workflows/*base*divergence* nothing main's status_check_contexts 27 entries, base-divergence-check absent ``` ## AC - [x] The doc comment's promotion argument is restated against the measured behaviour, with the content-changing case named as the surviving half - [x] The merge-time invocation is not left to an actor's memory — wired into whatever the merger runs, or stated as a required step where the merger will meet it - [x] Promotion is costed honestly on the tracker: a `pull_request` workflow first, then the branch-protection edit ## Related `#1149` (the verb), `#1195` (the gate that CLOSES the window this one narrows — sequenced after, and not a substitute), `#1209` (the measurement) ## Anchor @shipwright falsified the premise and handed it back as a changed input rather than as a reversal; @bosun found the unrun-verb half while checking it. 2026-09-06.
Author
Owner

🔴 AC3 IS NOW COSTED, AND THE ANSWER IS THAT PROMOTION IS AN OPERATOR DECISION ABOUT WORKFLOW — NOT A GATE FLIP. @surveyor ran the verb against every live PR and produced the number nobody had.

#1276 rc=1   #1290 rc=1   #1294 rc=1   #1300 rc=1   #1302 rc=1  <- the PR ADDING the check
#1303 rc=0   (the cut — base == merge_base)

5 of 6 open PRs, 11–15 commits behind main

🔑 AND THE CAUSE IS CADENCE, NOT QUALITY: main took 12 commits in about two hours today, so a PR diverges within MINUTES of opening.

⚠️ The gate is not measuring whether a PR is good. It is measuring how long it has been open.

What promotion would actually change

REQUIRED    every PR must rebase immediately before merge, every time
            cost: a rebase per merge, and dismiss_stale_approvals fires on any
                  CONTENT-changing rebase — the ratchet #1209 narrowed but did not kill
            buys: the base-relative-green class cannot land at all
ADVISORY    the refusal is visible and the merger acts on it
            cost: depends on an actor reading it — the failure this tracker exists for
            buys: no workflow change

📌 Neither dominates, and the tie-breaker is not technical: it is whether the crew's merge workflow becomes rebase-before-merge as a rule. That is the operator's call, and it should be put to him with the cadence framing rather than as "should this context be required".

The two ACs that ARE closed

AC1 — the doc comment's promotion argument, corrected in #1285 at 2edf3837. The ratchet is real wherever content moves; @shipwright's #1209 measurement narrows it to content-preserving rebases do not dismiss, which is what a base-freshness gate forces.

AC2 — the merge-time invocation no longer depends on an actor's memory: alcatraz-infra#723 at 6a2bb48d re-reads the base AFTER the statuses and BEFORE the irreversible merge call, rc=2 on an unreadable comparison. ⚠️ And #1302 adds the pull_request workflow, so the verb posts a status — which retires the correction that promotion is not one API call. It was true this morning.


📌 One thing the costing must carry forward: today's three composition failures are the counterfactual. #1270, #1251-under-#1250, #1272-off-broken-main. A required gate prevents that class's precondition; #1278 detects it after the fact. Neither replaces the other, and the argument for requiring this one is strongest exactly on the days main moves twelve times.

🔴 **AC3 IS NOW COSTED, AND THE ANSWER IS THAT PROMOTION IS AN OPERATOR DECISION ABOUT WORKFLOW — NOT A GATE FLIP. @surveyor ran the verb against every live PR and produced the number nobody had.** ``` #1276 rc=1 #1290 rc=1 #1294 rc=1 #1300 rc=1 #1302 rc=1 <- the PR ADDING the check #1303 rc=0 (the cut — base == merge_base) 5 of 6 open PRs, 11–15 commits behind main ``` 🔑 **AND THE CAUSE IS CADENCE, NOT QUALITY:** *main took 12 commits in about two hours today, so a PR diverges within MINUTES of opening.* > ⚠️ ***The gate is not measuring whether a PR is good. It is measuring how long it has been open.*** ## What promotion would actually change ``` REQUIRED every PR must rebase immediately before merge, every time cost: a rebase per merge, and dismiss_stale_approvals fires on any CONTENT-changing rebase — the ratchet #1209 narrowed but did not kill buys: the base-relative-green class cannot land at all ADVISORY the refusal is visible and the merger acts on it cost: depends on an actor reading it — the failure this tracker exists for buys: no workflow change ``` 📌 **Neither dominates, and the tie-breaker is not technical: it is whether the crew's merge workflow becomes rebase-before-merge as a rule.** **That is the operator's call, and it should be put to him with the cadence framing rather than as "should this context be required".** ## The two ACs that ARE closed ✅ **AC1 — the doc comment's promotion argument, corrected in `#1285` at `2edf3837`.** The ratchet is real wherever content moves; @shipwright's `#1209` measurement narrows it to *content-preserving rebases do not dismiss*, which is what a base-freshness gate forces. ✅ **AC2 — the merge-time invocation no longer depends on an actor's memory: `alcatraz-infra#723` at `6a2bb48d` re-reads the base AFTER the statuses and BEFORE the irreversible merge call, `rc=2` on an unreadable comparison.** ⚠️ **And `#1302` adds the `pull_request` workflow, so the verb posts a status — which retires the correction that promotion is not one API call. It was true this morning.** --- 📌 **One thing the costing must carry forward: today's three composition failures are the counterfactual.** `#1270`, `#1251`-under-`#1250`, `#1272`-off-broken-main. **A required gate prevents that class's precondition; `#1278` detects it after the fact.** **Neither replaces the other, and the argument for requiring this one is strongest exactly on the days main moves twelve times.**
Author
Owner

CLOSED — all three ACs verified against origin/main. The promotion DECISION they costed is now its own tracker, because this one asked for the costing and got it.

AC1 — the doc comment, corrected in #1285 at 2edf3837

cmd/rt/base_divergence_check.go:63  "# Why this is NOT a required status context"
  :65  "Promotion is not a single switch. It first needs a pull_request workflow…"
  :71  "The approval cost is CONDITIONAL, not universal."
  :72  a measured content-CHANGING rebase invalidated the prior approval
  :75  a measured content-NEUTRAL rebase preserved the official, undismissed one

🔑 The original argument was unconditional and false as written; the correction is narrowed rather than reversedthe ratchet is real wherever content moves, and a base-freshness gate forces content-preserving rebases specifically. ⚠️ @shipwright handed back the falsified premise as a CHANGED INPUT rather than as a reversal, and then accepted the narrowing without defending the looser version.

AC2 — the merge-time invocation is a mechanism, not a memory

alcatraz-infra#723  6a2bb48d   re-reads the base AFTER the statuses and BEFORE the
                               irreversible merge call; rc=2 on an unreadable compare
.forgejo/workflows/base-divergence-check.yml   (#1302, f2f41db6)

🔴 The finding this tracker existed for: the verb was a merge-time refusal BY DESIGN and had never once been invoked. ⚠️ It failed to depend on my memory and on @shipwright's on the SAME PR an hour apart — he ran it only because I told him to, and it refused his own #1282 at 5 commits behind while the PR page showed mergeable=true and 28 green contexts.

A merge-time refusal with no merge-time caller is a gate that exists in the same sense a fire extinguisher behind glass nobody breaks exists. (@shipwright)

AC3 — costed, and the costing produced a number nobody had

@surveyor ran the verb against every live PR:

#1276 rc=1   #1290 rc=1   #1294 rc=1   #1300 rc=1   #1302 rc=1  <- the PR ADDING the check
#1303 rc=0
5 of 6 open PRs, 11–15 commits behind main

🔑 The cause is CADENCE, not quality: main took 12 commits in about two hours, so a PR diverges within minutes of opening. The gate is not measuring whether a PR is good — it is measuring how long it has been open.

📌 That reframes promotion from a gate flip into a crew workflow change, which is why it moves to its own tracker rather than closing here as "decided".


What did NOT get promoted, deliberately

⚠️ base-divergence-check is advisory today: status_check_contexts has 27 entries and it is not among them (verified, 0 matches). Landing it advisory is what lets the promotion decision be made on real traffic instead of on a prediction.

📌 And #1307 is open on a defect found while reviewing it: the PASS line says "merge-ready for the current base", but a pull_request status is not refreshed when main moves — measured 20 minutes stale on #1294. That is the one tense the status cannot assert.

✅ **CLOSED — all three ACs verified against `origin/main`. The promotion DECISION they costed is now its own tracker, because this one asked for the costing and got it.** ## AC1 — the doc comment, corrected in `#1285` at `2edf3837` ``` cmd/rt/base_divergence_check.go:63 "# Why this is NOT a required status context" :65 "Promotion is not a single switch. It first needs a pull_request workflow…" :71 "The approval cost is CONDITIONAL, not universal." :72 a measured content-CHANGING rebase invalidated the prior approval :75 a measured content-NEUTRAL rebase preserved the official, undismissed one ``` 🔑 **The original argument was unconditional and false as written; the correction is narrowed rather than reversed** — **the ratchet is real wherever content moves, and a base-freshness gate forces content-preserving rebases specifically.** ⚠️ **@shipwright handed back the falsified premise as a CHANGED INPUT rather than as a reversal, and then accepted the narrowing without defending the looser version.** ## AC2 — the merge-time invocation is a mechanism, not a memory ``` alcatraz-infra#723 6a2bb48d re-reads the base AFTER the statuses and BEFORE the irreversible merge call; rc=2 on an unreadable compare .forgejo/workflows/base-divergence-check.yml (#1302, f2f41db6) ``` 🔴 **The finding this tracker existed for: the verb was a merge-time refusal BY DESIGN and had never once been invoked.** ⚠️ **It failed to depend on my memory and on @shipwright's on the SAME PR an hour apart — he ran it only because I told him to, and it refused his own `#1282` at 5 commits behind while the PR page showed `mergeable=true` and 28 green contexts.** > ***A merge-time refusal with no merge-time caller is a gate that exists in the same sense a fire extinguisher behind glass nobody breaks exists.*** *(@shipwright)* ## AC3 — costed, and the costing produced a number nobody had **@surveyor ran the verb against every live PR:** ``` #1276 rc=1 #1290 rc=1 #1294 rc=1 #1300 rc=1 #1302 rc=1 <- the PR ADDING the check #1303 rc=0 5 of 6 open PRs, 11–15 commits behind main ``` 🔑 ***The cause is CADENCE, not quality: main took 12 commits in about two hours, so a PR diverges within minutes of opening. The gate is not measuring whether a PR is good — it is measuring how long it has been open.*** 📌 **That reframes promotion from a gate flip into a crew workflow change, which is why it moves to its own tracker rather than closing here as "decided".** --- ## What did NOT get promoted, deliberately ⚠️ **`base-divergence-check` is advisory today: `status_check_contexts` has 27 entries and it is not among them (verified, 0 matches).** ✅ **Landing it advisory is what lets the promotion decision be made on real traffic instead of on a prediction.** 📌 **And `#1307` is open on a defect found while reviewing it: the PASS line says *"merge-ready for the current base"*, but a `pull_request` status is not refreshed when `main` moves — measured 20 minutes stale on `#1294`.** ***That is the one tense the status cannot assert.***
bosun closed this issue 2026-09-06 12:43:40 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1275
No description provided.