Two complementary improvements aimed at reducing recurring maintenance
overhead:
1. **Test-count centralisation** — `doc/api/tests.md` is now the
single source of truth for the test counts. All other docs
(README, CLAUDE.md, doc/contributing.md, doc/getting-started.md,
doc/math/validation.md, doc/math/validation-protocol.md,
scripts/try_it.sh) use qualitative phrasing + a link instead of
hardcoded numbers. The previous regime had eight places with
"227 CGAL tests, 23 non-CGAL tests" that drifted apart across
releases (the v0.9.0 release-prep needed to touch nine files).
2. **Versioning policy** — `doc/release-policy.md` (new, ~250 lines)
formalises:
* SemVer rules for the pre-1.0 and post-1.0 phases.
* Phase-milestone → MINOR-bump mapping (v0.10.0 → Phase 9c, …).
* Single-source-of-truth table for moving numbers (test counts,
version, date).
* Step-by-step release process (the recipe that worked for v0.9.0
after the false-start with PR #11/#12).
* Hotfix policy + post-1.0 deprecation policy.
* Known failure modes and how to recover from them.
Plus a small CI gate:
3. **scripts/check-test-counts.sh** — verifies the totals in
doc/api/tests.md match `ctest` output. Re-uses existing build-cgal/
if present. Exit 0 on match, 1 on divergence with recovery hints.
Cheap enough (~30 s) to run on every PR.
Other cleanups
──────────────
* code/tests/cgal/CMakeLists.txt — stale "Test 7 (genus-2 homology)
as GTEST_SKIP stub until Phase 8" comment removed; that test landed
as HomologyGenerators.Genus2_FourCutEdges in Phase 7.
* CLAUDE.md — "test-fast also runs stubs" Known Quirks entry updated
to reflect the v0.9.0 stub cleanup (no GTEST_SKIPs remain).
* CLAUDE.md doc map — new entry for doc/release-policy.md.
Stubs audit
───────────
Zero GTEST_SKIP() calls remain in the codebase as of this commit.
The only references to stubs are in historical documentation
(CHANGELOG.md v0.7.0 entry, doc/roadmap/* "deferred to research-track"
notes) — those are intended.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.4 KiB
Release & versioning policy
Audience: maintainers and recurring contributors of conformallab++. External users only need this if they are publishing a fork.
Created: 2026-05-22, after the v0.9.0 release that exposed two recurring failure modes: (a) the v0.9.0 tag had to be moved twice because release-prep landed on the wrong commit, and (b) the test counts were hardcoded in eight different documents and drifted apart.
This document codifies how conformallab++ chooses version numbers, how releases are produced, and which artefacts must stay in sync.
1. Version-number rules (SemVer)
The project follows Semantic Versioning 2.0.
Tag format: vMAJOR.MINOR.PATCH (e.g. v0.9.0).
Pre-1.0 phase (current — until CGAL submission)
While the project is at version 0.x.y, the public API is not yet
frozen. The convention we use during the pre-1.0 phase is:
| Bump | When |
|---|---|
0.x.0 → 0.(x+1).0 (MINOR) |
Significant new feature: new functional, new Newton solver, new public CGAL entry, new Phase milestone completed. May break the public API. Treated as the "interesting" release type during pre-1.0. |
0.x.y → 0.x.(y+1) (PATCH) |
Bug fixes, performance optimisations without API change, doc-only follow-up releases, CI/infrastructure fixes. Never breaks the public API. |
0.x.y → 1.0.0 |
API freeze + CGAL submission readiness. All public headers under include/CGAL/ are stable. Cumulative changelog updated to call out every public-API decision in retrospect. |
Phase milestone → MINOR-bump mapping (historical + planned):
| Tag | Phase milestone |
|---|---|
v0.1.0 |
Phase 1 (special functions) |
v0.2.0 |
Phase 2 (hyper-ideal geometry) |
v0.7.0 |
Phase 7 complete (period matrix, fundamental domain) |
v0.9.0 |
Phase 9a + 9b + 8b-Lite |
v0.10.0 |
Phase 9c (4g-polygon, genus g > 1) |
v0.11.0 |
Phase 10a (harmonic + holomorphic 1-forms) |
v1.0.0 |
CGAL package submission-ready |
Post-1.0 phase (planned, ≥ v1.0.0)
Standard SemVer with no pre-1.0 exceptions:
| Bump | When |
|---|---|
MAJOR (X.0.0 → (X+1).0.0) |
Any breaking change to the public API: removed function, signature change, renamed CGAL entry, behaviour change of a documented contract. |
MINOR (X.Y.0 → X.(Y+1).0) |
New features, new public entries, new Default-traits classes. Strictly additive — no removal, no behaviour change. |
PATCH (X.Y.Z → X.Y.(Z+1)) |
Bug fixes only. No new files, no new public symbols. |
Pre-release tags
For testing-quality releases:
v1.0.0-alpha.1,v1.0.0-alpha.2, … — early preview, API may still shift.v1.0.0-beta.1— feature-complete preview, API stable but tests may still find issues.v1.0.0-rc.1,v1.0.0-rc.2, … — release candidate, only blocker fixes before the final tag.
What does NOT trigger a release
- Internal refactoring without user-visible change.
- Documentation-only updates (unless the documentation is itself a product, like a new tutorial).
- Test additions / coverage improvements.
- CI / infrastructure changes.
Group such changes into the next planned release.
2. Single source of truth for moving numbers
Three numbers tend to drift across multiple documents in any project: test counts, version strings, and date strings. The conformallab++ rule is:
| Number / string | Single source of truth | Anywhere else |
|---|---|---|
| Total / per-suite test counts | doc/api/tests.md |
Use qualitative phrasing + link, e.g. "full test suite passes, 0 skipped — see doc/api/tests.md". |
| Project version | CITATION.cff version: field |
Don't hardcode in headers, READMEs, doc bodies. Reference by name ("v0.9.0") only when the historical version actually matters (changelog entries, release-note bodies, Phase-milestone tables). |
| Release date | CITATION.cff date-released: + CHANGELOG.md section header |
Don't hardcode elsewhere. |
| Test-suite description per file | doc/api/tests.md |
Headers and code files may list the suites they implement, but never claim a count of suites elsewhere. |
Why qualitative phrasing for test counts
The counts change with every PR that adds or removes a test. Hardcoding the number "227" in eight documents means eight separate edits per PR — and historically we have failed to keep them in sync more than once.
The cost of looking up the exact number in doc/api/tests.md is one
click; the cost of fixing a stale "227" in seven places is real PR
overhead. We pay the click.
Audit script
scripts/check-test-counts.sh (when present) reads the totals from
doc/api/tests.md, runs ctest, and fails the CI if they diverge.
This catches the tests.md itself going stale even if no other doc
hardcodes the number.
3. Release process (checklist)
The recipe that worked for v0.9.0 (after the false-start with PR #11 /
#12). Apply for every vX.Y.Z release.
Pre-release prep — on a release branch
- Update
CHANGELOG.md— add a new top-level section## [vX.Y.Z] — YYYY-MM-DD. Categorise changes byAdded,Changed,Removed,Fixed,Deprecated,Security. See Keep a Changelog 1.1.0. - Update
CITATION.cff—version:anddate-released:fields. - Refresh
doc/api/tests.md— re-run the full test suite locally and update the per-suite tables + the two**Total: N tests, 0 skipped.**lines (one forconformallab_tests, one forconformallab_cgal_tests). - Run
scripts/check-test-counts.shif present — must pass. - Verify no hardcoded counts remain elsewhere:
grep -rn "[0-9]\+ tests pass\|[0-9]\+ tests, 0 skipped\|[0-9]\+ CGAL tests" \ README.md CLAUDE.md doc/ scripts/ \ | grep -v "doc/api/tests.md" # Expected: zero results (other than tests.md itself). - Commit with the message
release: vX.Y.Z — <one-line summary>. Push to a feature branch.
Open the release PR
- Open one PR with all prep commits. Title:
release: vX.Y.Z (<summary>). Description: copy the newCHANGELOG.mdsection. - Wait for CI green —
test-fast+test-cgal+doc-build. - Merge with a merge commit (the merge SHA becomes the release target).
After-merge — tag and announce
- Verify
mainhead is the merge commit:git fetch origin && git log origin/main --oneline -1 - Create the Gitea release via API or web UI.
target_commitishMUST be the merge SHA from step 10 (NOT the feature branch HEAD). The web UI does this automatically; the API needs explicitsha. - Sync the codeberg mirror:
git fetch origin --tags git push codeberg main --tags - Update CLAUDE.md "Release state" section so future
contributors see the new version at a glance. (This is the only
place where the current version appears outside
CITATION.cffandCHANGELOG.md.)
Common failure modes and how to recover
| Symptom | Recovery |
|---|---|
| Tag points to an earlier commit than the one you wanted | Delete the release + tag via Gitea API, re-create with the correct target_commitish. |
Release-prep commit lands on a feature branch but not on main after merge |
Open a fast-follow release/vX.Y.Z-finalise PR. Cherry-pick the missing commit; merge; move the tag. |
doc/api/tests.md counts are stale post-merge |
One-line PR: re-run ctest, update tests.md, no version bump (it's a doc fix between releases). |
4. Hotfix policy
For a vX.Y.0 minor release that needs an urgent fix before the next
planned minor:
- Branch off the release tag (NOT off
main):git checkout -b hotfix/vX.Y.1 vX.Y.0 - Apply the minimal fix.
- Tag
vX.Y.1from the hotfix branch. - Merge the hotfix back into
mainso the fix isn't lost.
Hotfixes are rare for a research-quality library; the typical path is to wait for the next minor. Use hotfixes only for security issues or build-broken-on-supported-platform regressions.
5. Deprecation policy (post-1.0)
When a public symbol is to be removed in version M.0.0:
- In any earlier
M-1minor release, mark it[[deprecated]](C++17) or via a Doxygen\deprecatedtag. - Add a
Deprecatedsection to that version's CHANGELOG entry with the planned removal version. - Keep the symbol working for at least one full minor cycle before removal.
- Remove in the
M.0.0release; document the removal in theRemovedsection.
Pre-1.0, this policy does not apply — breaking changes are allowed on any minor bump as long as they are listed in CHANGELOG.
6. Related documents
CHANGELOG.md— per-version history.CITATION.cff— authoritative version + date for citation.doc/api/tests.md— single source of truth for test counts.doc/contributing.md— code style + PR mechanics.doc/roadmap/phases.md— what each future MINOR bump will deliver.