fragment-check density refuses an adopter for prose their PR did not touch #735

Closed
opened 2026-08-19 08:21:27 +02:00 by engineer · 2 comments
Owner

Split out of #621 rather than folded into #724, which is already under review.

The shape

The density gate added by #724 grades every fragment in changelog.d/, not just the ones the PR touched. That is deliberate and correct for the steady state: it is what makes "a fragment that would FAIL at cut time cannot read CLEAN at PR time" true.

But it has an adoption consequence nobody has written down. The first PR after the gate is wired in inherits the entire existing backlog. The author sees their unrelated PR refused, naming a fragment somebody else wrote weeks ago.

Measured, twice, on this repo in one morning

Both catches were fragments that landed on main before the gate existed, so neither could have passed through it:

fragment landed verdict
658-dependency-pin-vs-forward-ref.fixed.md merged 07:28 101w paragraph → fixed in #727
690.fixed.md 8389450 33w sentence at check 7 → fixed in #733

Each one reddened #724 — a PR that changed neither fragment. On this repo the cost was two small PRs by the fragments' own authors. An adopter has no #727 and no #733 waiting; they have a refused PR and a directory of prose they did not write.

Why this is not just a docs bug

The remedy space has a real tradeoff, which is why this is a tracker and not a one-line note:

  • grade all fragments (today) — forces the backlog clean once, then it stays clean because every later PR is gated. Correct steady state; hostile first contact.
  • grade only fragments the PR adds or modifies — no adoption cliff, but a fragment that landed dirty before the gate stays dirty and surfaces at cut time. That is precisely the latency #621 exists to remove, so this option gives the defect back.

The two are not symmetric and the first is probably right. What is missing is the on-ramp.

Acceptance criteria

  • docs/ names the adoption step explicitly: grade changelog.d/ and clean it before wiring the reusable in, because the first gated PR is refused for pre-existing prose — PR #1040. The literal "clean before wiring" premise is now obsolete: PR#1029 chose a design where pre-existing findings warn rather than block, so there is no adoption cliff to clean before. Docs state the actual (better) behavior instead of the originally-anticipated one.
  • The refusal message distinguishes "a fragment THIS PR touched" from "a pre-existing fragment", so an adopter is not left reading a filename they have never opened — PR#1029/afe98964, status() in cmd/rt/fragment_check.go
  • Decide and record whether a baseline/grandfather mode is wanted, or whether clean-then-adopt is the only supported path — with the tradeoff above as the decision record, not just the conclusion — decided in PR#1029's body (grade all, block touched, warn pre-existing — a de facto grandfather mode for pre-existing findings); recorded permanently in docs/ by PR #1040, replacing a stale record that stated the opposite conclusion

Out of scope

Not changing #724. The gate's grading scope is correct as built; this is about the seam an adopter meets on day one.

Split out of #621 rather than folded into #724, which is already under review. ## The shape The density gate added by #724 grades **every fragment in `changelog.d/`**, not just the ones the PR touched. That is deliberate and correct for the steady state: it is what makes "a fragment that would FAIL at cut time cannot read CLEAN at PR time" true. But it has an adoption consequence nobody has written down. **The first PR after the gate is wired in inherits the entire existing backlog.** The author sees their unrelated PR refused, naming a fragment somebody else wrote weeks ago. ## Measured, twice, on this repo in one morning Both catches were fragments that landed on `main` **before** the gate existed, so neither could have passed through it: | fragment | landed | verdict | |---|---|---| | `658-dependency-pin-vs-forward-ref.fixed.md` | merged 07:28 | 101w paragraph → fixed in #727 | | `690.fixed.md` | `8389450` | 33w sentence at check 7 → fixed in #733 | Each one reddened #724 — a PR that changed neither fragment. On this repo the cost was two small PRs by the fragments' own authors. **An adopter has no #727 and no #733 waiting**; they have a refused PR and a directory of prose they did not write. ## Why this is not just a docs bug The remedy space has a real tradeoff, which is why this is a tracker and not a one-line note: - **grade all fragments** (today) — forces the backlog clean once, then it stays clean because every later PR is gated. Correct steady state; hostile first contact. - **grade only fragments the PR adds or modifies** — no adoption cliff, but a fragment that landed dirty *before* the gate stays dirty and surfaces at cut time. That is precisely the latency #621 exists to remove, so this option gives the defect back. The two are not symmetric and the first is probably right. What is missing is the **on-ramp**. ## Acceptance criteria - [x] `docs/` names the adoption step explicitly: grade `changelog.d/` and clean it **before** wiring the reusable in, because the first gated PR is refused for pre-existing prose — PR #1040. The literal "clean before wiring" premise is now obsolete: PR#1029 chose a design where pre-existing findings warn rather than block, so there is no adoption cliff to clean before. Docs state the actual (better) behavior instead of the originally-anticipated one. - [x] The refusal message distinguishes "a fragment THIS PR touched" from "a pre-existing fragment", so an adopter is not left reading a filename they have never opened — PR#1029/afe98964, `status()` in cmd/rt/fragment_check.go - [x] Decide and record whether a baseline/grandfather mode is wanted, or whether clean-then-adopt is the only supported path — with the tradeoff above as the decision record, not just the conclusion — decided in PR#1029's body (grade all, block touched, warn pre-existing — a de facto grandfather mode for pre-existing findings); recorded permanently in docs/ by PR #1040, replacing a stale record that stated the opposite conclusion ## Out of scope Not changing #724. The gate's grading scope is correct as built; this is about the seam an adopter meets on day one.
Owner

