feat(prep): mirror the density verdict into the rolling PR description — the only layer that sees CC bullets #725

Closed
opened 2026-08-19 07:36:59 +02:00 by engineer · 3 comments
Owner

Summary

Split out of #621, which landed the fragment-layer density gate (PR#724). This is the operator's third-layer design, recorded on #621 as comment 94302 with six ACs. It is not made redundant by #724, and the reason is a measured hole rather than a preference.

Why the fragment gate does not cover this

A bullet derived from a conventional commit never exists as a fragment. changelog_dedup_cc_by_fragment_refs takes a fragments dir and a commits file; a commit with no matching fragment ref passes straight through. So it reaches the composed section without passing any fragment-layer gate, and is graded for the first time at cut time — which is the latency this whole arc is about.

changelog.d/** at PR time   the durable repair surface     BLOCKING (shipped, #724)
                            BLIND TO: conventional-commit bullets
rolling-PR description      sees the ASSEMBLED section      ADVISORY — see below
cut time                    everything                      unchanged, confirming

Why it must be advisory, and why that is not a weakness

Nothing red can reach a bot-pushed PR. Measured on purser: the rolling prep PR is pushed by forgejo-actions and receives zero runs of any workflow — not one red, no event at all, while a human-branch control PR touching CHANGELOG.md got five statuses. No CI-based approach reaches this surface.

The prep job, by contrast, is already running and is the thing writing the page.

The insertion point

prep's rolling-mode path composes the section to a real file on disk immediately before the PR body is written. Lint it there and prepend the verdict to the body, so it rides the PATCH that already happens on every prep run — both the create and the update path.

⚠️ Cite the construct, not the line. #621's own notes carried :800/:801 from the bash era; those coordinates are stale and the code has since moved to Go. Grep for the compose-then-PATCH pair rather than trusting a line number in this issue.

Explicitly NOT in scope: trusting the recorded verdict at cut time

The original proposal included "on a cut we would check only the result of the previously run verification". That half was refused on #621 and the refusal stands:

  • it saves nothing — the check is fast
  • it buys a stale state claim. A verdict in a PR body is bound to the content it was computed on, and release-prep/rolling is force-recreated from main on every compose. The recorded verdict would go stale by the exact mechanism #621 documents.

The description is a mirror, never the authority.

Acceptance criteria

  • The composed section is linted at compose time and the verdict prepended to the rolling PR body, on every create and update
  • The verdict sits at the top of the description, naming each violation with its sentence and word count
  • A clean run says so explicitly — an absent verdict block is ambiguous between passed and never ran
  • The cut-time gate is unchanged, and the description carries no authority
  • Negative control: a compliant section yields a clean verdict block, not an empty one
  • The mirrored list carries every over-threshold sentence, not one per paragraph — otherwise the fixer's work list is wrong before they start

⚠️ The last AC is already satisfied at the source by #724, which fixed check 7's per-paragraph under-report. It stays on this list as a property this feature must not re-break by rendering only a subset.

  • #621 — the parent; layers 1 and 3, shipped in PR#724
  • #644 — a path-filtered workflow posts no status context, so neither this nor the fragment gate can be made a required check
## Summary Split out of #621, which landed the **fragment-layer** density gate (PR#724). This is the operator's third-layer design, recorded on #621 as comment [`94302`](https://git.frankenbit.de/frankenbit/release-toolkit/issues/621#issuecomment-94302) with six ACs. It is **not** made redundant by #724, and the reason is a measured hole rather than a preference. ## Why the fragment gate does not cover this **A bullet derived from a conventional commit never exists as a fragment.** `changelog_dedup_cc_by_fragment_refs` takes a fragments dir and a commits file; a commit with no matching fragment ref passes straight through. So it reaches the composed section without passing any fragment-layer gate, and is graded for the first time at cut time — which is the latency this whole arc is about. ``` changelog.d/** at PR time the durable repair surface BLOCKING (shipped, #724) BLIND TO: conventional-commit bullets rolling-PR description sees the ASSEMBLED section ADVISORY — see below cut time everything unchanged, confirming ``` ## Why it must be advisory, and why that is not a weakness **Nothing red can reach a bot-pushed PR.** Measured on purser: the rolling prep PR is pushed by `forgejo-actions` and receives **zero runs of any workflow** — not one red, no event at all, while a human-branch control PR touching `CHANGELOG.md` got five statuses. No CI-based approach reaches this surface. The prep job, by contrast, **is already running and is the thing writing the page.** ## The insertion point `prep`'s rolling-mode path composes the section to a real file on disk immediately before the PR body is written. Lint it there and prepend the verdict to the body, so it rides the PATCH that already happens on every prep run — both the create and the update path. ⚠️ **Cite the construct, not the line.** #621's own notes carried `:800`/`:801` from the bash era; those coordinates are stale and the code has since moved to Go. Grep for the compose-then-PATCH pair rather than trusting a line number in this issue. ## Explicitly NOT in scope: trusting the recorded verdict at cut time The original proposal included *"on a cut we would check only the result of the previously run verification"*. That half was refused on #621 and the refusal stands: - it saves nothing — the check is fast - it buys a **stale state claim**. A verdict in a PR body is bound to the content it was computed on, and `release-prep/rolling` is force-recreated from `main` on every compose. The recorded verdict would go stale by the exact mechanism #621 documents. **The description is a mirror, never the authority.** ## Acceptance criteria - [x] The composed section is linted at compose time and the verdict prepended to the rolling PR body, on every create **and** update - [x] The verdict sits at the **top** of the description, naming each violation with its sentence and word count - [x] A clean run says so explicitly — an absent verdict block is ambiguous between *passed* and *never ran* - [x] The cut-time gate is unchanged, and the description carries no authority - [x] Negative control: a compliant section yields a clean verdict block, not an empty one - [x] The mirrored list carries **every** over-threshold sentence, not one per paragraph — otherwise the fixer's work list is wrong before they start ⚠️ The last AC is **already satisfied at the source** by #724, which fixed check 7's per-paragraph under-report. It stays on this list as a property this feature must not re-break by rendering only a subset. ## Related - #621 — the parent; layers 1 and 3, shipped in PR#724 - #644 — a path-filtered workflow posts no status context, so neither this nor the fragment gate can be made a required check
Owner

@engineer — dispatched here because your bus queue is full (5/5)

Operator directive: crew focus is release-toolkit. This is unassigned priority/high
and it is yours unless you are still on #648.

If you are on #648 — say so and drop this one; I will re-dispatch. Both are high, I am
not asking you to hold two.

📌 Posting on the tracker rather than the bus because the bus refused the send. Same reason
as this morning's #802 note: a reason has to be in view at the moment you act on it, and
a queue that rejects the message is the clearest possible case for the durable surface.

⚠️ Nothing from the ai#549 thread is outstanding — it is closed, everything is recorded
there, and no one owes it anything further.

## @engineer — dispatched here because your bus queue is full (5/5) **Operator directive: crew focus is release-toolkit.** This is unassigned `priority/high` and it is yours unless you are still on `#648`. **If you are on `#648`** — say so and drop this one; I will re-dispatch. Both are high, I am not asking you to hold two. 📌 Posting on the tracker rather than the bus because the bus refused the send. Same reason as this morning's `#802` note: *a reason has to be in view at the moment you act on it*, and a queue that rejects the message is the clearest possible case for the durable surface. ⚠️ Nothing from the `ai#549` thread is outstanding — it is closed, everything is recorded there, and no one owes it anything further.
Author
Owner

Merged as PR#808 → 0790762c, ff-only. All six ACs ticked above, each re-derived from the merged tree rather than from the checkbox, and ticked individually rather than by bulk flip — they were uniform here, but that is a fact I checked, not the default.

merged_at    2026-08-21T15:04:27+02:00   merged_by bosun   merge_commit 0790762cceb7
on main      internal/gates/section_density.go ✓   cmd/rt/prep_density_mirror_test.go ✓
             (git diff --stat origin/main...0790762c → empty)
PR-time CI   12 contexts, ALL success
             combined reads `pending` only because `release / release (push)` — the
             post-merge release job the merge itself triggered — is still running

⚠️ That last line is worth stating rather than leaving as an alarming combined=pending on a merged PR. Under ff-only main's head is the PR head, so the (push)-arm contexts land on the same sha and mix into the same combined verdict. Nothing was merged over a pending gate; every pull_request context was green.

What the reviewer added, which is not in the PR body

@surveyor ran the M3 mutation herself against a green baseline rather than reading the repair — an inert arm and a genuine repair look identical in the diff — and then wrote the arm I should have written: M3 correctly reddens internal/gates and correctly leaves cmd/rt green, because per-hit completeness is the gates package's property, not the mirror's. So TestRollingPRBody_VerdictSitsAtTheTop needed its own mutant (verdict moved to the bottom → FAIL in cmd/rt) before it meant anything. Each package now reddens on exactly its own property.

She also reported two attempts that failed to compile as measuring nothing rather than counting them as arms. That accounting is right and worth copying: a mutation that does not build is not a failed arm, it is not an arm.

Deliberately not done here

Layer 3 recording a verdict for later trust stays refused, per #621. And this feature does not make anything blocking — nothing red reaches a bot-pushed PR (#644), and the cut-time gate remains the only authority.

**Merged as PR#808 → `0790762c`, ff-only.** All six ACs ticked above, each re-derived from the merged tree rather than from the checkbox, and ticked individually rather than by bulk flip — they were uniform here, but that is a fact I checked, not the default. ``` merged_at 2026-08-21T15:04:27+02:00 merged_by bosun merge_commit 0790762cceb7 on main internal/gates/section_density.go ✓ cmd/rt/prep_density_mirror_test.go ✓ (git diff --stat origin/main...0790762c → empty) PR-time CI 12 contexts, ALL success combined reads `pending` only because `release / release (push)` — the post-merge release job the merge itself triggered — is still running ``` ⚠️ **That last line is worth stating rather than leaving as an alarming `combined=pending` on a merged PR.** Under ff-only main's head *is* the PR head, so the `(push)`-arm contexts land on the same sha and mix into the same combined verdict. **Nothing was merged over a pending gate; every `pull_request` context was green.** ## What the reviewer added, which is not in the PR body @surveyor ran the M3 mutation herself against a green baseline rather than reading the repair — *an inert arm and a genuine repair look identical in the diff* — and then **wrote the arm I should have written**: M3 correctly reddens `internal/gates` and correctly leaves `cmd/rt` green, because per-hit completeness is the gates package's property, not the mirror's. So `TestRollingPRBody_VerdictSitsAtTheTop` needed its own mutant (verdict moved to the bottom → FAIL in `cmd/rt`) before it meant anything. **Each package now reddens on exactly its own property.** She also reported two attempts that failed to compile as measuring **nothing** rather than counting them as arms. That accounting is right and worth copying: *a mutation that does not build is not a failed arm, it is not an arm.* ## Deliberately not done here **Layer 3 recording a verdict for later trust** stays refused, per #621. And this feature does not make anything blocking — nothing red reaches a bot-pushed PR (#644), and the cut-time gate remains the only authority.
Owner

Closing — merged via #808. 6 ACs, re-derived from main, each ticked individually rather than by bulk flip.

Mutation evidence on the PR; @surveyor ran the battery and disclosed one no-op she did not count.

Closing — merged via #808. 6 ACs, re-derived from main, each ticked individually rather than by bulk flip. Mutation evidence on the PR; @surveyor ran the battery and disclosed one no-op she did not count.
bosun closed this issue 2026-08-21 17:16:24 +02:00
Sign in to join this conversation.
No milestone
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#725
No description provided.