Description
Classification: documentation drift. The code is right; docs/auto-update.md is stale.
rootline 9.12.1 sets u.VersionPolicy = autoupdate.SameMajorOnly (cmd/rootline/main.go:21) and prints a stderr notice when a staged release crosses that boundary (cmd/rootline/main.go:36-38). That policy shipped for #118 in 2ccc7c0 ("fix(autoupdate): enforce compatibility boundary (#119)").
docs/auto-update.md — the page a user is sent to when the binary does not update — never mentions the boundary. Not in the flow diagram, not in the version-comparison table, not in the section literally titled When Auto-update Does NOT Run, not in Troubleshooting. Every claim on that page still describes the pre-#119 unconditional updater.
The consequence: after any major release, a user on the previous major sees rootline --version stop changing forever. The page's Troubleshooting section walks them through three causes (dev build, PATH shadowing, empty staging directory) and none of them is the real one — the staging directory is not empty in the withheld case, which is the exact opposite of the symptom step 3 describes.
Steps to Reproduce
Isolated fixture, no network, using the installed 9.12.1 binary as both the running binary and the staged payload (the payload's bytes are irrelevant — only the staged directory's version tag drives the policy):
FX=$(mktemp -d)
mkdir -p "$FX/home/Library/Caches/rootline/staged/v10.0.0" "$FX/bin" "$FX/work"
cp "$(command -v rootline)" "$FX/bin/rootline"
cp "$(command -v rootline)" "$FX/home/Library/Caches/rootline/staged/v10.0.0/rootline"
chmod +x "$FX/home/Library/Caches/rootline/staged/v10.0.0/rootline"
cd "$FX/work" && HOME="$FX/home" "$FX/bin/rootline" --version
ls "$FX/home/Library/Caches/rootline/staged/v10.0.0/"
Control — same fixture, only the staged tag changes from v10.0.0 to v9.99.0:
FX2=$(mktemp -d)
mkdir -p "$FX2/home/Library/Caches/rootline/staged/v9.99.0" "$FX2/bin" "$FX2/work"
cp "$(command -v rootline)" "$FX2/bin/rootline"
cp "$(command -v rootline)" "$FX2/home/Library/Caches/rootline/staged/v9.99.0/rootline"
chmod +x "$FX2/home/Library/Caches/rootline/staged/v9.99.0/rootline"
cd "$FX2/work" && HOME="$FX2/home" "$FX2/bin/rootline" --version
ls "$FX2/home/Library/Caches/rootline/staged/v9.99.0/"
Expected Behavior
Per docs/auto-update.md:11-13, ApplyStagedIfAvailable() has exactly two outcomes:
1. ApplyStagedIfAvailable() — sync: checks ~/.cache/rootline/staged/ for a newer binary
→ if found: atomic rename over current binary, re-exec (process is replaced)
→ if not found or not newer: continues normally
v10.0.0 is found and is newer, so per the documented flow it should be renamed over the current binary and re-exec'd, silently — docs/auto-update.md:3 promises the update is "transparent — no prompts, no interruptions, no downtime".
Actual Behavior
Cross-major staged release (v10.0.0) — withheld, and it says so on stderr:
rootline: incompatible update withheld: current 9.12.1, available v10.0.0; run the installer to upgrade deliberately
rootline version 9.12.1
EXIT:0
$ ls "$FX/home/Library/Caches/rootline/staged/v10.0.0/"
rootline # 12M, still there — not applied, not removed
Control, same-major staged release (v9.99.0) — applied, silently:
rootline version 9.12.1
EXIT:0
$ ls "$FX2/home/Library/Caches/rootline/staged/v9.99.0/"
# empty — the staged binary was renamed over the running one
The control is what makes this a policy gate rather than a broken staging path: the only difference between the two runs is the version tag on the staging subdirectory. 9.99.0 > 9.12.1 is applied; 10.0.0 > 9.12.1 is refused. That is SameMajorOnly working exactly as #118 asked for it, and it is invisible on the page that documents this subsystem.
Affected documentation sites
file:line |
Claim |
Why it is now wrong |
docs/auto-update.md:3 |
"The update is transparent — no prompts, no interruptions, no downtime." |
A withheld update writes a line to stderr (cmd/rootline/main.go:36-38). Not a prompt, but not silent either. |
docs/auto-update.md:11-13 |
Two-branch flow: found+newer → apply; not-found-or-not-newer → continue |
Misses the third branch: found + newer + out of boundary → withheld, notice, staged file retained. |
docs/auto-update.md:47 |
"isNewer then compares with a strict >" |
> is necessary but no longer sufficient. 10.0.0 > 9.12.1 and the update is still refused. |
docs/auto-update.md:50-54 |
Replaced?-yes/no table, four rows, all within one major |
No row covers a major-version change, which is the only case where the answer changed. |
docs/auto-update.md:60-64 |
"When Auto-update Does NOT Run" — one row, version == "dev" |
This is precisely where the boundary belongs, and it is the section's only entry. |
docs/auto-update.md:78 |
"All auto-update errors are suppressed" |
The withheld condition comes back as an error value and is reported, not suppressed (cmd/rootline/main.go:32-39). |
docs/auto-update.md:86-90 |
Troubleshooting "The binary is not updating" — three causes |
None is the boundary. Step 3 reads an empty staging directory as the symptom; in the withheld case the staging directory is populated and stays populated. |
Provenance — how the page was missed
#118 asked for it explicitly: "Cover same-major allowance, cross-major withholding, and notice emission exactly once only for withheld updates. Update the documented auto-update behavior."
The implementing commit updated one of the two documentation surfaces:
$ git show --stat 2ccc7c0
fix(autoupdate): enforce compatibility boundary (#119)
CLAUDE.md | 2 +-
cmd/rootline/autoupdate_test.go | 77 +++++++++++++++++++++++++++++++++++++++++
cmd/rootline/main.go | 35 +++++++++++++++++--
go.mod | 2 +-
go.sum | 4 +--
CLAUDE.md:133 carries the full, correct policy today — same-major for stable, same-minor pre-1.0, retained-not-applied, stderr notice, deliberate-reinstall guidance. docs/auto-update.md was not in the commit and contains zero occurrences of withheld, compatibility, major-as-boundary, or VersionPolicy:
$ grep -ric "withheld\|compatibility\|SameMajorOnly\|VersionPolicy" docs/auto-update.md
0
So the fix is a one-pass edit of docs/auto-update.md to match what CLAUDE.md:133 already states. No code change is implied by this issue.
Environment
- Rootline version:
rootline version 9.12.1 (equal to master at 7d95e35)
- OS: macOS (darwin/arm64)
- Fixture:
HOME-isolated temp dir, no network access, staged payload is a copy of the installed binary
Related work
Labels owed by a maintainer if --label documentation did not stick: documentation.
Description
Classification: documentation drift. The code is right;
docs/auto-update.mdis stale.rootline9.12.1 setsu.VersionPolicy = autoupdate.SameMajorOnly(cmd/rootline/main.go:21) and prints a stderr notice when a staged release crosses that boundary (cmd/rootline/main.go:36-38). That policy shipped for #118 in2ccc7c0("fix(autoupdate): enforce compatibility boundary (#119)").docs/auto-update.md— the page a user is sent to when the binary does not update — never mentions the boundary. Not in the flow diagram, not in the version-comparison table, not in the section literally titled When Auto-update Does NOT Run, not in Troubleshooting. Every claim on that page still describes the pre-#119 unconditional updater.The consequence: after any major release, a user on the previous major sees
rootline --versionstop changing forever. The page's Troubleshooting section walks them through three causes (devbuild, PATH shadowing, empty staging directory) and none of them is the real one — the staging directory is not empty in the withheld case, which is the exact opposite of the symptom step 3 describes.Steps to Reproduce
Isolated fixture, no network, using the installed 9.12.1 binary as both the running binary and the staged payload (the payload's bytes are irrelevant — only the staged directory's version tag drives the policy):
Control — same fixture, only the staged tag changes from
v10.0.0tov9.99.0:Expected Behavior
Per
docs/auto-update.md:11-13,ApplyStagedIfAvailable()has exactly two outcomes:v10.0.0is found and is newer, so per the documented flow it should be renamed over the current binary and re-exec'd, silently —docs/auto-update.md:3promises the update is "transparent — no prompts, no interruptions, no downtime".Actual Behavior
Cross-major staged release (
v10.0.0) — withheld, and it says so on stderr:Control, same-major staged release (
v9.99.0) — applied, silently:The control is what makes this a policy gate rather than a broken staging path: the only difference between the two runs is the version tag on the staging subdirectory.
9.99.0 > 9.12.1is applied;10.0.0 > 9.12.1is refused. That isSameMajorOnlyworking exactly as #118 asked for it, and it is invisible on the page that documents this subsystem.Affected documentation sites
file:linedocs/auto-update.md:3cmd/rootline/main.go:36-38). Not a prompt, but not silent either.docs/auto-update.md:11-13docs/auto-update.md:47isNewerthen compares with a strict>">is necessary but no longer sufficient.10.0.0 > 9.12.1and the update is still refused.docs/auto-update.md:50-54docs/auto-update.md:60-64version == "dev"docs/auto-update.md:78cmd/rootline/main.go:32-39).docs/auto-update.md:86-90Provenance — how the page was missed
#118 asked for it explicitly: "Cover same-major allowance, cross-major withholding, and notice emission exactly once only for withheld updates. Update the documented auto-update behavior."
The implementing commit updated one of the two documentation surfaces:
CLAUDE.md:133carries the full, correct policy today — same-major for stable, same-minor pre-1.0, retained-not-applied, stderr notice, deliberate-reinstall guidance.docs/auto-update.mdwas not in the commit and contains zero occurrences ofwithheld,compatibility,major-as-boundary, orVersionPolicy:So the fix is a one-pass edit of
docs/auto-update.mdto match whatCLAUDE.md:133already states. No code change is implied by this issue.Environment
rootline version 9.12.1(equal tomasterat7d95e35)HOME-isolated temp dir, no network access, staged payload is a copy of the installed binaryRelated work
Labels owed by a maintainer if
--label documentationdid not stick:documentation.