# 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](https://semver.org). 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` | Reviewer-ready: 100 % Doxygen, quality-gate suite, reviewer materials, compile-time matrix (no new geometry phase) | | `v0.11.0` | Phase 9c (4g-polygon, genus g > 1) *(planned)* | | `v0.12.0` | Phase 10a (harmonic + holomorphic 1-forms) *(planned)* | | `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`](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. ### Documentation passes The single-source-of-truth rules above only work if the docs are actually re-synced on a predictable schedule. A "documentation pass" is the named ritual that does this. Two rules make it durable instead of forgotten: **Anchor it to a trigger, never do it mid-task.** | Trigger | What the pass covers | |---|---| | **Release tag** | `CHANGELOG.md`, `CITATION.cff` (version + date), `doc/api/tests.md`, CLAUDE.md "Release state" + "Documentation map" count. | | **Feature branch, just before the PR** | Land all doc edits the branch implies as a single final `docs: sync` commit — so the doc update is part of every merge, not a separate event you can skip. | | **Mid-code-work** | **Do not fix docs inline.** Note the drift (a TODO, a memory decision-record, or a flagged follow-up task) and fold it into the next `docs: sync`. Inline doc edits bloat the PR's scope and — for AI-assisted sessions — bust the prompt cache (see `.claude/token-hygiene.md`). | **Make drift visible with a cheap gate.** `scripts/quality/check-doc-freshness.sh` verifies the *string-only* cross-file invariants in well under a second (no build): * release version agrees across `CITATION.cff`, `CHANGELOG.md` (newest section), and CLAUDE.md; * release date agrees across `CITATION.cff` and `CHANGELOG.md`; * the "N documents across …" count in CLAUDE.md's Documentation map equals the actual `doc/**/*.md` file count. Run it as the first step of any documentation pass. The two build-dependent checks it deliberately skips — `scripts/check-test-counts.sh` (needs a build dir) and `scripts/gen-headers-md.py` drift (needs Doxygen XML) — belong to the *release* pass, not the per-branch one. --- ## 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 1. **Update `CHANGELOG.md`** — add a new top-level section `## [vX.Y.Z] — YYYY-MM-DD`. Categorise changes by `Added`, `Changed`, `Removed`, `Fixed`, `Deprecated`, `Security`. See [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). 2. **Update `CITATION.cff`** — `version:` and `date-released:` fields. 3. **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 for `conformallab_tests`, one for `conformallab_cgal_tests`). 4. **Run `scripts/check-test-counts.sh`** if present — must pass. 5. **Verify no hardcoded counts remain** elsewhere: ```bash 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). ``` 6. **Commit** with the message `release: vX.Y.Z — `. Push to a feature branch. ### Open the release PR 7. **Open one PR** with all prep commits. Title: `release: vX.Y.Z ()`. Description: copy the new `CHANGELOG.md` section. 8. **Wait for CI green** — `test-fast` + `quality-gates`. (`test-cgal` is disabled since 2026-05-26 — OOM-prone on the runner; run the CGAL suite locally instead, see step 3.) 9. **Merge** with a merge commit (the merge SHA becomes the release target). ### After-merge — tag and announce 10. **Verify `main` head is the merge commit**: ```bash git fetch origin && git log origin/main --oneline -1 ``` 11. **Create the Gitea release** via API or web UI. `target_commitish` MUST be the merge SHA from step 10 (NOT the feature branch HEAD). The web UI does this automatically; the API needs explicit `sha`. 12. **Sync the codeberg mirror**: ```bash git fetch origin --tags git push codeberg main --tags ``` 13. **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.cff` and `CHANGELOG.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: 1. Branch off the release tag (NOT off `main`): ```bash git checkout -b hotfix/vX.Y.1 vX.Y.0 ``` 2. Apply the minimal fix. 3. Tag `vX.Y.1` from the hotfix branch. 4. Merge the hotfix back into `main` so 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`: 1. In any earlier `M-1` minor release, mark it `[[deprecated]]` (C++17) or via a Doxygen `\deprecated` tag. 2. Add a `Deprecated` section to that version's CHANGELOG entry with the planned removal version. 3. Keep the symbol working for at least one full minor cycle before removal. 4. Remove in the `M.0.0` release; document the removal in the `Removed` section. 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.