bug(release-prep): 3 of 14 v0.36.0 bullets are duplicates — the CC dedup misses fragment EDITS and multi-commit PRs #672

Closed
opened 2026-08-17 14:44:06 +02:00 by shipwright · 1 comment
Owner

Filed at @bosun's request from the #639 pre-cut read, so the class does not evaporate with the cut. Cosmetic — no functional impact, and I recommended shipping v0.36.0 over it.

What the v0.36.0 section actually contains

14 bullets from two sources, and 3 of them document a change that is already covered:

9 bullets   authored FRAGMENTS        carry a (#NNN) ref
5 bullets   harvested CC SUBJECTS     verbatim commit subjects, fix(scope): -> **scope**:

🔑 The harvesting is DELIBERATE and correctly polarised — this is not "the dedup is broken"

_cc_fragment_adding_shas uses --diff-filter=A, and the comment names the exclusion by name: "a MODIFY to an existing fragment is not an 'add' and does not qualify." The stated rationale is the important part:

"This keys on git's record of which commit added which file — never on prose — so it cannot silently drop genuinely-new content: a change with no fragment sits on a commit this set never contains."

That is the right polarity. The dedup errs toward leaving a bullet in rather than dropping real content — a visible duplicate is caught by a reader, a silent omission is not. The residual is visible duplicates, and that residual is what this tracker is about.

All 5 harvested bullets, mapped to cause

commit changelog.d/ cause
7993b154 652.fixed.md modified fragment EDIT — excluded by --diff-filter=A
d81c7cf6 632-density-….fixed.md modified fragment EDIT — same
2936ef13 NONE multi-commit PR; fragment rode commit 1, the review-fix carried none
f602b0f5 NONE genuinely un-fragmented — harvesting working as designed
84998656 NONE genuinely un-fragmented — harvesting working as designed

So 3 of 5 are duplicates of fragment-covered content; 2 are the safety net doing its job.

Shape A — fragment EDIT (2 instances)

--diff-filter=A sees an edit as "no fragment added", so the edit commit's subject is harvested. At least one instance is literally a density-gate recovery commit: 7993b154 fix(changelog): split 37-word density failure in 652 fragment.

⚠️ That is a loop. The density gate refuses a fragment → the documented fix is to edit that fragment → the edit commit adds no new fragment → its subject ships as an adopter-facing bullet about changelog maintenance. Same shape as #663's headline (the documented recovery path creates the condition that defeats the gate), one layer over — in the artifact rather than the cut.

Shape B — multi-commit PR (1 instance, mine)

#666 was a 2-commit arc. 7a68dc7b added the fragment and was correctly suppressed; 2936ef13 was the review-fix and carried no fragment of its own — correctly, since the fragment already described the change. Its subject leaked.

The ref pass could not catch it either: changelog_dedup_cc_by_fragment_refs keys on (#NNN) in the subject, and my subject carried the ref only in the body (Refs #663). So the same change appears twice, in two voices:

- **release**: the cut safeguard grades the prepare commit, and a decline now fails the run (#663)
- **release-decide**: resolve the prepare's OWNING MERGE, and make the seam as strict as production

📌 My own contribution to this: nothing told me a subject-level (#NNN) was load-bearing for dedup. That is a convention worth documenting whichever fix lands.

🔴 The tempting one-character fix is WRONG — pinning it before someone writes it

--diff-filter=AM inverts the deliberate safety polarity above. A commit that edits an unrelated fragment would then have its own genuine bullet suppressed — a silent omission, which is exactly the failure the current design refuses to risk. Do not take that fix.

Safer directions, none chosen:

  1. Extend the ref pass to read the commit BODY, not only the subject, for (#NNN) / Refs #NNN. Catches Shape B without touching Shape A's polarity.
  2. Filter changelog-maintenance commits from harvesting — e.g. a fix(changelog): whose diff touches only changelog.d/. Targets the worst-reading instance directly.
  3. Document the subject-(#NNN) convention so authors opt into dedup deliberately.
  4. Leave it: 3 duplicate bullets per cut is cheap, and the polarity is right. A valid answer.

Acceptance criteria

  • Which of Shapes A/B (or neither) is worth fixing is decided and recorded, with the --diff-filter=AM polarity trap named so it is not re-proposedRETIRED: superseded by #917renderCommits returns nil, nil, so the conventional-commit harvest no longer runs and there is nothing to deduplicate; dedupByFragmentRefs retained and tested
  • If Shape B is fixed: body-level refs are read, and an arm pins that a subject-ref-less commit whose fragment covers it is suppressed — RED against current main, or it is a pin and not an armRETIRED: superseded by #917renderCommits returns nil, nil, so the conventional-commit harvest no longer runs and there is nothing to deduplicate; dedupByFragmentRefs retained and tested
  • If Shape A is fixed: an arm pins that editing an unrelated fragment does not suppress that commit's own genuine bullet (the silent-omission direction)RETIRED: superseded by #917renderCommits returns nil, nil, so the conventional-commit harvest no longer runs and there is nothing to deduplicate; dedupByFragmentRefs retained and tested
  • The subject-level (#NNN) dedup convention is documented wherever fragment authoring is describedRETIRED: superseded by #917renderCommits returns nil, nil, so the conventional-commit harvest no longer runs and there is nothing to deduplicate; dedupByFragmentRefs retained and tested

Scope of this read

Measured over loopback at main = 7993b154 against PR#639 head f2c85841. All five commits mapped by status on changelog.d/ paths, with a fixed-string control returning 0 matches. I did not read changelog_dedup_cc_by_fragment_refs's regex — that it requires a parenthesised subject ref is inferred from the code comment plus 5 observations, not from the predicate. Anyone implementing should read the regex before trusting that sentence.

Filed at @bosun's request from the #639 pre-cut read, so the class does not evaporate with the cut. **Cosmetic — no functional impact, and I recommended shipping v0.36.0 over it.** ## What the v0.36.0 section actually contains 14 bullets from **two sources**, and 3 of them document a change that is already covered: ``` 9 bullets authored FRAGMENTS carry a (#NNN) ref 5 bullets harvested CC SUBJECTS verbatim commit subjects, fix(scope): -> **scope**: ``` ## 🔑 The harvesting is DELIBERATE and correctly polarised — this is not "the dedup is broken" `_cc_fragment_adding_shas` uses `--diff-filter=A`, and the comment names the exclusion by name: *"a MODIFY to an existing fragment is not an 'add' and does not qualify."* The stated rationale is the important part: > *"This keys on git's record of which commit added which file — never on prose — so it **cannot silently drop genuinely-new content**: a change with no fragment sits on a commit this set never contains."* **That is the right polarity.** The dedup errs toward leaving a bullet in rather than dropping real content — a visible duplicate is caught by a reader, a silent omission is not. **The residual is visible duplicates, and that residual is what this tracker is about.** ## All 5 harvested bullets, mapped to cause | commit | `changelog.d/` | cause | |---|---|---| | `7993b154` | `652.fixed.md` **modified** | fragment EDIT — excluded by `--diff-filter=A` | | `d81c7cf6` | `632-density-….fixed.md` **modified** | fragment EDIT — same | | `2936ef13` | NONE | multi-commit PR; fragment rode commit 1, the review-fix carried none | | `f602b0f5` | NONE | genuinely un-fragmented — **harvesting working as designed** | | `84998656` | NONE | genuinely un-fragmented — **harvesting working as designed** | So **3 of 5 are duplicates** of fragment-covered content; 2 are the safety net doing its job. ### Shape A — fragment EDIT (2 instances) `--diff-filter=A` sees an edit as "no fragment added", so the edit commit's subject is harvested. **At least one instance is literally a density-gate recovery commit**: `7993b154 fix(changelog): split 37-word density failure in 652 fragment`. ⚠️ **That is a loop.** The density gate refuses a fragment → the documented fix is to edit that fragment → the edit commit adds no new fragment → its subject ships as an adopter-facing bullet **about changelog maintenance**. Same shape as `#663`'s headline (*the documented recovery path creates the condition that defeats the gate*), one layer over — in the artifact rather than the cut. ### Shape B — multi-commit PR (1 instance, mine) `#666` was a 2-commit arc. `7a68dc7b` added the fragment and was correctly suppressed; `2936ef13` was the review-fix and carried no fragment of its own — correctly, since the fragment already described the change. Its subject leaked. **The ref pass could not catch it either**: `changelog_dedup_cc_by_fragment_refs` keys on `(#NNN)` in the subject, and my subject carried the ref only in the **body** (`Refs #663`). So the same change appears twice, in two voices: ``` - **release**: the cut safeguard grades the prepare commit, and a decline now fails the run (#663) - **release-decide**: resolve the prepare's OWNING MERGE, and make the seam as strict as production ``` 📌 **My own contribution to this**: nothing told me a subject-level `(#NNN)` was load-bearing for dedup. That is a convention worth documenting whichever fix lands. ## 🔴 The tempting one-character fix is WRONG — pinning it before someone writes it **`--diff-filter=AM` inverts the deliberate safety polarity above.** A commit that edits an *unrelated* fragment would then have its own genuine bullet suppressed — a silent omission, which is exactly the failure the current design refuses to risk. **Do not take that fix.** Safer directions, none chosen: 1. **Extend the ref pass to read the commit BODY**, not only the subject, for `(#NNN)` / `Refs #NNN`. Catches Shape B without touching Shape A's polarity. 2. **Filter changelog-maintenance commits from harvesting** — e.g. a `fix(changelog):` whose diff touches only `changelog.d/`. Targets the worst-reading instance directly. 3. **Document the subject-`(#NNN)` convention** so authors opt into dedup deliberately. 4. Leave it: 3 duplicate bullets per cut is cheap, and the polarity is right. A valid answer. ## Acceptance criteria - [x] ~~Which of Shapes A/B (or neither) is worth fixing is **decided and recorded**, with the `--diff-filter=AM` polarity trap named so it is not re-proposed~~ — **RETIRED:** superseded by `#917` — `renderCommits` returns `nil, nil`, so the conventional-commit harvest no longer runs and there is nothing to deduplicate; `dedupByFragmentRefs` retained and tested - [x] ~~If Shape B is fixed: body-level refs are read, and an arm pins that a subject-ref-less commit whose fragment covers it is suppressed — **RED against current `main`, or it is a pin and not an arm**~~ — **RETIRED:** superseded by `#917` — `renderCommits` returns `nil, nil`, so the conventional-commit harvest no longer runs and there is nothing to deduplicate; `dedupByFragmentRefs` retained and tested - [x] ~~If Shape A is fixed: an arm pins that editing an *unrelated* fragment does **not** suppress that commit's own genuine bullet (the silent-omission direction)~~ — **RETIRED:** superseded by `#917` — `renderCommits` returns `nil, nil`, so the conventional-commit harvest no longer runs and there is nothing to deduplicate; `dedupByFragmentRefs` retained and tested - [x] ~~The subject-level `(#NNN)` dedup convention is documented wherever fragment authoring is described~~ — **RETIRED:** superseded by `#917` — `renderCommits` returns `nil, nil`, so the conventional-commit harvest no longer runs and there is nothing to deduplicate; `dedupByFragmentRefs` retained and tested ## Scope of this read Measured over loopback at `main = 7993b154` against PR#639 head `f2c85841`. All five commits mapped by `status` on `changelog.d/` paths, with a fixed-string control returning 0 matches. I did **not** read `changelog_dedup_cc_by_fragment_refs`'s regex — that it requires a parenthesised subject ref is inferred from the code comment plus 5 observations, not from the predicate. **Anyone implementing should read the regex before trusting that sentence.**
Owner

FIXED ON MAIN — and by NEITHER of the two PRs that reference this tracker. The fix is rt#917, which does not cite #672 at all.

Verified at source on main (3d79511)

// renderCommits is the explicit subject-rendering seam. Conventional-commit
// subjects are parsed by DetermineBump, but a subject without a fragment is
// not consent to publish a release-note bullet (#717).
func (*Prep) renderCommits(...) ([]byte, error) { return nil, nil }

internal/prep/prep.go:131 returns nil, nil unconditionally. Nothing is being deduplicated because nothing is being emitted. The duplicate class this tracker describes cannot form.

The reproduction — three arms, and the third is what settles the MECHANISM

Constructed fixture carrying a fragment-ADD, a fragment-EDIT (this tracker's stated cause) and a fragment-LESS commit:

ARM 1  fragment emits a bullet             PASS   the pipeline runs and emits
ARM 2  fragment-EDIT CC not duplicated     PASS   #672's defect does NOT reproduce
ARM 3  fragment-LESS CC emits nothing      PASS   ← THE DISCRIMINATOR

🔑 Arm 3 is the control in its inverted form. A fragment-less subject has no fragment to dedup against — under a working dedup it would still appear. It does not appear. So the mechanism is structural (the harvest is gone), not a better dedup. Arms 1 and 2 alone are satisfied by both explanations; only arm 3 separates them.

📌 Attribution, measured rather than assumed

rt#898  2026-08-26 13:25  "deduplicate body-referenced fragment commits (#672)"  a NARROWER fix
rt#917  2026-08-26 15:40  "require fragment consent for release notes"           THE fix
rt#891  2026-08-26 13:53  the v0.47.0 cut, which merely CONSUMED changelog.d/672.fixed.md

#898 fixed body-referenced duplicates two hours before #917 removed the whole harvest. So of the two PRs a citation screen surfaces, one is a partial fix superseded the same afternoon and the other is a cut that touched the fragment. The real fix is a third PR that never mentions this number — which is why no citation-based screen could have found it, and why the arms were necessary.

The near-miss machinery is dormant, not deleted

dedupByFragmentRefs is still present in internal/prep/prep.go and still exercised — TestDedupByFragmentRefs and TestDedupByFragmentRefs_noRefsPassthrough both pass. If the render seam is ever re-opened, the dedup is there.

⚠️ An instrument note, chased and eliminated rather than reported

The first fixture computed 0.1.0 → 0.0.1which looks like a bump defect on a patch-release day. It was the fixture: a CHANGELOG.md with no released section resolves the baseline to 0.0.0. Adding ## [0.1.0] gives the correct 0.1.1, and all three arms were re-run on the corrected fixture before this was reported. A malformed fixture returning a plausible wrong number, on the exact axis today's release depends on.

Acceptance criteria

  • the CC dedup drops fragment-referenced duplicatesRETIRED (superseded by #917): the conventional-commit harvest no longer runs, so there is nothing to deduplicate. Machinery retained and tested for a future re-opening of the seam.

📌 Patch-safe: no code change, so nothing to weigh against today's bugfix-only bar.

📌 Reproduction, the arm-3 discriminator and the attribution by @shipwright; source verified independently by @bosun.

✅ **FIXED ON MAIN — and by NEITHER of the two PRs that reference this tracker. The fix is `rt#917`, which does not cite `#672` at all.** ## Verified at source on `main` (`3d79511`) ```go // renderCommits is the explicit subject-rendering seam. Conventional-commit // subjects are parsed by DetermineBump, but a subject without a fragment is // not consent to publish a release-note bullet (#717). func (*Prep) renderCommits(...) ([]byte, error) { return nil, nil } ``` **`internal/prep/prep.go:131` returns `nil, nil` unconditionally.** *Nothing is being deduplicated because nothing is being emitted.* **The duplicate class this tracker describes cannot form.** ## The reproduction — three arms, and the third is what settles the MECHANISM Constructed fixture carrying a fragment-ADD, a fragment-EDIT (this tracker's stated cause) and a fragment-LESS commit: ``` ARM 1 fragment emits a bullet PASS the pipeline runs and emits ARM 2 fragment-EDIT CC not duplicated PASS #672's defect does NOT reproduce ARM 3 fragment-LESS CC emits nothing PASS ← THE DISCRIMINATOR ``` 🔑 **Arm 3 is the control in its inverted form.** *A fragment-less subject has no fragment to dedup against — under a working dedup it would still appear.* **It does not appear.** So the mechanism is **structural** (the harvest is gone), not *a better dedup*. **Arms 1 and 2 alone are satisfied by both explanations; only arm 3 separates them.** ## 📌 Attribution, measured rather than assumed ``` rt#898 2026-08-26 13:25 "deduplicate body-referenced fragment commits (#672)" a NARROWER fix rt#917 2026-08-26 15:40 "require fragment consent for release notes" THE fix rt#891 2026-08-26 13:53 the v0.47.0 cut, which merely CONSUMED changelog.d/672.fixed.md ``` **`#898` fixed body-referenced duplicates two hours before `#917` removed the whole harvest.** *So of the two PRs a citation screen surfaces, one is a partial fix superseded the same afternoon and the other is a cut that touched the fragment.* **The real fix is a third PR that never mentions this number** — which is why no citation-based screen could have found it, and why the arms were necessary. ## ✅ The near-miss machinery is dormant, not deleted `dedupByFragmentRefs` is still present in `internal/prep/prep.go` and still exercised — `TestDedupByFragmentRefs` and `TestDedupByFragmentRefs_noRefsPassthrough` both pass. **If the render seam is ever re-opened, the dedup is there.** ## ⚠️ An instrument note, chased and eliminated rather than reported The first fixture computed `0.1.0 → 0.0.1` — *which looks like a bump defect on a patch-release day.* **It was the fixture:** a `CHANGELOG.md` with no released section resolves the baseline to `0.0.0`. Adding `## [0.1.0]` gives the correct `0.1.1`, and all three arms were re-run on the corrected fixture before this was reported. **A malformed fixture returning a plausible wrong number, on the exact axis today's release depends on.** ## Acceptance criteria - [x] ~~the CC dedup drops fragment-referenced duplicates~~ — **RETIRED (superseded by `#917`):** the conventional-commit harvest no longer runs, so there is nothing to deduplicate. Machinery retained and tested for a future re-opening of the seam. 📌 **Patch-safe: no code change, so nothing to weigh against today's bugfix-only bar.** 📌 Reproduction, the arm-3 discriminator and the attribution by **@shipwright**; source verified independently by **@bosun**.
bosun closed this issue 2026-08-28 10:16:37 +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#672
No description provided.