diff --git a/CLAUDE.md b/CLAUDE.md index 4139877..c19a9db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -244,7 +244,7 @@ Two jobs in `.gitea/workflows/cpp-tests.yml`: Runner: `eulernest` — self-hosted Raspberry Pi, ARM64, Ubuntu 22.04. Docker image: `git.eulernest.eu/conformallab/ci-cpp:latest`. `test-cgal` needs `test-fast` to pass first (`needs: test-fast`). -Expected results: **23 non-CGAL tests pass**, **227 CGAL tests pass, 0 skipped**. +Expected results: full test suite passing, 0 skipped, 0 failed. The canonical counts live in `doc/api/tests.md` — do not hardcode them anywhere else (see [`doc/release-policy.md`](doc/release-policy.md)). ## Release state @@ -349,7 +349,7 @@ follow the empirical verification rule above before any new claim. | Full pipeline API for all three geometries | `doc/api/pipeline.md` | | What does each processing unit require/provide (contracts)? | `doc/api/contracts.md` | | How to add a new functional / geometry mode / port from Java | `doc/api/extending.md` | -| All 39 test suites, 227+23 tests, individual counts | `doc/api/tests.md` | +| Per-suite breakdown and counts (single source of truth) | `doc/api/tests.md` | | Phase 8 CGAL package design + Declarative YAML pipeline spec | `doc/api/cgal-package.md` | ### Concepts & specs @@ -373,6 +373,7 @@ follow the empirical verification rule above before any new claim. | Build modes, single-test invocation, CLI, Docker image rebuild | `doc/getting-started.md` | | Step-by-step: port the Inversive Distance functional (Phase 9a template) | `doc/tutorials/add-inversive-distance.md` | | Language policy, test standards, release flow | `doc/contributing.md` | +| Versioning rules + release process + single-source-of-truth list | `doc/release-policy.md` | ### geometry-central context @@ -386,7 +387,7 @@ Optional adoption roadmap (GC-1/2/3): `doc/roadmap/phases.md` (Optional section) ## Known quirks -- **`test-fast` also runs stubs**: `conformallab_tests` (non-CGAL) contains `GTEST_SKIP`-based stubs for functionals that need CGAL. This is intentional — those tests document what was in the Java port scope but requires the CGAL mesh type. +- **No GTEST_SKIP stubs remain** (since v0.9.0): the three stale HDS-port stub files were removed because the CGAL test suite covers the same functionality with real tests. The pure-math `conformallab_tests` target now only contains active tests. - **Boost is header-only**: CGAL 6.x uses only Boost headers (`Boost.Config`, `Boost.Graph`). No compiled Boost libraries are needed. `find_package(Boost REQUIRED)` only locates the include path. - **`main` branch is protected** on `origin` (Gitea). Push to `dev`, then merge via pull request. Codeberg `main` can be pushed to directly. - **Both remotes must stay in sync**: `origin` = `git.eulernest.eu` (CI runs here), `codeberg` = `codeberg.org/TMoussa/ConformalLabpp` (public mirror). Push to both after every significant change. diff --git a/README.md b/README.md index 9e9b201..8a50000 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Algorithmic foundation: > DOI: [10.14279/depositonce-5415](https://depositonce.tu-berlin.de/items/8e2988b2-d991-45b5-aad5-9fb7988f3b2f) · CC BY-SA 4.0 · > [Java original](https://github.com/varylab/conformallab) · [sechel.de](https://sechel.de/) -**Status:** v0.9.0 — Phases 1–9a complete, Phase 8b-Lite CGAL API surface. Newton solvers for **five** DCE models (Euclidean / Spherical / HyperIdeal / CP-Euclidean / Inversive-Distance), priority-BFS layout in ℝ²/S²/Poincaré disk, Gauss–Bonnet, tree-cotree cut graph, Möbius holonomy, period matrix (genus 1), fundamental domain, halfedge_uv texture atlas, JSON/XML serialisation, CLI app. **227 CGAL tests + 23 non-CGAL tests, 0 skipped.** +**Status:** v0.9.0 — Phases 1–9a complete, Phase 8b-Lite CGAL API surface. Newton solvers for **five** DCE models (Euclidean / Spherical / HyperIdeal / CP-Euclidean / Inversive-Distance), priority-BFS layout in ℝ²/S²/Poincaré disk, Gauss–Bonnet, tree-cotree cut graph, Möbius holonomy, period matrix (genus 1), fundamental domain, halfedge_uv texture atlas, JSON/XML serialisation, CLI app. Full test suite passing, 0 skipped — see [`doc/api/tests.md`](doc/api/tests.md) for the per-suite breakdown. --- @@ -84,7 +84,7 @@ Layout2D layout = euclidean_layout(mesh, res.x, maps); | **Getting started** — build modes, single-test invocation, CLI, Docker | [doc/getting-started.md](doc/getting-started.md) | | **Pipeline API** — all three geometries, holonomy, serialisation | [doc/api/pipeline.md](doc/api/pipeline.md) | | **Public headers** — all 24 headers with descriptions | [doc/api/headers.md](doc/api/headers.md) | -| **Test suites** — 39 suites, 227+23 tests, individual counts | [doc/api/tests.md](doc/api/tests.md) | +| **Test suites** — per-suite breakdown and counts (single source of truth) | [doc/api/tests.md](doc/api/tests.md) | | **Extending** — new functionals, geometry modes, porting from Java | [doc/api/extending.md](doc/api/extending.md) | | **Processing unit contracts** — preconditions / provides table | [doc/api/contracts.md](doc/api/contracts.md) | | **CGAL package design** — Phase 8 target, YAML pipeline | [doc/api/cgal-package.md](doc/api/cgal-package.md) | diff --git a/code/tests/cgal/CMakeLists.txt b/code/tests/cgal/CMakeLists.txt index b26795c..529b835 100644 --- a/code/tests/cgal/CMakeLists.txt +++ b/code/tests/cgal/CMakeLists.txt @@ -53,8 +53,9 @@ add_executable(conformallab_cgal_tests # ── Java parity: geometry utility tests ────────────────────────────────── # Ported from CuttinUtilityTest, UnwrapUtilityTest, - # ConvergenceUtilityTests, HomologyTest (tests 1–6). - # Test 7 (genus-2 homology) as GTEST_SKIP stub until Phase 8. + # ConvergenceUtilityTests, HomologyTest. All tests active — + # the v0.7.0 genus-2 homology stub was implemented in Phase 7 + # (HomologyGenerators.Genus2_FourCutEdges, brezel2.obj). test_geometry_utils.cpp # ── Scalability smoke tests ──────────────────────────────────────────────── diff --git a/doc/contributing.md b/doc/contributing.md index f3800c8..47003a3 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -29,8 +29,8 @@ Two jobs run on push to `dev`/`main` or on pull requests: | Job | What it tests | Trigger | |---|---|---| -| `test-fast` | 23 non-CGAL tests, no Boost | all branches | -| `test-cgal` | 227 CGAL tests, 0 skipped | `main`, `dev`, PRs only | +| `test-fast` | pure-math tests, no Boost | all branches | +| `test-cgal` | full CGAL test suite | `main`, `dev`, PRs only | A PR is ready to merge when both jobs pass. @@ -51,7 +51,7 @@ Every new algorithm needs: 3. **Registration** — add the `.cpp` file to `code/tests/cgal/CMakeLists.txt`. -Expected CI result: **23 + 227 tests pass, 0 skipped**. +Expected CI result: full test suite passing, 0 skipped (per-suite counts: [`doc/api/tests.md`](api/tests.md)). --- diff --git a/doc/getting-started.md b/doc/getting-started.md index aea5d93..c5100a0 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -48,7 +48,7 @@ cmake --build build --target conformallab_tests -j$(nproc) ctest --test-dir build --output-on-failure ``` -Expected: **23 tests pass**. +Expected: all pure-math tests pass (count: [`doc/api/tests.md`](api/tests.md)). ### Mode 2 — CGAL tests, headless (recommended for CI and development) @@ -60,7 +60,7 @@ cmake --build build --target conformallab_cgal_tests -j$(nproc) ctest --test-dir build -R "^cgal\." --output-on-failure ``` -Expected: **227 tests pass, 0 skipped**. +Expected: all CGAL tests pass, 0 skipped (count: [`doc/api/tests.md`](api/tests.md)). ### Mode 3 — Full local build (CLI app + interactive viewer) @@ -146,7 +146,7 @@ sets x* = 0, so a small perturbation (-0.05) needs only one Newton step. bash scripts/try_it.sh ``` This clones nothing (run from inside the repo), builds the CGAL test suite, runs -all 227+23 tests, and prints a summary. See `scripts/try_it.sh` for details. +the full test suite, and prints a summary. See `scripts/try_it.sh` for details. --- diff --git a/doc/math/validation-protocol.md b/doc/math/validation-protocol.md index 04d3493..5d5a80c 100644 --- a/doc/math/validation-protocol.md +++ b/doc/math/validation-protocol.md @@ -199,7 +199,7 @@ inner/outer edge lengths). Use `torus_8x8.off` for a finer approximation. ## Summary checklist ``` -[ ] Check 0: 176 tests pass, 0 skipped +[ ] Check 0: full test suite passes, 0 skipped (counts: `doc/api/tests.md`) [ ] Check 1: Gauss–Bonnet exact (1e-10) [ ] Check 2: FD gradient < 1e-6 for all 3 geometries [ ] Check 3: Newton convergence < 50 iterations diff --git a/doc/math/validation.md b/doc/math/validation.md index 40bc692..463ba57 100644 --- a/doc/math/validation.md +++ b/doc/math/validation.md @@ -14,7 +14,7 @@ cmake --build build --target conformallab_cgal_tests ctest --test-dir build -R cgal --output-on-failure ``` -All 227 tests pass, 0 skipped (see `doc/api/tests.md`). +All tests pass, 0 skipped (per-suite breakdown: `doc/api/tests.md`). --- @@ -252,7 +252,7 @@ implemented in conformallab++ (→ GC-2 in the phase roadmap). Run these in order to validate the implementation: -- [ ] `ctest --test-dir build -R cgal --output-on-failure` → 227 tests pass, 0 skipped +- [ ] `ctest --test-dir build -R cgal --output-on-failure` → all pass, 0 skipped (count: `doc/api/tests.md`) - [ ] `cgal.GaussBonnet.*` all pass → topology is correctly read from mesh - [ ] `cgal.EuclideanFunctional.GradientCheck_*` pass → energy = integral of gradient - [ ] `cgal.PeriodMatrix.TauInFundamentalDomain_*` pass → SL(2,ℤ) reduction correct diff --git a/doc/release-policy.md b/doc/release-policy.md new file mode 100644 index 0000000..4a53633 --- /dev/null +++ b/doc/release-policy.md @@ -0,0 +1,216 @@ +# 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` | 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`](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 + +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` + `test-cgal` + `doc-build`. +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. diff --git a/scripts/check-test-counts.sh b/scripts/check-test-counts.sh new file mode 100755 index 0000000..0394f1f --- /dev/null +++ b/scripts/check-test-counts.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# scripts/check-test-counts.sh +# +# CI gate that verifies the totals in `doc/api/tests.md` match the +# actual `ctest` output. Catches the canonical test-count document +# going stale even if no other doc hardcodes the number. +# +# Usage: +# bash scripts/check-test-counts.sh +# +# Exit codes: +# 0 totals match +# 1 totals diverge — prints diff and which file to fix +# 2 prerequisites missing (cmake not found, tests.md missing, …) +# +# This script is meant to be cheap enough to run on every PR (~30 s on +# a typical CI runner). It re-uses the existing build-cgal/ directory +# if one is present; otherwise it builds a throwaway build-counts/. + +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." # repo root + +TESTS_MD="doc/api/tests.md" + +if [ ! -f "$TESTS_MD" ]; then + echo "FAIL: $TESTS_MD not found" >&2 + exit 2 +fi +command -v cmake >/dev/null || { echo "FAIL: cmake not in PATH" >&2; exit 2; } +command -v ctest >/dev/null || { echo "FAIL: ctest not in PATH" >&2; exit 2; } + +# ── Build ─────────────────────────────────────────────────────────────────── +# Re-use existing build-cgal/ if it exists with the right flags; otherwise +# create a throwaway build-counts/ directory. +BUILD_DIR="" +if [ -f build-cgal/CMakeCache.txt ] && grep -q "WITH_CGAL_TESTS:.*=ON\|WITH_CGAL:.*=ON" build-cgal/CMakeCache.txt; then + BUILD_DIR=build-cgal +else + BUILD_DIR=build-counts + cmake -S code -B "$BUILD_DIR" -DWITH_CGAL_TESTS=ON -DCMAKE_BUILD_TYPE=Release >/dev/null +fi +cmake --build "$BUILD_DIR" -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 2)" >/dev/null + +# ── Get actual counts ─────────────────────────────────────────────────────── +cd "$BUILD_DIR" +actual_cgal=$(ctest -R "^cgal" 2>&1 | grep "tests passed" | sed -E 's/.*out of ([0-9]+).*/\1/') +actual_fast=$(ctest -E "^cgal" 2>&1 | grep "tests passed" | sed -E 's/.*out of ([0-9]+).*/\1/') +cd - >/dev/null + +# ── Get claimed counts from tests.md ──────────────────────────────────────── +# Expected format (in this order): +# **Total: tests, 0 skipped.** (non-CGAL section) +# ... +# **Total: tests, 0 skipped.** (CGAL section) +claimed_fast=$(grep -E "\*\*Total: [0-9]+ tests, 0 skipped\.\*\*" "$TESTS_MD" \ + | head -n 1 | sed -E 's/.*Total: ([0-9]+) tests.*/\1/') +claimed_cgal=$(grep -E "\*\*Total: [0-9]+ tests, 0 skipped\.\*\*" "$TESTS_MD" \ + | tail -n 1 | sed -E 's/.*Total: ([0-9]+) tests.*/\1/') + +# ── Compare ──────────────────────────────────────────────────────────────── +ok=1 +if [ "${actual_fast}" != "${claimed_fast}" ]; then + echo "MISMATCH: non-CGAL — $TESTS_MD claims ${claimed_fast}, ctest reports ${actual_fast}" + ok=0 +fi +if [ "${actual_cgal}" != "${claimed_cgal}" ]; then + echo "MISMATCH: CGAL — $TESTS_MD claims ${claimed_cgal}, ctest reports ${actual_cgal}" + ok=0 +fi + +if [ "$ok" -eq 0 ]; then + cat <