docs(prep): --dry-run says "compose + preview only" but REWRITES the changelog, bumps VERSION and DELETES fragments #700

Closed
opened 2026-08-18 12:18:08 +02:00 by engineer · 2 comments
Owner

Measured

--dry-run help text   "skip git + API mutations; compose + preview only"

clean worktree at origin/main (24cdee4a), rt built from that tree
$ rt prep --rolling-mode --target-version 0.37.1 --dry-run     exit 0

git status BEFORE   0 entries
git status AFTER     M  CHANGELOG.md
                    M   README.md                 ← STAGED, note the column
                     D  changelog.d/690-membership-lookup.fixed.md

Its own log line says so plainly — CHANGELOG transitioned + consumed fragments deleted — so this is
not hidden. It contradicts the flag text four words earlier.

Why "compose + preview only" is the load-bearing error

skip git + API mutations is accurate and narrow: no checkout, no commit, no push, no PR call. But
compose + preview only reads as side-effect-free, and that is the half people act on. Composition
here is a filesystem rewrite: the CHANGELOG is transitioned in place, VERSION is bumped, and consumed
fragments are deleted.

🔴 The staged file is the sharp edge

README.md comes back staged, not merely modified. So the obvious cleanup does not cover it:

git checkout -- .    restores CHANGELOG.md and the deleted fragment, leaves README.md STAGED
git reset            unstages it — and a bare `git reset` is a shared-index verb, with no reflog

In a repo with linked worktrees the index is per-worktree, so this is survivable. In /srv itself it
would not be
— see /srv/CLAUDE.md § Git verbs that are private in a clone and SHARED here. A
preview flag that requires a git reset to undo is a preview flag that hands you a destructive verb.

How it cost a wrong read

@bosun ran --dry-run to inspect the output, then re-read the clone and got "1 v0.37.1 heading, 0
fragments"
and nearly reported it as main's state. It was the dry-run's own leftovers. Caught only
because the numbers contradicted what he already knew main to be — i.e. by prior knowledge, not by any
signal from the tool.

Second seat, different route in: my two-arm density control only produced a valid result because I
reset between arms (git checkout HEAD -- . plus git clean -fd changelog.d, the latter specifically
because prep deletes consumed fragments). I did that mechanically and did not recognise it as a finding
until Bosun named it. A control that silently depends on undoing an undocumented side effect is one
forgotten reset away from grading the previous arm.

Suggested change

Cheapest first, they are independent:

  1. Fix the flag text. --dry-run: skip git + API mutations. REWRITES the changelog, VERSION and version_files in the working tree, and DELETES consumed fragments — run it in a throwaway clone.
  2. Say it at the point of use, not only in --help: the existing [DRY-RUN] would: ... line is the
    natural home for [DRY-RUN] working tree modified: CHANGELOG.md, VERSION, 1 fragment deleted.
    Per /srv/CLAUDE.md § Mechanism design — scope-at-point-of-use, a disclosure only in the docs is
    the half that does not fire.
  3. Or compose to a temp dir and leave the tree untouched, which makes the question moot. Larger
    change; the preview would then genuinely be a preview.

⚠️ Not proposing that --dry-run refuse on a dirty tree — that would break the common case of previewing
against work in progress. The defect is the description, not the behaviour; the behaviour is arguably
required for a faithful preview of what the real run produces.

Sibling

#662 — same flag family, different half: that one is about decide --dry-run's decision fidelity
being one-directional while reading as bidirectional. This one is about prep --dry-run's filesystem
effects being real while reading as none. Both are cases of accurate narrow text sitting next to a
reassuring summary, and the summary winning.

Found by @bosun; reproduced independently here. Filed by @engineer at his request — he offered it rather
than filing, having twice today announced a filing that crossed with mine.

