The package version — Version: in DESCRIPTION, the one place
it lives — its components, and the tools that advance them.
The full version handling inside the release sequence is in
process/.
Up to five dot-separated components; two are independent counters that advance on different strands and never interfere:
| Component | Example | Meaning | Advanced by |
|---|---|---|---|
| 1–3 | 1.5.5 |
release-line identity — the upstream DuckDB tag | set explicitly at release |
| 4th | 1.5.5.9004 |
R-client counter | fledge, on main |
| 5th | 1.5.5.9004.42 |
vendor counter | scripts/vendor-one.sh, once per vendor commit |
The 4th free-runs on main,
where every glue and R change is born,
and is inherited frozen on a -dev branch —
frozen at the value the series was seeded with,
because scripts/series-port.sh classifies a
version bump VERSION and does not port it
(#2496).
A -dev version says which release line the series was seeded from
and how far its own vendoring has run;
what main is at today is read from main.
The 5th exists only on -dev branches, making every vendor commit
installable as a distinct version on r-universe.
Each strand owns one counter and freezes the other,
which is what lets the DESCRIPTION merge driver
(scripts/merge-version.sh)
resolve the Version: line without a conflict:
it takes the component-wise maximum of the two sides,
gated on an equal major.minor.patch prefix,
so the fourth stays the R-client strand's and the fifth the vendor strand's,
and a cross-release forward-port never inherits a foreign prefix.
Every other line of DESCRIPTION still goes through a normal three-way merge.
On a -dev branch the vendor counter orders, it does not count:
gaps from folded repairs are accepted,
but it must rise strictly across a series' vendor commits —
r-universe installs by version,
so two commits sharing one are two it cannot tell apart.
Taking the maximum is what keeps that true through a replay,
but only while both strands share a major.minor.patch prefix.
Where they do not, the gate keeps our side verbatim,
so a replayed vendor commit arrives with its parent's counter
and the series stops being orderable —
scripts/series-advance.sh restamps it,
and refuses to push a replay whose counter did not rise
(.claude/skills/series-loop.md).
main-dev carries the preview prefix 1.5.99
and its buffer main-build carries 1.5.5,
which is that state.
Aligning a series' two strands is the fix.
Exempting the fifth component from the gate — taking the maximum counter
across differing prefixes too — is declined
(#2488):
main has itself carried five-component versions,
so the gate is what keeps a foreign counter off a -dev,
and a forward rebuild reads its counter back from the merged
DESCRIPTION, where a maximum would restore the old chain's numbering.
The prefix gate has a consequence worth knowing
(it is the answer to
#2488 —
no, nothing should inherit across prefixes):
because the driver keeps ours verbatim when the prefixes differ,
it does not renumber a -dev branch when the base moves to a new
patch release.
Rebasing from 1.5.4.9005.N onto a 1.5.5.9000 base leaves every commit
at the base version rather than producing …9000.1, …2, …3;
the vendor counter has to be re-applied as part of the rebase.
The gate is doing its job — this is the one case where it does less than
a reader might assume.
A forward rebuild is the exception.
scripts/series-forward-build.sh
renumbers the fifth component as a true counter, one per replayed commit,
so the new chain counts itself rather than carrying the old one's numbering —
and the counter is read back from HEAD at each step,
which is what makes the replay restartable after a conflict.
A released version is the bare three-component prefix, matching
the upstream tag, set explicitly with
fledge::bump_version("X.Y.Z") —
never derived from git; the tag follows DESCRIPTION.
fledge.yaml runs fledge on main,
writing NEWS.md from commit messages
and merging its bump PR by squash — which is why commit messages on main are
written to be read as changelog entries.