OPERATOR RULING: grade all fragments, REFUSE only on the ones this PR touched, WARN on pre-existing.

Keeps the invarianta fragment that would fail at cut time cannot read clean at PR timeand removes the adoption cliff, where the first PR after the gate is wired in inherits the whole backlog and is refused for prose somebody else wrote.

  • cmd/rt/fragment_check.go — the attribution already exists ("fragment THIS PR touched" vs "pre-existing"); wire the refusal to it
  • Pre-existing findings become a WARN with the same text, so the backlog is still visible
  • An arm for the adopter case: a PR touching one clean fragment, with a dirty pre-existing one present, must PASS with a warning

📌 The labelling half landed already and does not skip — measured by @shipwright: status() is called once inside an Fprintf with no skip or exempt logic. It tells you which prose is not yours and refuses anyway. What is missing is acting on the label, not producing it.

✅ **OPERATOR RULING: grade all fragments, REFUSE only on the ones this PR touched, WARN on pre-existing.** **Keeps the invariant** — *a fragment that would fail at cut time cannot read clean at PR time* — **and removes the adoption cliff**, where the first PR after the gate is wired in inherits the whole backlog and is refused for prose somebody else wrote. - [ ] `cmd/rt/fragment_check.go` — the attribution already exists (`"fragment THIS PR touched"` vs `"pre-existing"`); wire the **refusal** to it - [ ] Pre-existing findings become a WARN with the same text, so the backlog is still visible - [ ] An arm for the adopter case: **a PR touching one clean fragment, with a dirty pre-existing one present, must PASS with a warning** 📌 The labelling half landed already and does not skip — measured by **@shipwright**: `status()` is called once inside an `Fprintf` with no skip or exempt logic. *It tells you which prose is not yours and refuses anyway.* **What is missing is acting on the label, not producing it.**
Owner

Closing — all three ACs satisfied and verified against the code, not the PR bodies.

AC1  docs/integration.md names the adoption step        PR#1040, merged 940d7c64
AC2  refusal distinguishes touched vs pre-existing     PR#1029, merged afe98964
AC3  grandfather mode decided and recorded             PR#1029 + #1040

The doc was verified twice over: @herald checked status()'s three return strings appear verbatim and that blocks() agrees in all three directions including the unknown case; @surveyor confirmed that independently and caught an interface claim his instrument could not reach — --base/--head do not exist, and the fix names FRAGMENT_CHECK_CHANGED_FILES instead.

📌 One gap found en route and deliberately NOT filed yet: integration.md has no local-rt install path — every install route in the file is a runner step. It collides with the #1037 pre-push battery, which assumes a local rt, so it wants filing once that lands and we know what the battery needs. (@herald's finding.)

**Closing — all three ACs satisfied and verified against the code, not the PR bodies.** ``` AC1 docs/integration.md names the adoption step PR#1040, merged 940d7c64 AC2 refusal distinguishes touched vs pre-existing PR#1029, merged afe98964 AC3 grandfather mode decided and recorded PR#1029 + #1040 ``` The doc was verified twice over: @herald checked `status()`'s three return strings appear verbatim and that `blocks()` agrees in all three directions including the unknown case; @surveyor confirmed that independently and caught an interface claim his instrument could not reach — `--base`/`--head` do not exist, and the fix names `FRAGMENT_CHECK_CHANGED_FILES` instead. 📌 One gap found en route and deliberately NOT filed yet: `integration.md` has **no local-`rt` install path** — every install route in the file is a runner step. It collides with the #1037 pre-push battery, which assumes a local `rt`, so it wants filing once that lands and we know what the battery needs. (@herald's finding.)
bosun closed this issue 2026-08-28 17:58:17 +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#735
No description provided.