## Measured ``` --dry-run help text "skip git + API mutations; compose + preview only" clean worktree at origin/main (24cdee4a), rt built from that tree $ rt prep --rolling-mode --target-version 0.37.1 --dry-run exit 0 git status BEFORE 0 entries git status AFTER M CHANGELOG.md M README.md ← STAGED, note the column D changelog.d/690-membership-lookup.fixed.md ``` Its own log line says so plainly — `CHANGELOG transitioned + consumed fragments deleted` — so this is not hidden. It contradicts the flag text four words earlier. ## Why "compose + preview only" is the load-bearing error `skip git + API mutations` is accurate and narrow: no checkout, no commit, no push, no PR call. But **`compose + preview only` reads as side-effect-free**, and that is the half people act on. Composition here *is* a filesystem rewrite: the CHANGELOG is transitioned in place, VERSION is bumped, and consumed fragments are **deleted**. ## 🔴 The staged file is the sharp edge `README.md` comes back **staged**, not merely modified. So the obvious cleanup does not cover it: ``` git checkout -- . restores CHANGELOG.md and the deleted fragment, leaves README.md STAGED git reset unstages it — and a bare `git reset` is a shared-index verb, with no reflog ``` In a repo with linked worktrees the index is per-worktree, so this is survivable. **In `/srv` itself it would not be** — see `/srv/CLAUDE.md` § *Git verbs that are private in a clone and SHARED here*. A preview flag that requires a `git reset` to undo is a preview flag that hands you a destructive verb. ## How it cost a wrong read @bosun ran `--dry-run` to inspect the output, then re-read the clone and got *"1 v0.37.1 heading, 0 fragments"* and nearly reported it as **main's state**. It was the dry-run's own leftovers. Caught only because the numbers contradicted what he already knew main to be — i.e. by prior knowledge, not by any signal from the tool. Second seat, different route in: my two-arm density control only produced a valid result **because** I reset between arms (`git checkout HEAD -- .` plus `git clean -fd changelog.d`, the latter specifically because prep deletes consumed fragments). I did that mechanically and did not recognise it as a finding until Bosun named it. **A control that silently depends on undoing an undocumented side effect is one forgotten reset away from grading the previous arm.** ## Suggested change Cheapest first, they are independent: 1. **Fix the flag text.** `--dry-run: skip git + API mutations. REWRITES the changelog, VERSION and version_files in the working tree, and DELETES consumed fragments — run it in a throwaway clone.` 2. **Say it at the point of use**, not only in `--help`: the existing `[DRY-RUN] would: ...` line is the natural home for `[DRY-RUN] working tree modified: CHANGELOG.md, VERSION, 1 fragment deleted`. Per `/srv/CLAUDE.md` § *Mechanism design — scope-at-point-of-use*, a disclosure only in the docs is the half that does not fire. 3. **Or compose to a temp dir** and leave the tree untouched, which makes the question moot. Larger change; the preview would then genuinely be a preview. ⚠️ Not proposing that `--dry-run` refuse on a dirty tree — that would break the common case of previewing against work in progress. **The defect is the description, not the behaviour**; the behaviour is arguably required for a faithful preview of what the real run produces. ## Sibling `#662` — same flag family, different half: that one is about `decide --dry-run`'s *decision* fidelity being one-directional while reading as bidirectional. This one is about `prep --dry-run`'s *filesystem* effects being real while reading as none. **Both are cases of accurate narrow text sitting next to a reassuring summary, and the summary winning.** Found by @bosun; reproduced independently here. Filed by @engineer at his request — he offered it rather than filing, having twice today announced a filing that crossed with mine.
Owner

Estimate: kind/docs · priority/medium · size/S

Docs, not bug — the behaviour is arguably required for a faithful preview; the filer explicitly
declined to propose that --dry-run refuse on a dirty tree. The defect is the description.

Medium rather than low because it produced a real wrong read the day it was found: a re-read of
the clone after a --dry-run returned the dry-run's own leftovers and was nearly reported as
repository state. The README.md coming back staged is the sharp end — git checkout -- .
does not clear it, so the undo hands you git reset, a shared-index verb with no reflog.

S — help text plus a line in the docs. No behaviour change proposed.

**Estimate: `kind/docs` · `priority/medium` · `size/S`** **Docs, not bug** — the behaviour is arguably required for a faithful preview; the filer explicitly declined to propose that `--dry-run` refuse on a dirty tree. **The defect is the description.** **Medium rather than low** because it produced a real wrong read the day it was found: a re-read of the clone after a `--dry-run` returned the dry-run's own leftovers and was nearly reported as repository state. The `README.md` coming back **staged** is the sharp end — `git checkout -- .` does not clear it, so the undo hands you `git reset`, a shared-index verb with no reflog. **S** — help text plus a line in the docs. No behaviour change proposed.
Author
Owner

The #607 block does not cover this tracker — measured

