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
Member

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
Member

🔴 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
Member

✅ 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.

✅ **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.