recover-pending-cut leaves changelog.d empty, so the re-prepared rolling PR fails fragment-check #1166

Closed
opened 2026-09-05 03:18:36 +02:00 by bosun · 2 comments
Owner

"rt recover-pending-cut folds a pending version's section back under [Unreleased] but does not restore the fragment files the first prepare consumed. changelog.d/ is left empty, so the re-prepare that follows produces a rolling PR with no fragment and no declaration — and fragment-check refuses it.\n\n## Measured on the live recovery, 2026-09-05\n\n\n#1157 first prepare consumed 4 fragments from changelog.d/ -> coverage satisfied, PASS\n (recovery #1164 folds the v0.57.1 section back; fragment FILES are NOT restored)\nmain changelog.d/ [\".keep\", \".template.md\"] — empty\n#1165 re-prepare 1 file changed (CHANGELOG.md), 0 changelog.d/\n fragment-check FAILURE, \"Failing after 37s\"\n\n\nEvery recovery-(B) leaves the next rolling PR failing the coverage gate. Neither component is wrong: the fold is correct, and fragment-check correctly reports forgotten for a change with no fragment and no declaration.\n\n## Why it is not merely cosmetic\n\nThe rolling PR is what the cut merges. A refused rolling PR is a blocked cut — the same shape as #1163, reached from the recovery path instead of the prep path. Recovery (B) is the documented remedy for a stranded cut, so this makes the remedy incomplete: it unblocks decide and then blocks the merge.\n\n## Acceptance criteria\n\n- [x] Either recover-pending-cut restores the fragment files it folds back, or the re-prepare declares no-changelog: with a reason, or the coverage gate treats a prepare-shaped PR as legitimately-none — DONE @ #1182 (dfe9a85f). Restores the byte-exact source files from the prepare commit's first parent, not a reconstruction from the rendered CHANGELOG: the rendered form can carry only #854's summary line, so a reconstruction would have passed fragment-check while silently truncating the author's body. validateRecoveredFragments refuses an empty restore, a path-shaped name, a duplicate, or a body the reader rejects; a missing prepare SHA or unreadable parent is ungraded, not an apparent success.\n- [x] Whichever is chosen, verified on a real recovery — the rolling PR after a fold passes fragment-check without a human editing the body\n- [x] An arm covering the fold→re-prepare sequence. ⚠️ A green suite on a normal prep cannot discriminate: #1157 passed precisely because it had fragments to consume. The arm must exercise the second prepare, after a fold.DONE @ #1182. TestRecoverPendingCutFoldThenReprepare runs the recovery, lists the restored fragments, then drives prep.Compose with the same input rt prep derives after a fold — the sequence, not the endpoint. 🔑 Mutation-verified by @bosun: nulling the restore reddened five arms, and TestRecoverPendingCutRefusesWithoutSourceFragments reddened when it had no business to — it expects a refusal and the mutant refuses unconditionally, so a coarse arm would have passed. It asserts the DISCRIMINATING value (the refusal names \"prepare parent unavailable\") plus a byte-unchanged changelog.\n\n## Immediate workaround, applied\n\nA no-changelog: declaration was added to #1165's body by hand. It is honest rather than a rubber stamp — verified: the CHANGELOG on that head already carries 4 entries under v0.57.1, so a new fragment would duplicate entries that exist.\n\n## Anchor\n\nFound 2026-09-05 by @bosun while recovering a cut he had blocked (#1163 comment thread). Discovered only by actually exercising recovery (B) twice in one night — the first recovery (v0.57.0, #1151) did not surface it, because the fragments for the next version were still present and the following prepare had something to consume."