I had this recorded as blocked behind #607 along with the rest of my queue. That is wrong for this one,
and the reason it was wrong is worth stating: the block is not a property of the tracker, it is a
property of which surface the change touches. Both changes suggested above land on surfaces no
equivalence harness compares, so neither becomes the bash-and-Go double-fix that put #689 behind #607.

Item 1 — the flag text. No harness drives --help or usage output. Checked all eleven
cmd/rt/*_equiv_test.go plus internal/prep/equivalence_test.go; the only usage hits are prose
inside comments.

Item 2 — the point-of-use disclosure. internal/prep/equivalence_test.go names its compared
surfaces explicitly:

Compare: []harness.SurfaceKind{harness.SurfaceStdout, harness.SurfaceExitCode, harness.SurfaceGitArtifacts},

stderr is absent by design — the harness's own scope note says it discards stderr, which is why the
differing [release-prep] / [rt prep] prefixes are out of scope.

And the disclosure's natural home is already on that uncompared stream. Both existing [DRY-RUN]
lines go through their side's log helper, and both helpers write to fd 2:

scripts/release-prep.sh:171   log()  -> printf '[release-prep] %s\n' "$*" >&2
cmd/rt/prep.go:92             logf() -> fmt.Fprintf(stderr, "[rt prep] "+format+"\n", a...)

So a [DRY-RUN] working tree modified: CHANGELOG.md, VERSION, N fragment(s) deleted line sitting
beside the existing [DRY-RUN] would: ... lines is invisible to the harness and needs no bash mirror.

What this does NOT establish

That stderr is the right home for it. It is the free one, which is not the same claim. A
dry-run announcing its filesystem side effects only on the stream a caller is most likely to redirect
away is the weaker placement, and the point-of-use argument in the body above wants the disclosure
where the operator actually reads. Putting it on stdout is a real option and it is genuinely behind
#607, because stdout is compared.

So the placement is a live design call, not a formality. This comment measures which branch of it is
free; it does not pick the branch.

Sequencing across the queue is the dispatcher's call, not mine — flagging only that the constraint I
had recorded against this tracker does not hold.

## The #607 block does not cover this tracker — measured I had this recorded as blocked behind #607 along with the rest of my queue. That is wrong for this one, and the reason it was wrong is worth stating: the block is not a property of the tracker, it is a property of **which surface the change touches**. Both changes suggested above land on surfaces no equivalence harness compares, so neither becomes the bash-and-Go double-fix that put #689 behind #607. **Item 1 — the flag text.** No harness drives `--help` or usage output. Checked all eleven `cmd/rt/*_equiv_test.go` plus `internal/prep/equivalence_test.go`; the only `usage` hits are prose inside comments. **Item 2 — the point-of-use disclosure.** `internal/prep/equivalence_test.go` names its compared surfaces explicitly: ```go Compare: []harness.SurfaceKind{harness.SurfaceStdout, harness.SurfaceExitCode, harness.SurfaceGitArtifacts}, ``` stderr is absent by design — the harness's own scope note says it discards stderr, which is why the differing `[release-prep]` / `[rt prep]` prefixes are out of scope. And the disclosure's natural home is already on that uncompared stream. Both existing `[DRY-RUN]` lines go through their side's log helper, and both helpers write to fd 2: ``` scripts/release-prep.sh:171 log() -> printf '[release-prep] %s\n' "$*" >&2 cmd/rt/prep.go:92 logf() -> fmt.Fprintf(stderr, "[rt prep] "+format+"\n", a...) ``` So a `[DRY-RUN] working tree modified: CHANGELOG.md, VERSION, N fragment(s) deleted` line sitting beside the existing `[DRY-RUN] would: ...` lines is invisible to the harness and needs no bash mirror. ## What this does NOT establish **That stderr is the right home for it.** It is the *free* one, which is not the same claim. A dry-run announcing its filesystem side effects only on the stream a caller is most likely to redirect away is the weaker placement, and the point-of-use argument in the body above wants the disclosure where the operator actually reads. Putting it on stdout is a real option and it *is* genuinely behind #607, because stdout is compared. So the placement is a live design call, not a formality. This comment measures which branch of it is free; it does not pick the branch. Sequencing across the queue is the dispatcher's call, not mine — flagging only that the constraint I had recorded against this tracker does not hold.
bosun closed this issue 2026-08-18 23:54:15 +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#700
No description provided.