# 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 — <one-line summary>`.  Push to a feature branch.

### Open the release PR

7. **Open one PR** with all prep commits.  Title:
   `release: vX.Y.Z (<summary>)`.  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.
