Refresh CLAUDE.md for v0.10.0 (3 CI jobs incl. disabled test-cgal,
compile-time option matrix, reviewer/pages docs, agentic + token-hygiene
workflow patterns) and condense the historical Phase-8/audit logs to
pointers.
Add the documentation-pass process so the single-source-of-truth rules
stay enforced:
* scripts/quality/check-doc-freshness.sh — string-only drift gate
(version/date across CITATION/CHANGELOG/CLAUDE, doc-map count), <1 s,
registered in run-all.sh + quality README.
* doc/release-policy.md — "Documentation passes" subsection (triggers +
docs:sync rule); fix stale Phase-milestone mapping (v0.10.0 was
reviewer-ready, 9c→v0.11.0) and the test-cgal CI mention.
Add shared Claude config (un-ignore the two files only):
* .claude/settings.json — permission allowlist for safe repo commands.
* .claude/token-hygiene.md — Tier-3 cache-discipline user guide that
CLAUDE.md instructs Claude to remind the user about.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
11 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 |
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". |
| 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.cffandCHANGELOG.md; - the "N documents across …" count in CLAUDE.md's Documentation map
equals the actual
doc/**/*.mdfile 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
- 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+quality-gates. (test-cgalis disabled since 2026-05-26 — OOM-prone on the runner; run the CGAL suite locally instead, see step 3.) - 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.