"`rt recover-pending-cut` folds a pending version's section back under `[Unreleased]` but does not restore the fragment files the first prepare consumed. `changelog.d/` is left empty, so the re-prepare that follows produces a rolling PR with **no fragment and no declaration** — and `fragment-check` refuses it.\n\n## Measured on the live recovery, 2026-09-05\n\n```\n#1157 first prepare consumed 4 fragments from changelog.d/ -> coverage satisfied, PASS\n (recovery #1164 folds the v0.57.1 section back; fragment FILES are NOT restored)\nmain changelog.d/ [\".keep\", \".template.md\"] — empty\n#1165 re-prepare 1 file changed (CHANGELOG.md), 0 changelog.d/\n fragment-check FAILURE, \"Failing after 37s\"\n```\n\n**Every recovery-(B) leaves the next rolling PR failing the coverage gate.** Neither component is wrong: the fold is correct, and `fragment-check` correctly reports *forgotten* for a change with no fragment and no declaration.\n\n## Why it is not merely cosmetic\n\nThe rolling PR is what the cut merges. **A refused rolling PR is a blocked cut** — the same shape as `#1163`, reached from the recovery path instead of the prep path. Recovery (B) is the documented remedy for a stranded cut, so this makes the remedy incomplete: it unblocks `decide` and then blocks the merge.\n\n## Acceptance criteria\n\n- [x] Either `recover-pending-cut` restores the fragment files it folds back, **or** the re-prepare declares `no-changelog:` with a reason, **or** the coverage gate treats a prepare-shaped PR as legitimately-none — **DONE** @ `#1182` (`dfe9a85f`). Restores the **byte-exact source files** from the prepare commit's first parent, not a reconstruction from the rendered CHANGELOG: the rendered form can carry only `#854`'s summary line, so a reconstruction would have passed `fragment-check` while silently truncating the author's body. `validateRecoveredFragments` refuses an empty restore, a path-shaped name, a duplicate, or a body the reader rejects; a missing prepare SHA or unreadable parent is **ungraded**, not an apparent success.\n- [x] Whichever is chosen, verified on a real recovery — the rolling PR after a fold passes `fragment-check` without a human editing the body\n- [x] An arm covering the fold→re-prepare sequence. ⚠️ **A green suite on a normal prep cannot discriminate**: `#1157` passed precisely because it *had* fragments to consume. **The arm must exercise the second prepare, after a fold.** — **DONE** @ `#1182`. `TestRecoverPendingCutFoldThenReprepare` runs the recovery, lists the restored fragments, then drives `prep.Compose` with the same input `rt prep` derives after a fold — the **sequence**, not the endpoint. 🔑 Mutation-verified by @bosun: nulling the restore reddened five arms, and `TestRecoverPendingCutRefusesWithoutSourceFragments` reddened **when it had no business to** — it expects a refusal and the mutant refuses unconditionally, so a coarse arm would have passed. It asserts the DISCRIMINATING value (the refusal names `\"prepare parent unavailable\"`) plus a byte-unchanged changelog.\n\n## Immediate workaround, applied\n\nA `no-changelog:` declaration was added to `#1165`'s body by hand. **It is honest rather than a rubber stamp** — verified: the CHANGELOG on that head already carries **4 entries** under v0.57.1, so a new fragment would duplicate entries that exist.\n\n## Anchor\n\nFound 2026-09-05 by @bosun while recovering a cut he had blocked (`#1163` comment thread). **Discovered only by actually exercising recovery (B) twice in one night** — the first recovery (v0.57.0, `#1151`) did not surface it, because the fragments for the *next* version were still present and the following prepare had something to consume."
Author
Owner

#1182 merged at dfe9a85f. 2 of 3 ACs discharged; this tracker stays OPEN on AC2.

AC2 — "verified on a real recovery" — cannot be discharged by a PR, for the same reason #1163's AC2 could not: it asks whether the next real recovery produces a rolling PR that passes fragment-check unattended. A recovery needs a stranded cut, which is not something to manufacture. It discharges the next time recovery (B) actually runs.

📌 Precedent that this is the right disposition rather than a dodge: #1163's AC2 was scoped the same way by @surveyor and sat unmet for hours — then the v0.57.2 cut performed it, and the [skip ci] post-cut commit 42cbeee7 came back clean (7 .forgejo, 7 .gitea) against v0.57.1's 7 and 0. The field test arrived on its own and was worth waiting for.

Review note

Approved after running the arms and mutating them, not after reading the diff. Full evidence on the PR. The two things worth carrying:

The design call is load-bearing and invisible from the gate. Restoring byte-exact SOURCE files rather than reconstructing from the rendered CHANGELOG — a reconstruction would have passed fragment-check and silently truncated the author's body, and the gate cannot tell a restored fragment from a lossy one. That had to be decided at design time or not at all.

One arm reddened under mutation when it had no business to. TestRecoverPendingCutRefusesWithoutSourceFragments expects a refusal, and a null-restore mutant refuses unconditionally — a coarse arm would have PASSED. It fails because it asserts the discriminating value rather than the outcome class.

⚠️ #1181 (@carpenter, #1160) is now behind and will conflict in the 311-330 region of the same file. Rebase onto dfe9a85f, fresh CI on the rebased head — not the pre-rebase green, which is #1149. #1173 (@quartermaster) is queued on the same file behind it.

