diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..cc10de4 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(cmake:*)", + "Bash(ctest:*)", + "Bash(nice:*)", + "Bash(./build/conformallab_tests:*)", + "Bash(./build/conformallab_cgal_tests:*)", + "Bash(bash scripts/quality/*)", + "Bash(bash scripts/*)", + "Bash(python3 scripts/*)", + "Bash(codespell:*)", + "Bash(shellcheck:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git branch:*)", + "Bash(git ls-remote:*)", + "Bash(git ls-files:*)", + "Bash(ls:*)", + "Bash(find:*)", + "Bash(grep:*)", + "Bash(rg:*)", + "Bash(wc:*)", + "Bash(curl -s*)" + ], + "deny": [ + "Bash(git push --force* origin main*)", + "Bash(git push -f* origin main*)", + "Bash(git reset --hard*)", + "Bash(rm -rf /*)" + ] + } +} diff --git a/.claude/token-hygiene.md b/.claude/token-hygiene.md new file mode 100644 index 0000000..2be7875 --- /dev/null +++ b/.claude/token-hygiene.md @@ -0,0 +1,62 @@ +# Token-Hygiene — User Guide (Tier 3: Cache-Disziplin) + +Eine kurze Anleitung **für dich** (den Nutzer), wie du die Prompt-Cache-Mechanik +auf deiner Seite richtig bedienst. Claude liest diese Datei mit und **erinnert +dich aktiv**, wenn es eines der unten genannten Anti-Patterns beobachtet. + +Hintergrund: Der stabile Anfang jeder Anfrage (System-Prompt + `CLAUDE.md`) wird +von Anthropic **gecacht**, mit einer **TTL von 5 Minuten**. Solange der Cache +warm ist, zahlst du diesen Teil nicht erneut. Ein Cache-Miss bedeutet: der ganze +Kontext wird uncached neu gelesen — langsamer und teurer. + +--- + +## Die drei Regeln + +### 1. Stabilen Präfix stabil halten +`CLAUDE.md` und Projekt-Config **nicht mitten in einer Arbeitssession editieren**. +Jede Änderung an der Datei bustet den Cache für **alle** folgenden Turns dieser +Session. Sammle Doku-/Config-Änderungen und mach sie in **einem eigenen +Durchgang** (so wie dieser hier), nicht verstreut zwischen Code-Arbeit. + +> **Reminder-Trigger für Claude:** Wenn der Nutzer mitten in einer laufenden +> Code-/Debug-Aufgabe eine Änderung an `CLAUDE.md` oder `.claude/settings.json` +> verlangt → kurz darauf hinweisen, dass das den Cache bustet, und anbieten, es +> zu bündeln/ans Ende zu legen. + +### 2. Keine Kette kurzer Wartepausen +Eine Pause **> 5 Minuten** kostet beim nächsten Turn einen Cache-Miss. Eine +*einzelne* lange Pause ist egal. Schlecht ist, viele kurze „warte mal kurz"-Turns +aneinanderzuhängen, bei denen jeder nach >5 Min den Kontext neu liest. Wenn du +auf etwas Externes wartest (CI, Deploy), lieber **einmal** lang warten als +mehrfach pollen. + +> **Reminder-Trigger für Claude:** Wenn der Nutzer wiederholt kurze +> „warte/poll mal"-Anweisungen mit Lücken um die 5-Minuten-Grenze gibt → einen +> einzelnen längeren Wartezyklus (oder eine Hintergrund-/Monitor-Lösung) +> vorschlagen statt mehrfachem Pollen. + +### 3. Eine Session pro Thema +Lange Multi-Themen-Sessions sind der größte versteckte Kostenfaktor (jeder Turn +schickt die ganze History neu). Nach einer abgeschlossenen Aufgabe: **neue +Session** (`/clear`) statt zum nächsten, unzusammenhängenden Thema im selben +Thread zu wechseln. Das ist eigentlich Tier 1, aber es interagiert direkt mit +dem Cache: ein frischer Thread = kleiner, vollständig gecachter Präfix. + +> **Reminder-Trigger für Claude:** Wenn die Session erkennbar das Thema wechselt +> (z.B. von „Pages-Fix" zu „neues Feature implementieren") → vorschlagen, eine +> frische Session zu starten, und optional die wichtigsten Entscheidungen vorher +> in Memory/Decision-Records festhalten. + +--- + +## Schnell-Check vor einer langen Session + +- [ ] Ist das ein **einzelnes** Thema? Wenn nein → splitten. +- [ ] Stehen anstehende `CLAUDE.md`-Änderungen an? → **vorher** erledigen, dann stabil lassen. +- [ ] Erwarte ich lange Wartezeiten (CI/Build)? → Hintergrund-Job + **eine** lange Wartephase, kein Polling. + +## Verwandte Hebel (nicht Cache, aber gleiche Wirkung) +Tier 1 (Session-Schnitt) und Tier 2 (Kommando-Disziplin: Output filtern, Pfad- +Scope, nicht zurücklesen) stehen als operative Regeln im Abschnitt +*„Agentic workflow patterns" → „Token hygiene"* in der `CLAUDE.md`. diff --git a/.gitignore b/.gitignore index 5379152..91586fc 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,10 @@ Testing/ *.user *.suo -# Claude Code worktrees -.claude/ +# Claude Code worktrees + local/session data (but track shared project config) +.claude/* +!.claude/settings.json +!.claude/token-hygiene.md # Doxygen output doc/doxygen/ diff --git a/CLAUDE.md b/CLAUDE.md index e5c0ea3..59458b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,20 @@ cmake --build build -j$(nproc) `-DWITH_CGAL=ON` automatically enables `-DWITH_VIEWER=ON`, which pulls in GLFW and requires `wayland-scanner`. Never use this in headless CI. +### Compile-time options (v0.10.0 matrix) + +The CGAL test build defaults to **PCH + Unity Build ON** (CGAL test wall-time 78 s → 55 s, CPU time 676 s → 167 s on Apple M1). Opt-in/opt-out flags — full measurements + macOS-vs-Linux notes in `doc/architecture/compile-time.md`: + +| Flag | Default | Effect | +|---|---|---| +| `-DBUILD_TESTING=OFF` | ON | Skip the entire test subtree (headers-only consumers). | +| `-DCONFORMALLAB_USE_PCH=OFF` | ON | Disable precompiled headers for `conformallab_cgal_tests`. | +| `-DCMAKE_UNITY_BUILD=OFF` | ON | Disable Unity (jumbo) build. | +| `-DCONFORMALLAB_DEV_BUILD=ON` | OFF | Dev iteration: PCH on, Unity forced off (cheaper incremental rebuilds). | +| `-DCONFORMALLAB_FAST_TEST_BUILD=ON` | OFF | `-O0 -g` for tests (faster compile, slower run). | +| `-DCONFORMALLAB_USE_CCACHE=ON` | OFF | Route compiles through ccache. | +| `-DCONFORMALLAB_HEADERS_CHECK=ON` | OFF | Standalone header self-containment check target. | + ### Running a single test ```bash @@ -243,87 +257,50 @@ my_map[v] = 3.14; ## CI pipeline -Two jobs in `.gitea/workflows/cpp-tests.yml`: +Three jobs in `.gitea/workflows/cpp-tests.yml`: -| Job | CMake flags | Deps | Triggers on | -|---|---|---|---| -| `test-fast` | *(none)* | Eigen + GTest only | all branches | -| `test-cgal` | `-DWITH_CGAL_TESTS=ON` | + Boost | pull requests only | +| Job | CMake flags | Deps | Triggers on | Status | +|---|---|---|---|---| +| `test-fast` | *(none)* | Eigen + GTest only | all branches | **active** | +| `test-cgal` | `-DWITH_CGAL_TESTS=ON` | + Boost | pull requests only | **DISABLED 2026-05-26** (`if: false`) | +| `quality-gates` | *(none)* | + codespell, shellcheck | all branches (`needs: test-fast`) | **active** | -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`). +Runner: `eulernest` — self-hosted Raspberry Pi, ARM64, Ubuntu 22.04. Docker image: `git.eulernest.eu/conformallab/ci-cpp:latest`. `test-cgal` and `quality-gates` both need `test-fast` to pass first (`needs: test-fast`). -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)). +`quality-gates` runs four required structural gates: `license-headers.sh`, `cgal-conventions.py`, `codespell.sh`, `shellcheck.sh --strict`. Seven more gates (clang-format, cmake-format, cppcheck, sanitizers, clang-tidy, multi-compiler, reproducible-build) are local-only — see `scripts/quality/README.md`. + +**`test-cgal` is gated off** (`if: false`, see the DISABLED-2026-05-26 comment block in the workflow): the `-j1` CGAL build is too memory-heavy for the 1.6 GB runner and OOMs intermittently without exposing real regressions. Consequence: the two structural sub-gates that lived inside it — `scripts/check-test-counts.sh` and `scripts/try_it.sh` — are currently **un-gated**; run them locally before tagging a release. Re-enable by restoring `if: github.event_name == 'pull_request'`. + +Two other workflows are also restricted to `workflow_dispatch:` only (auto-trigger disabled 2026-05-26 while the codeberg pages-branch push is being stabilised): +- `.gitea/workflows/doxygen-pages.yml` — publishes Doxygen HTML + reviewer hub to the codeberg `pages` branch. +- `.gitea/workflows/perf-compile-time.yml` — Linux ARM64 compile-time benchmark. + +Expected results: `test-fast` + `quality-gates` green on every push, 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 -Current release: **v0.9.0** (tag on `main`, released 2026-05-22). -Phases 1–9a complete, Phase 8b-Lite CGAL API surface complete (all 5 DCE models reachable via ``), Phase 9b block-FD HyperIdeal Hessian shipped (~96× speed-up). Next planned milestones: Phase 9c (4g-polygon, genus g > 1) and Phase 9b-analytic (Schläfli identity). See `doc/release-policy.md` for the version-tag policy and `doc/roadmap/phases.md` for the phase plan. +Current release: **v0.10.0** (tag on `main`, released 2026-05-26 — the "reviewer-ready" release). +Phases 1–9a complete, Phase 8b-Lite CGAL API surface complete (all 5 DCE models reachable via ``), Phase 9b block-FD HyperIdeal Hessian shipped (~96× speed-up). v0.10.0 added: 100 % Doxygen public-API coverage (396/396 symbols, 0 warnings), a 14-gate structural quality suite (4 required in CI), the reviewer materials package (`doc/reviewer/`), `output_uv_map` for 4 of 5 DCE entries, six new roadmap phases + three RESEARCH phases, and a six-mode compile-time workflow matrix. Numbers at release: **259/259 tests pass, 0 skipped** (23 non-CGAL + 236 CGAL). Next planned milestones: Phase 9c (4g-polygon, genus g > 1) and Phase 9b-analytic (Schläfli identity). See `doc/release-policy.md` for the version-tag policy, `CHANGELOG.md` for full release notes, and `doc/roadmap/phases.md` for the phase plan. -## Phase 8 strategic decisions (2026-05-19) +## Phase 8 CGAL-package decisions (frozen 2026-05-19) -The CGAL-package architecture was frozen on 2026-05-19. Full design: -[`doc/api/cgal-package.md`](doc/api/cgal-package.md). Key decisions: +Locked architecture choices — full design in [`doc/api/cgal-package.md`](doc/api/cgal-package.md), locked-vs-flexible split in `doc/architecture/locked-vs-flexible.md`: +**MIT license** (no LGPL switch) · **Named Parameters** (`CGAL::parameters::...`) · default kernel **`Simple_cartesian`** · **dual-layer wrapper** (`code/include/*.hpp` = implementation, `include/CGAL/*.h` = thin wrapper, no duplication) · target design is generic **`FaceGraph + HalfedgeGraph`** but ships Surface_mesh-only · upstream CGAL submission is pre-submission-ready, not bound (12+ month horizon). Phase 8 extensions (8a.2 generic FaceGraph, 8c manuals, 8d CGAL-format tests, 8e YAML pipeline) are deferred to on-demand. -| Decision | Choice | -|---|---| -| Submission to upstream CGAL | **Pre-submission-ready, not bound.** 12+ months horizon. | -| License | **MIT preserved** (no LGPL switch). | -| Mesh-type flexibility | **Generic `FaceGraph + HalfedgeGraph`** in target design; MVP starts Surface_mesh-only. | -| Parameter style | **Named Parameters** (`CGAL::parameters::...`). | -| Default kernel | **`Simple_cartesian`** (status quo). | -| Backward compatibility | **Dual-layer wrapper** — `code/include/*.hpp` stays as implementation, `include/CGAL/*.h` is thin wrapper. No algorithm duplication. | -| Implementation strategy | **Hybrid MVP** — minimum Phase 8 (traits + one wrapper) first, then Phase 9 in full, then Phase 8 extensions only on concrete demand. | -| Phase-8 MVP acceptance test | **Phase 9a (Inversive-Distance)** as the first new client of the new traits API. | +## Port-vs-research maintenance rule -### Implementation sequence (committed) - -``` -1. Phase 7.5 Doxygen + cleanup done ✅ -2. Phase 8 MVP — traits + one euclidean wrapper 3–5 days -3. Phase 9a — Inversive-Distance against new traits 3–5 days -4. Phase 9b — analytic HyperIdeal Hessian 1 week -5. Phase 9c — 4g-polygon for genus g > 1 1 week - → port really complete, v0.9.0 release -``` - -Phase 8 extensions (8a.2 generic FaceGraph, 8c full Doxygen manuals, 8d -CGAL-format tests, 8e YAML pipeline) are deferred to on-demand status — -no speculative architecture for an uncertain submission. - -Root-level files added at v0.7.0: -- `CITATION.cff` — machine-readable citation (Sechelmann 2016, Springborn 2020, Bobenko–Springborn 2004) -- `CONTRIBUTING.md` — short root-level pointer to `doc/contributing.md` -- `scripts/try_it.sh` — one-script quickstart: build → 209 tests → example run -- CMake install target: `cmake --install build --prefix /usr/local` → headers land in `include/conformallab/` - -## Port-vs-research maintenance rule (2026-05-21 audit) - -Before claiming something "ports X from Java", **verify empirically**: +Before claiming something "ports X from Java", **verify empirically** — if zero matches, it is **new research** (→ `doc/roadmap/research-track.md` with citations, **not** `doc/roadmap/java-parity.md`): ```bash find /Users/tarikmoussa/Desktop/conformallab -iname "*X*" grep -r "ClassName" /Users/tarikmoussa/Desktop/conformallab/src ``` -If zero matches, the work is **new research** — add it to -`doc/roadmap/research-track.md` with primary literature citations, -**not** to `doc/roadmap/java-parity.md`. - -The 2026-05-21 audit found four pre-existing mis-labels: - -| Item | Wrong claim | Reality | -|---|---|---| -| `InversiveDistanceFunctional` | "Java port (Luo 2004)" | No such Java class exists | -| HyperIdeal Hessian (FD) | "Phase 4a" | Research — Java has `hasHessian()==false` | -| HyperIdeal Hessian (analytic) | "Phase 9b port" | Research — derivation via Schläfli 1858 | -| Tutorial framing | "ports `InversiveDistanceFunctional.java`" | Implementation from Luo 2004 + Glickenstein 2011 | - -All four are corrected as of this commit. Future contributors must -follow the empirical verification rule above before any new claim. +The 2026-05-21 audit corrected four mis-labels (now fixed): `InversiveDistanceFunctional`, both HyperIdeal Hessians (FD + analytic), and the inversive-distance tutorial were all wrongly tagged "Java port" — they are research (no Java parent; Java declares `hasHessian()==false`). Details in `research-track.md`. ## Documentation map -24 documents across 6 categories. Read the relevant one before reasoning from scratch +38 documents across 7 categories. Read the relevant one before reasoning from scratch — do not hallucinate content that is already written down. ### Mathematics & theory @@ -348,6 +325,9 @@ follow the empirical verification rule above before any new claim. | Key architectural decisions and their rationale | `doc/architecture/design-decisions.md` | | Detailed comparison with geometry-central (CMU): overlap, adoption, scientific value | `doc/architecture/geometry-central-comparison.md` | | Phase 9a validation report (CP-Euclidean port + Luo-inversive-distance literature check) | `doc/architecture/phase-9a-validation.md` | +| Compile-time measurements + six-mode workflow matrix (macOS-vs-Linux honesty notes) | `doc/architecture/compile-time.md` | +| Required vs optional dependencies + standalone-verification recipe | `doc/architecture/dependencies.md` | +| Which 12 architecture decisions are locked vs still flexible | `doc/architecture/locked-vs-flexible.md` | ### API & extension @@ -383,6 +363,18 @@ follow the empirical verification rule above before any new claim. | Language policy, test standards, release flow | `doc/contributing.md` | | Versioning rules + release process + single-source-of-truth list | `doc/release-policy.md` | +### Reviewer materials (v0.10.0) + +| Question | Document | +|---|---| +| One-page reviewer briefing | `doc/reviewer/briefing.md` | +| Seven scoped reviewer questions (Q1–Q7) | `doc/reviewer/questions.md` | +| Internal meeting agenda | `doc/reviewer/agenda.md` | +| Reviewer landing index | `doc/reviewer/README.md` | +| Hand-curated reviewer landing page (HTML, source-of-truth for codeberg pages) | `doc/reviewer/hub.html` | + +The published hub lives at https://tmoussa.codeberg.page/ConformalLabpp/ (Doxygen index at `/doxygen.html`). See the "Codeberg pages" quirk below for how it is republished. + ### geometry-central context **geometry-central** (Keenan Crane, CMU) implements the same discrete conformal @@ -393,9 +385,42 @@ The shared mathematical core (Springborn 2020) means cross-validation is meaning Full analysis: `doc/architecture/geometry-central-comparison.md`. Optional adoption roadmap (GC-1/2/3): `doc/roadmap/phases.md` (Optional section). +## Agentic workflow patterns + +Recommended loops when working in this repo. Prefer the cheapest gate that catches the class of error you just touched. + +- **Add/port an algorithm**: new `.hpp` in `code/include/` → test in `code/tests/cgal/` (must include a gradient-check, see Test design patterns) → register in `code/tests/cgal/CMakeLists.txt` → build `conformallab_cgal_tests` → `ctest -R "^cgal\."`. Before claiming "ports X", run the empirical port-vs-research check above. +- **Inner dev loop** (fast iteration): `-DCONFORMALLAB_DEV_BUILD=ON` (PCH on, Unity off) for cheap incremental rebuilds; run a single suite via `--gtest_filter`. Switch back to the default (Unity on) for a final full build. +- **Before any commit**: run the four required gates locally — they mirror CI exactly and are seconds-cheap: `bash scripts/quality/license-headers.sh`, `python3 scripts/quality/cgal-conventions.py`, `bash scripts/quality/codespell.sh`, `bash scripts/quality/shellcheck.sh --strict`. +- **Before tagging a release**: also run the two now-un-gated structural gates (test-cgal is disabled in CI): `BUILD_DIR=build bash scripts/check-test-counts.sh` and `bash scripts/try_it.sh`. Update `CHANGELOG.md`, `CITATION.cff`, and the `doc/api/tests.md` counts (single source of truth). +- **Touching public-API headers**: rebuild Doxygen (`cmake --build build --target doc`) and re-check coverage (`bash scripts/doxygen-coverage.sh --threshold 100`); regenerate `doc/api/headers.md` via `python3 scripts/gen-headers-md.py` (or `bash scripts/regen-docs.sh`). +- **Landing to `main`** (origin is protected): branch → push to `origin` → open PR via `gh`/Gitea API → merge via API → also push `codeberg/main` directly → keep both remotes in sync. +- **Republishing the reviewer hub**: see the Codeberg `pages` quirk below — manual force-push of an orphan branch; verify the live URL with a cache-bust query. +- **Delegation**: this repo's heavy builds are slow on the ARM64 runner — when a task is genuinely parallelisable and independent, consider a background agent; otherwise handle inline. Always verify an agent's actual diff, not just its summary. +- **settings.json**: `.claude/settings.json` (committed) pre-allows the safe read/build/test/quality commands so they don't prompt, and denies destructive git on `main`. Extend the allowlist as new safe commands recur rather than re-approving each time. + +### Token hygiene — session-cut (Tier 1, highest impact) + +Every turn re-sends the whole conversation, so context accumulation is the largest avoidable cost. + +- **One session per task.** After a task is done, start a fresh session (`/clear`) instead of pivoting to an unrelated task in the same thread — the old task's context is dead weight in every later turn. +- **Compact proactively at clean breakpoints.** Run `/compact ` when a task finishes and before the next starts, rather than waiting for auto-compaction at the limit (which you don't control). +- **Delegate read-heavy sweeps to a subagent.** An `Explore`/`Plan` subagent reads large amounts in *its* context and returns a short summary — the bulk never enters the main context. Use it for "where is X used / what depends on Y"; for a *known* path, `Read` directly (a subagent starts cold and only pays off on a large search space). + +### Token hygiene — command discipline (Tier 2) + +- **Scope + filter together.** Path-scope searches (`grep -rn "newton_" code/include/`, not repo-wide). Filter test output (`ctest -R "cgal.NewtonSolver" --output-on-failure`, or `--gtest_filter` for one suite) instead of dumping all 259 results. +- **Never read raw logs into context.** Redirect to a file, then `grep`/`tail` it. With `run_in_background`, read the output file selectively rather than pulling it whole. +- **Don't re-read a file you just edited.** `Edit` errors on stale state — the harness tracks it; a `Read`-back after a successful edit is wasted context. +- **Reference by line number** (`newton_solver.hpp:147`) instead of re-pasting code blocks. + +Cache discipline (Tier 3) is a user-facing guide — see [`.claude/token-hygiene.md`](.claude/token-hygiene.md). **Remind the user of the relevant Tier-3 rule when you observe the matching anti-pattern** (mid-session CLAUDE.md edits, chained sub-5-minute waits, long multi-topic sessions). + ## Known quirks - **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. +- **Both remotes must stay in sync**: `origin` = `git.eulernest.eu` (CI runs here), `codeberg` = `codeberg.org/TMoussa/ConformalLabpp` (public mirror, SSH). Push to both after every significant change. +- **Codeberg `pages` branch is an orphan publish target** that serves the reviewer hub + Doxygen HTML at https://tmoussa.codeberg.page/ConformalLabpp/. Its source-of-truth is `doc/reviewer/hub.html` (installed as `index.html`) + a local Doxygen build (`cmake --build build --target doc`, demoted to `/doxygen.html`). Because the auto-publish workflow (`doxygen-pages.yml`) is on `workflow_dispatch:` only, the branch is **not** refreshed on normal pushes and has been accidentally lost during force-push/merge cleanups. To republish manually: build Doxygen, copy `doc/doxygen/html/.` into a scratch dir, `mv index.html doxygen.html`, copy `hub.html` → `index.html`, then `git init -b pages && git commit && git push -f codeberg pages:pages`. Codeberg pages caches for ~10 min (`Cache-Control: max-age=600`) — verify with a cache-busting `?cb=$(date +%s)` query. Protect the branch in codeberg settings to prevent deletion (leave force-push allowed so CI/manual republish still works). +- **Both `main` branches are independent for the pages cycle**: `origin/main` is protected (PR-only); `codeberg/main` can be pushed directly. diff --git a/doc/release-policy.md b/doc/release-policy.md index e0a8472..6331c6e 100644 --- a/doc/release-policy.md +++ b/doc/release-policy.md @@ -37,8 +37,9 @@ Phase milestone → MINOR-bump mapping (historical + planned): | `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) | +| `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`) @@ -104,6 +105,36 @@ overhead. We pay the click. 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) @@ -137,7 +168,9 @@ The recipe that worked for v0.9.0 (after the false-start with PR #11 / 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`. +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). diff --git a/scripts/quality/README.md b/scripts/quality/README.md index 12ad35e..c732589 100644 --- a/scripts/quality/README.md +++ b/scripts/quality/README.md @@ -19,6 +19,7 @@ consistency + markdown links + end-to-end smoke via `try_it.sh`). | Script | What it checks | Wall time | Prereqs | |---|---|---|---| | `license-headers.sh` | every C++ source carries `SPDX-License-Identifier: MIT` | ~1 s | `bash` | +| `check-doc-freshness.sh` | string-only doc drift: version/date agree across CITATION/CHANGELOG/CLAUDE; doc-map count matches `doc/**/*.md` | <1 s | `bash` | | `cgal-conventions.py` | CGAL-1…6: include-guard format, `\file` brief, namespace nesting, tag-naming, no `using namespace`, no stray `#define` | ~1 s | `python3` | | `clang-format.sh` | every C++ source matches `.clang-format` (dry-run by default; `--fix` to apply) | ~2 s | `clang-format` ≥ 15 | | `cmake-format.sh` | every `CMakeLists.txt` matches `.cmake-format.yaml` + passes `cmake-lint` | ~2 s | `cmake-format` (pip: cmakelang) | diff --git a/scripts/quality/check-doc-freshness.sh b/scripts/quality/check-doc-freshness.sh new file mode 100755 index 0000000..bc6bbd4 --- /dev/null +++ b/scripts/quality/check-doc-freshness.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# scripts/quality/check-doc-freshness.sh +# +# Cheap, string-only documentation-freshness gate. Catches the cross-file +# drift that a "documentation pass" (see doc/release-policy.md) is meant to +# prevent — without building anything, so it runs in well under a second. +# +# Invariants checked: +# 1. Release version agrees across CITATION.cff, CHANGELOG.md (newest +# section) and CLAUDE.md ("Current release:" line). +# 2. Release date agrees across CITATION.cff and CHANGELOG.md. +# 3. The "N documents across ..." count in CLAUDE.md's Documentation map +# equals the actual number of doc/**/*.md files. +# +# NOT checked here (they need a build — run them as part of the release +# documentation pass, see doc/release-policy.md): +# * doc/api/tests.md counts → scripts/check-test-counts.sh (needs BUILD_DIR) +# * doc/api/headers.md regeneration → scripts/gen-headers-md.py (needs Doxygen XML) +# +# Usage: +# bash scripts/quality/check-doc-freshness.sh +# +# Exit codes: +# 0 all string invariants hold +# 1 at least one drift found +# 2 prerequisite missing (a required file is absent) + +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" || exit 2 + +for f in CITATION.cff CHANGELOG.md CLAUDE.md; do + if [ ! -f "$f" ]; then + echo "FAIL: required file not found: $f" >&2 + exit 2 + fi +done + +fail=0 +note() { printf ' %-4s %s\n' "$1" "$2"; } + +# Strip an optional leading "v" so "v0.10.0" and "0.10.0" compare equal. +strip_v() { printf '%s' "${1#v}"; } + +# ── Invariant 1 + 2: version & date triple ────────────────────────────────── +cit_ver=$(strip_v "$(grep -E '^version:' CITATION.cff | head -1 | awk '{print $2}')") +cit_date=$(grep -E '^date-released:' CITATION.cff | head -1 | awk '{print $2}') + +chg_header=$(grep -m1 -E '^## \[' CHANGELOG.md) +chg_ver=$(strip_v "$(printf '%s' "$chg_header" | sed -E 's/^## \[([^]]+)\].*/\1/')") +chg_date=$(printf '%s' "$chg_header" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1) + +claude_ver=$(strip_v "$(grep -m1 'Current release:' CLAUDE.md | sed -E 's/.*\*\*(v?[0-9][0-9.]*)\*\*.*/\1/')") + +echo "── version / date consistency ──" +note "CIT" "CITATION.cff version=$cit_ver date=$cit_date" +note "CHG" "CHANGELOG.md version=$chg_ver date=$chg_date" +note "CLD" "CLAUDE.md version=$claude_ver" + +if [ "$cit_ver" = "$chg_ver" ] && [ "$cit_ver" = "$claude_ver" ]; then + note "OK" "version agrees ($cit_ver)" +else + note "DRIFT" "version mismatch: CITATION=$cit_ver CHANGELOG=$chg_ver CLAUDE=$claude_ver" + fail=1 +fi + +if [ "$cit_date" = "$chg_date" ]; then + note "OK" "date agrees ($cit_date)" +else + note "DRIFT" "date mismatch: CITATION=$cit_date CHANGELOG=$chg_date" + fail=1 +fi + +# ── Invariant 3: documentation-map count ──────────────────────────────────── +stated=$(grep -m1 -E '^[0-9]+ documents across' CLAUDE.md | awk '{print $1}') +actual=$(find doc -name '*.md' -type f | wc -l | tr -d ' ') + +echo +echo "── documentation-map count ──" +note "CLD" "CLAUDE.md states: $stated documents" +note "DISK" "doc/**/*.md on disk: $actual" +if [ "$stated" = "$actual" ]; then + note "OK" "doc count agrees ($actual)" +else + note "DRIFT" "CLAUDE.md says $stated, disk has $actual — update the 'N documents across' line" + fail=1 +fi + +echo +if [ "$fail" -ne 0 ]; then + echo "FAIL: documentation drift detected." + echo "Recovery: run a documentation pass (see doc/release-policy.md →" + echo " 'Documentation passes'); fix the lines above, then re-run." + exit 1 +fi + +echo "OK: documentation strings are consistent." +echo "Reminder: at release also run scripts/check-test-counts.sh and" +echo " scripts/gen-headers-md.py (both need a build)." +exit 0 diff --git a/scripts/quality/run-all.sh b/scripts/quality/run-all.sh index f01f27f..2ae17e9 100755 --- a/scripts/quality/run-all.sh +++ b/scripts/quality/run-all.sh @@ -35,6 +35,7 @@ FAST=0 GATES_FAST=( "License headers | bash scripts/quality/license-headers.sh" + "Doc freshness | bash scripts/quality/check-doc-freshness.sh" "CGAL conventions | python3 scripts/quality/cgal-conventions.py" "clang-format drift | bash scripts/quality/clang-format.sh" "cmake-format/-lint | bash scripts/quality/cmake-format.sh"