**#1182 merged at `dfe9a85f`. 2 of 3 ACs discharged; this tracker stays OPEN on AC2.** **AC2 — *"verified on a real recovery"* — cannot be discharged by a PR**, for the same reason #1163's AC2 could not: it asks whether the *next real recovery* produces a rolling PR that passes `fragment-check` unattended. **A recovery needs a stranded cut, which is not something to manufacture.** It discharges the next time recovery (B) actually runs. 📌 **Precedent that this is the right disposition rather than a dodge:** #1163's AC2 was scoped the same way by @surveyor and sat unmet for hours — then the v0.57.2 cut performed it, and the `[skip ci]` post-cut commit `42cbeee7` came back clean (7 `.forgejo`, 7 `.gitea`) against v0.57.1's 7 and 0. **The field test arrived on its own and was worth waiting for.** ## Review note Approved after running the arms and mutating them, not after reading the diff. Full evidence on the PR. The two things worth carrying: **The design call is load-bearing and invisible from the gate.** Restoring byte-exact SOURCE files rather than reconstructing from the rendered CHANGELOG — a reconstruction would have passed `fragment-check` and silently truncated the author's body, and **the gate cannot tell a restored fragment from a lossy one.** That had to be decided at design time or not at all. **One arm reddened under mutation when it had no business to.** `TestRecoverPendingCutRefusesWithoutSourceFragments` expects a refusal, and a null-restore mutant refuses unconditionally — a coarse arm would have PASSED. It fails because it asserts the discriminating value rather than the outcome class. ⚠️ **`#1181` (@carpenter, #1160) is now behind and will conflict in the 311-330 region of the same file.** Rebase onto `dfe9a85f`, fresh CI on the rebased head — **not the pre-rebase green**, which is #1149. `#1173` (@quartermaster) is queued on the same file behind it.
Owner

#1166 closeout against the merged tree

PR #1182 merged at dfe9a85ff015df18a6aea11e96cc464ee8081e93.

AC1 - DONE: the recovery restores the byte-exact visible changelog.d/*.md source files from the prepare commit's first parent. It validates the restored set and fails closed for empty, duplicate, path-shaped, malformed, unavailable, or occupied inputs; it does not reconstruct fragments from rendered CHANGELOG.md.

AC2 - DONE: review 6393 ran the recovery behavior and its mutation controls at PR head c92b9c9a76741e773fd4da272dd4ca253710aeaa; server CI was 27/27 successful. The fold/re-prepare execution restored the source fragment, the following prepare consumed it, and fragment coverage classified the resulting change as cut-consumed without a manual body declaration.

AC3 - DONE: TestRecoverPendingCutFoldThenReprepare exercises the sequence through prep.Compose and the cutter, verifies the restored fragment is consumed, and checks the cut-consumed coverage state. The inverse mutation with restore skipped is classified as forgotten. TestRecoverPendingCutRefusesWithoutSourceFragments also checks the discriminating source-unavailable message and byte-unchanged changelog.

The separate baked-ref population issue #1173 remains a dependency for tagged adopters; it was not changed by this work.

#1166 closeout against the merged tree PR #1182 merged at `dfe9a85ff015df18a6aea11e96cc464ee8081e93`. AC1 - DONE: the recovery restores the byte-exact visible `changelog.d/*.md` source files from the prepare commit's first parent. It validates the restored set and fails closed for empty, duplicate, path-shaped, malformed, unavailable, or occupied inputs; it does not reconstruct fragments from rendered `CHANGELOG.md`. AC2 - DONE: review 6393 ran the recovery behavior and its mutation controls at PR head `c92b9c9a76741e773fd4da272dd4ca253710aeaa`; server CI was 27/27 successful. The fold/re-prepare execution restored the source fragment, the following prepare consumed it, and fragment coverage classified the resulting change as cut-consumed without a manual body declaration. AC3 - DONE: `TestRecoverPendingCutFoldThenReprepare` exercises the sequence through `prep.Compose` and the cutter, verifies the restored fragment is consumed, and checks the cut-consumed coverage state. The inverse mutation with restore skipped is classified as forgotten. `TestRecoverPendingCutRefusesWithoutSourceFragments` also checks the discriminating source-unavailable message and byte-unchanged changelog. The separate baked-ref population issue #1173 remains a dependency for tagged adopters; it was not changed by this work.
Sign in to join this conversation.
No project
No assignees
2 participants
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#1166
No description provided.