diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index a30033ef3..341ba0587 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,63 +1,326 @@
# Contributing to CDT++
-CDT++ v1.0.0 is the final planned C++23 feature release. The repository is
-preserved as a historical scientific reference and regression oracle for
-[causal-triangulations](https://github.com/acgetchell/causal-triangulations),
-the supported Rust successor. Active development, new scientific features,
-and general support belong in that project.
-
-Issue [#97](https://github.com/acgetchell/CDT-plusplus/issues/97) owns the
-final tag, GitHub release, and Zenodo record verification. Issue
-[#155](https://github.com/acgetchell/CDT-plusplus/issues/155) owns the separate
-stabilization gate, tracker closure, and eventual repository archival.
-Archival is not immediate: the repository remains maintenance-only until the
-owner determines that no release blockers remain, after which GitHub will make
-it read-only.
-
-## Final release corrections
-
-Before archival, pull requests are limited to release-blocking correctness,
-reproducibility, security, documentation, and metadata corrections. A
-correction must be tied to an existing release issue, preserve the bounded
-2+1-dimensional spherical scientific contract unless it fixes a documented
-defect, and include deterministic regression evidence when behavior changes.
-A critical defect found after v1.0.0 is published must use a new patch release;
-the v1.0.0 tag is immutable.
-
-Run the repository-owned validation appropriate to the correction:
-
-```bash
-just fix
-just check
-just release-check
+CDT++ v1.0.0 is the final planned C++23 feature release. The repository is preserved as a
+historical scientific reference. Active development, new scientific features, general support, and
+new reports belong in
+[causal-triangulations](https://github.com/acgetchell/causal-triangulations), the supported Rust
+successor.
+
+Issue [#97](https://github.com/acgetchell/CDT-plusplus/issues/97) owns the final tag, GitHub release,
+and Zenodo record verification. Issue
+[#155](https://github.com/acgetchell/CDT-plusplus/issues/155) owns the separate stabilization gate,
+tracker closure, and eventual repository archival. Archival is not immediate: the repository remains
+maintenance-only until the owner determines that no release blockers remain.
+
+## Maintenance scope
+
+Before archival, pull requests are limited to release-blocking correctness, reproducibility,
+security, documentation, and metadata corrections. A correction must:
+
+- be tied to an existing release issue;
+- preserve the bounded 2+1-dimensional spherical scientific contract unless it fixes a documented
+ defect;
+- include deterministic regression evidence when behavior changes;
+- update the user, scientific, API, or release documentation owned by the affected behavior; and
+- preserve the release, citation, reference-fixture, and archival metadata contracts.
+
+A critical defect found after v1.0.0 is published requires a new patch release; the v1.0.0 tag is
+immutable. Do not use this repository for speculative refactors, dependency churn, new platforms,
+new topologies or dimensions, broad performance programs, or new scientific capabilities.
+
+## Environment setup
+
+The repository uses [CMake] and [Ninja] for [C++23] sources, [vcpkg] manifest mode for C++ dependencies,
+[Just] for its command interface, and [uv] for [Python] support scripts. The smallest [pkgx]-assisted Unix
+setup is:
+
+- [Xcode Command Line Tools] on macOS, or a [C++23] compiler and base build environment on Linux;
+- [pkgx], which supplies the pinned Unix tool environment ephemerally;
+- [Just] 1.58.0 or newer; and
+- [Python] 3.14 and [uv] when running Python-backed checks directly.
+
+Without pkgx, provide [Git], [Bash], [CMake] 4.4.0 or newer, [Ninja], [Python], [GNU M4], [Autoconf],
+[Autoconf Archive], [Automake], [GNU Libtool], [Texinfo], and [pkg-config] through the host package manager.
+Documentation work also requires [Doxygen] 1.16.1 and [Graphviz] 15.1.0. The build does not require a personal vcpkg
+checkout, a fork, a submodule, Docker, or a hosted development environment.
+
+For native Windows work, use an x64 [Developer Command Prompt or Developer PowerShell] for
+[Visual Studio] 2022 17.4 or newer, with [MSVC] 19.34 or newer available. Install [Git for Windows]
+and expose [Git Bash] on `PATH`, because the Justfile uses Bash as its recipe shell. The supported build path also
+requires [Just] 1.58.0 or newer, [Python] 3.14 with `python.exe` on `PATH`, [CMake] 4.4.0 or newer, and
+[Ninja]. The tested Windows cell uses Python 3.14.6, CMake 4.4.1, and Ninja 1.13.0. Complete local
+validation additionally requires [uv] 0.12.3, [typos] 1.49.0, and [Go] or [pinact] 4.1.1 for the workflow
+policy checks. The CI cell sets `VCPKG_DEFAULT_TRIPLET=x64-windows`; set the same value when a local
+vcpkg environment would otherwise select a different triplet.
+
+Start with the canonical build and locked Python environment:
+
+```bash
+just python-sync
+just build
+```
+
+`just build` creates an ignored `.cache/vcpkg` checkout at the `builtin-baseline` recorded in
+`vcpkg.json`, installs the manifest dependencies, builds under `out/build/reference`, and runs the
+supported CTest suite. On Unix, `scripts/pkgx-env.sh` can be sourced to expose the same pinned tool
+environment and repository-owned `VCPKG_ROOT` in an interactive shell or IDE. The underlying
+`./scripts/build.sh` and `scripts\build.bat` entry points are available for native troubleshooting.
+
+### Tested release matrix
+
+| CI cell | Host | Compiler | Standard library | Required contract |
+| --- | --- | --- | --- | --- |
+| Ubuntu GCC | `ubuntu-latest` | [GCC] 16 | libstdc++ | `just ci` and `just build-parallel` |
+| Ubuntu Clang | `ubuntu-latest` | [Clang] 22 | libstdc++ | `just ci` and `just build-parallel` |
+| macOS AppleClang | `macos-latest` | [Runner AppleClang][Xcode] | libc++ | `just ci` and `just viewer-build` |
+| Windows MSVC | `windows-latest`, x64 | [Runner MSVC][MSVC] | MSVC STL | `just ci` |
+
+Linux compiler packages are pinned by the [`Justfile`](../Justfile). Native macOS and Windows
+compilers follow the GitHub-hosted runner images. CMake enforces the minimum compiler floor: GCC
+13.3, Clang 22, AppleClang 15, and MSVC 19.34. These are tested release cells, not a support promise
+for every host and compiler pairing.
+
+## Maintainer workflow
+
+Run the narrowest relevant validator while editing, then the complete contract before opening or
+updating a pull request. The primary recipes are:
+
+| Command | Purpose |
+| --- | --- |
+| `just build` | Bootstrap, configure, build, and run the headless smoke suite. |
+| `just build-debug` | Build production Debug targets and run compatible CLI integration tests. |
+| `just build-parallel` | Build and test the opt-in CGAL/oneTBB configuration. |
+| `just check` | Fast, non-mutating repository checks. |
+| `just ci` | Run the complete local pre-commit and pre-push validation contract. |
+| `just clang-tidy` | Analyze project C++ with LLVM 22. |
+| `just coverage` | Generate the Linux GCC LCOV and HTML coverage reports. |
+| `just docs-check` | Generate and validate documentation without changing the worktree. |
+| `just fix` | Format C++, Python, and the Justfile. |
+| `just initialize [ARGS]` | Build as needed and generate an initial triangulation. |
+| `just load INPUT [ARGS]` | Load an initialized triangulation and start a new CDT move series. |
+| `just reference-check` | Validate the committed reference package offline. |
+| `just release-check` | Validate synchronized release metadata and citation fields. |
+| `just resume CHECKPOINT [ARGS]` | Resume the identical CDT move series from a checkpoint. |
+| `just run [ARGS]` | Build as needed and run the primary CDT++ simulation. |
+| `just sanitize KIND` | Run the selected Linux sanitizer preset. |
+| `just viewer-check` | Validate viewer fixtures, manifests, and the tracked image. |
+
+`just check` covers C++ and Python formatting, Python lint and types, spelling, release and citation
+metadata, YAML, GitHub Actions syntax and security, whitespace, CMake preset parsing, Semgrep policy,
+reference-package consistency, and viewer artifacts. `just ci` adds action-pin policy, the supported
+build and test contract, regenerated-reference drift checks, and Python package validation.
+
+The GitHub Actions Ubuntu GCC, Ubuntu Clang, macOS AppleClang, and Windows MSVC cells run `just ci`.
+Both Ubuntu cells also run `just build-parallel`; macOS additionally builds and smoke-tests the
+viewer. Pull requests have separate coverage, generated-documentation, CodeQL, and sanitizer gates.
+
+### Project layout
+
+The primary source and generated-output boundaries are:
+
+- `.github/` — repository policy, issue templates, and CI workflows;
+- `cmake/` — CMake modules and validation helpers;
+- `docs/` — source documentation and generated `docs/html/` output;
+- `examples/` — compiled public API example;
+- `include/` — C++ public and internal headers;
+- `out/build/reference/` — ignored canonical headless build directory;
+- `reference/` — versioned language-neutral fixtures and canonical records;
+- `scripts/` — build, validation, comparison, and release support scripts;
+- `src/` — command-line program sources;
+- `tests/` — doctest, CTest, fixtures, and policy tests; and
+- `viewer/` — versioned render fixtures, manifests, and schemas.
+
+Do not hand-edit generated files or build output. Update their declared source and regenerate through
+the owning Just recipe.
+
+## Build and test validation
+
+The canonical Release build runs 135 CTest registrations: 108 doctest scenarios, 25 CLI integration
+tests, one compiled C++ API example, and one arithmetic-backend correctness test. The parallel
+configuration registers 136 tests: the 108 ordinary doctest scenarios, one parallel launcher with
+five scenarios, the same 25 integration tests, the C++ API example, and the arithmetic test.
+
+To rerun the complete supported suite without rebuilding:
+
+```bash
+ctest --preset reference-smoke
+```
+
+To run a focused category:
+
+```bash
+ctest --preset reference-smoke -L unit
+ctest --preset reference-smoke -L integration
+```
+
+The Debug build compiles the `cdt` and `initialize` production targets, then runs the 21
+Debug-compatible CTest entries labeled `integration`. It defines `CGAL_NDEBUG` because supported move
+paths deliberately traverse invalid intermediate triangulations while keeping CDT++ assertions
+enabled. Release remains the canonical complete test configuration.
+
+For behavior changes, add or update the smallest deterministic unit, integration, reference, or
+compiled-example evidence that would have caught the defect. Randomized CGAL topology counts and
+benchmark timings are diagnostic evidence, not exact correctness oracles.
+
+Checkpoint continuation is a scientific correctness boundary. Its tests must demonstrate that an uninterrupted run
+and the same run split at a persisted checkpoint retain the identical ordered transition trace, cumulative counters,
+and final canonical topology—not merely that a checkpoint can be parsed.
+
+## Documentation
+
+The top-level [`README.md`](../README.md) is for people building, running, and consuming CDT++. Keep
+maintenance mechanics in this guide and put detailed scientific or API contracts in their owning
+pages under `docs/` or `reference/`.
+
+Validate generated API documentation without changing the worktree:
+
+```bash
just docs-check
-just ci
-just build-parallel
```
-`just ci` is the supported headless build and test contract. The two Ubuntu
-compiler cells also run `just build-parallel`; the macOS cell additionally
-runs the opt-in archival viewer. Use `just clang-tidy` for C++ changes and the
-relevant `just sanitize asan`, `just sanitize lsan`, or `just sanitize tsan`
-workflow for memory, lifetime, undefined-behavior, or concurrency changes.
-MemorySanitizer remains experimental because third-party dependencies are not
-instrumented.
+Generate publishable output under `docs/html/` with:
+
+```bash
+just docs
+```
+
+Both recipes require Doxygen 1.16.1 and Graphviz 15.1.0 and use pkgx when matching local tools are
+unavailable. Doxygen 1.16.1 is the archival pin because 1.17.0 duplicates linked labels, emits broken
+alphabetical-index fragments for this repository, and injects an unused Mermaid CDN dependency.
+`scripts/validate_generated_site.py` checks the actual HTML, local links and fragments, duplicate IDs
+and link labels, and required assets.
+
+The documentation workflow runs `just docs-check` as the stable pull-request gate. After a successful
+`main` validation, a separate least-privilege job runs `just docs` and publishes its output to the
+`gh-pages` branch.
+
+## Static analysis and sanitizers
+
+Python 3.14 is selected by `.python-version`; uv locks the environment in `uv.lock`. Ruff owns Python
+formatting and linting, and ty owns static type checking. Use `just python-check` or
+`just python-fix`; both are incorporated into the repository-wide validation recipes.
+
+The C++ project follows the CppCore Guidelines as enforced by Clang-Tidy. Run the repository-pinned
+LLVM 22 configuration with:
+
+```bash
+just clang-tidy
+```
+
+AddressSanitizer plus UndefinedBehaviorSanitizer, LeakSanitizer, MemorySanitizer, and ThreadSanitizer
+share the repository-owned Linux driver and CMake presets:
+
+```bash
+just sanitize asan
+just sanitize lsan
+just sanitize msan
+just sanitize tsan
+```
+
+MemorySanitizer remains experimental because third-party dependencies are not instrumented.
+AddressSanitizer exercises the optional parallel CGAL/oneTBB path; ThreadSanitizer exercises the
+default sequential configuration.
+
+## Coverage
+
+Coverage reporting requires Linux, GNU GCC and its matching gcov, CMake, Ninja, LCOV 2.5 or newer,
+and `genhtml`:
+
+```bash
+CXX=g++ GCOV=gcov just coverage
+```
+
+The recipe writes `build/coverage.info` and `build/coverage-html/index.html`, retaining only
+project-owned `include/` and `src/` paths. It reports line and branch coverage. Function coverage is
+disabled because GCC can emit inconsistent function and line records for generated lambda bodies.
+
+GCC 16 emits three known line-hit and branch-unhit records for templated assignments in
+`Utilities.hpp`. The recipe requires that exact warning count so new or removed inconsistencies fail
+for review. The Codecov workflow uploads only the filtered tracefile and preserves both reports as a
+GitHub Actions artifact; use the LCOV artifact for the independent branch-coverage rate.
+
+## vcpkg maintenance
+
+`vcpkg.json` is the C++ dependency source of truth. Its `builtin-baseline` pins the official
+`microsoft/vcpkg` registry commit. The repository-local `.cache/vcpkg` checkout is disposable tool
+and cache infrastructure and must not be edited or committed.
+
+To update the baseline intentionally, bootstrap the current checkout, run the upstream baseline
+updater, synchronize the independently reviewed bootstrap-tool pins, inspect both diffs, and rebuild:
+
+```bash
+python3 scripts/bootstrap_vcpkg.py
+export VCPKG_ROOT="$PWD/.cache/vcpkg"
+"$VCPKG_ROOT/vcpkg" x-update-baseline
+just sync-vcpkg-tool-pins
+./scripts/build.sh
+```
+
+`just sync-vcpkg-tool-pins` reads the new baseline, fetches that exact upstream commit's tool
+metadata, validates the official Windows amd64 and arm64 assets, and atomically updates the release
+and SHA-256 pins in `scripts/bootstrap_vcpkg.py`. It leaves existing pins unchanged when an input
+cannot be fetched or validated. On Windows, invoke the synchronizer with
+`python.exe scripts\sync_vcpkg_tool_pins.py`.
+
+CodeQL uses a two-phase manual build so third-party implementation findings stay outside CDT++
+results. `just codeql-prepare` installs and configures dependencies before tracing;
+`just codeql-build` then compiles only the `cdt` and `initialize` production targets.
+
+## Submitting a correction
+
+Open a focused pull request against `main` and identify the blocking release issue. The description
+must summarize:
+
+- the release-blocking defect or inconsistency;
+- the correction and why it stays within the maintenance boundary;
+- the validators and tests run;
+- any scientific, compatibility, reproducibility, security, citation, or archival-metadata impact;
+ and
+- any platform-specific validation that could not be run locally.
-Open a focused pull request against `main`, identify the blocking release
-issue, and explain any scientific, compatibility, reproducibility, citation,
-or archival-metadata impact. All required GitHub Actions checks must pass.
+Use a concise conventional title such as `fix:`, `docs:`, `test:`, `build:`, or `ci:`. All required
+GitHub Actions checks must pass. Release tags, GitHub releases, Zenodo deposits, and repository
+archival remain owned by their dedicated release issues and must not be performed from an ordinary
+correction pull request.
## After archival
-Do not fork CDT++ merely to continue its retired development line. Use the
-active successor for new work. Historical forks remain subject to the BSD
-3-Clause license but are not supported by this repository and must not imply
-upstream maintenance or compatibility.
+Do not fork CDT++ merely to continue its retired development line. Use the active successor for new
+work. Historical forks remain subject to the BSD 3-Clause license but are not supported by this
+repository and must not imply upstream maintenance or compatibility.
-Security and support reporting is defined in
-[`SECURITY.md`](../SECURITY.md). The project
-[Code of Conduct](CODE_OF_CONDUCT.md) remains part of the preserved project
-record.
+Security and support reporting is defined in [`SECURITY.md`](../SECURITY.md). The project
+[Code of Conduct](CODE_OF_CONDUCT.md) remains part of the preserved project record. Contributors
+retain credit through the repository history, `CITATION.cff`, and the Zenodo archive.
-Contributors retain credit through the repository history, `CITATION.cff`,
-and the Zenodo archive.
+[Autoconf]: https://www.gnu.org/software/autoconf/
+[Autoconf Archive]: https://www.gnu.org/software/autoconf-archive/
+[Automake]: https://www.gnu.org/software/automake/
+[Bash]: https://www.gnu.org/software/bash/
+[C++23]: https://en.cppreference.com/w/cpp/23
+[Clang]: https://clang.llvm.org
+[CMake]: https://cmake.org
+[Developer Command Prompt or Developer PowerShell]: https://learn.microsoft.com/cpp/build/building-on-the-command-line
+[Doxygen]: https://www.doxygen.nl
+[GCC]: https://gcc.gnu.org
+[Git]: https://git-scm.com
+[Git Bash]: https://gitforwindows.org
+[Git for Windows]: https://gitforwindows.org
+[GNU Libtool]: https://www.gnu.org/software/libtool/
+[GNU M4]: https://www.gnu.org/software/m4/
+[Go]: https://go.dev/doc/install
+[Graphviz]: https://graphviz.org
+[Just]: https://just.systems
+[MSVC]: https://learn.microsoft.com/cpp/overview/visual-cpp-in-visual-studio
+[Ninja]: https://ninja-build.org
+[pkg-config]: https://www.freedesktop.org/wiki/Software/pkg-config/
+[pkgx]: https://pkgx.sh
+[pinact]: https://github.com/suzuki-shunsuke/pinact
+[Python]: https://www.python.org/downloads/
+[Texinfo]: https://www.gnu.org/software/texinfo/
+[typos]: https://github.com/crate-ci/typos
+[uv]: https://docs.astral.sh/uv/
+[Visual Studio]: https://visualstudio.microsoft.com/vs/
+[vcpkg]: https://learn.microsoft.com/vcpkg/
+[Xcode]: https://developer.apple.com/xcode/
+[Xcode Command Line Tools]: https://developer.apple.com/xcode/resources/
diff --git a/Justfile b/Justfile
index 6488f59ae..dc4bf20e5 100644
--- a/Justfile
+++ b/Justfile
@@ -14,6 +14,7 @@ doxygen_version := "1.16.1"
gcc_version := "16"
git_cliff_version := "2.13.1"
graphviz_version := "15.1.0"
+initializer_binary := if os_family() == "windows" { "out/build/reference/src/initialize.exe" } else { "out/build/reference/src/initialize" }
just_version := "1.58.0"
lcov_sha256 := "7e5e5a154bd5f3557659c328cab376764e7abd238bb403c424472c296b175126"
lcov_version := "2.5"
@@ -397,6 +398,16 @@ fix: _format-fix python-fix
just --fmt
@echo "Fixes applied."
+# Build as needed and generate an initial triangulation.
+[group('workflows')]
+initialize *args: build
+ {{ initializer_binary }} {{ args }}
+
+# Load an initialized triangulation and start a new CDT move series.
+[group('workflows')]
+load input *args: build
+ {{ primary_binary }} --input {{ quote(input) }} {{ args }}
+
# Run every non-mutating Python source check.
[group('workflows')]
python-check: python-format-check python-lint python-typecheck python-support-test python-entrypoint-test
@@ -411,22 +422,6 @@ python-entrypoint-test: _sync-python-dev
uv run --no-sync cdt-tag-release --help >/dev/null
uv run --no-sync python scripts/sync_vcpkg_tool_pins.py --help >/dev/null
-# Build the opt-in macOS CGAL/Qt viewer and run its noninteractive render smoke test.
-[group('workflows')]
-viewer-build:
- {{ if os() == "macos" { "just _build-viewer-unix" } else if os() == "windows" { "cmd.exe //d //c \"echo The archival viewer is not supported on Windows. 1>&2 & exit /b 2\"" } else if os() == "linux" { "sh -c 'echo The archival viewer is not supported on Linux. 1>&2; exit 2'" } else { "sh -c 'echo The archival viewer is supported only on macOS. 1>&2; exit 2'" } }}
-
-# Validate the tracked viewer fixture, render manifest, and canonical hero image.
-[group('workflows')]
-viewer-check: _sync-python-dev
- uv run --no-sync python scripts/validate_viewer_artifacts.py
-
-# Regenerate the README hero image from the tracked fixture and render manifest.
-[group('workflows')]
-viewer-render: viewer-build
- {{ viewer_binary }} --manifest {{ quote(viewer_manifest) }} --output {{ quote(viewer_image) }}
- just viewer-check
-
# Apply Ruff lint fixes and formatting to Python source.
[group('workflows')]
python-fix: _sync-python-dev
@@ -533,6 +528,11 @@ reference-regenerate: _sync-python-dev
release-check: _sync-python-dev
uv run --no-sync python scripts/release_check.py
+# Resume the identical CDT move series from a checkpoint.
+[group('workflows')]
+resume checkpoint *args: build
+ {{ primary_binary }} --resume {{ quote(checkpoint) }} {{ args }}
+
# Build as needed and run the primary CDT++ executable.
[group('workflows')]
run *args: build
@@ -619,3 +619,19 @@ update-actions:
just _yaml-check
just _action-lint
just _zizmor
+
+# Build the opt-in macOS CGAL/Qt viewer and run its noninteractive render smoke test.
+[group('workflows')]
+viewer-build:
+ {{ if os() == "macos" { "just _build-viewer-unix" } else if os() == "windows" { "cmd.exe //d //c \"echo The archival viewer is not supported on Windows. 1>&2 & exit /b 2\"" } else if os() == "linux" { "sh -c 'echo The archival viewer is not supported on Linux. 1>&2; exit 2'" } else { "sh -c 'echo The archival viewer is supported only on macOS. 1>&2; exit 2'" } }}
+
+# Validate the tracked viewer fixture, render manifest, and canonical hero image.
+[group('workflows')]
+viewer-check: _sync-python-dev
+ uv run --no-sync python scripts/validate_viewer_artifacts.py
+
+# Regenerate the README hero image from the tracked fixture and render manifest.
+[group('workflows')]
+viewer-render: viewer-build
+ {{ viewer_binary }} --manifest {{ quote(viewer_manifest) }} --output {{ quote(viewer_image) }}
+ just viewer-check
diff --git a/README.md b/README.md
index 9b67c709c..e1b1ae73b 100644
--- a/README.md
+++ b/README.md
@@ -8,15 +8,18 @@
[](https://www.adamgetchell.org/CDT-plusplus/)
[](https://codecov.io/gh/acgetchell/CDT-plusplus)
+CDT++ is the archival [C++23] implementation of spherical 2+1-dimensional
+[Causal Dynamical Triangulations][CDT].
+

This reproducible archival rendering is generated from a tracked triangulation fixture; see the
[viewer and visual-artifact contract](docs/viewer.md).
-## Archival and maintenance status
+## Maintenance status
**CDT++ v1.0.0 is the final planned C++23 feature release.** This repository preserves the C++ implementation as a
-historical scientific reference and regression oracle. After the release and Zenodo handoff in
+historical scientific reference. After the release and Zenodo handoff in
[issue #97](https://github.com/acgetchell/CDT-plusplus/issues/97), it will remain maintenance-only during a
stabilization window. [Issue #155](https://github.com/acgetchell/CDT-plusplus/issues/155) will archive it only after
the repository owner ends the stabilization window and determines that no release blockers remain. It does not
@@ -25,39 +28,27 @@ correctness, reproducibility, security, documentation, and metadata corrections.
requires a new patch release rather than changing the v1.0.0 tag.
For active use, development, and new reports, go to
-[causal-triangulations](https://github.com/acgetchell/causal-triangulations), the supported Rust successor. The local
-Python comparison harness in this archive only orchestrates independent C++ and Rust executables and analyzes their
-declared outputs; it is not a second scientific implementation. See [Security and support](#security-and-support)
-for the post-release reporting boundary.
-
-## Table of contents
-
-- [CDT-plusplus](README.md)
- - [Archival and maintenance status](#archival-and-maintenance-status)
- - [Introduction](#introduction)
- - [Regression-oracle scope](#regression-oracle-scope)
- - [Usage](#usage)
- - [Release scope and limitations](#release-scope-and-limitations)
- - [Quickstart](#quickstart)
- - [Current reference-suite status](#current-reference-suite-status)
- - [Setup](#setup)
- - [Tested release matrix](#tested-release-matrix)
- - [Prerequisites](#prerequisites)
- - [Developer workflow](#developer-workflow)
- - [vcpkg maintenance](#vcpkg-maintenance)
- - [Build](#build)
- - [Project Layout](#project-layout)
- - [Command-line reference](#command-line-reference)
- - [Documentation](#documentation)
- - [Citing CDT++](#citing-cdt)
- - [Testing](#testing)
- - [Static Analysis](#static-analysis)
- - [Sanitizers](#sanitizers)
- - [Offline Comparison](#offline-comparison)
- - [Visualization](#visualization)
- - [Security and support](#security-and-support)
- - [Contributing](#contributing)
- - [Issues](#issues)
+[causal-triangulations](https://github.com/acgetchell/causal-triangulations), the supported Rust successor. See
+[Security and support](#security-and-support) for the post-release reporting boundary.
+
+## Contents
+
+- [Introduction](#introduction)
+- [Features](#features)
+- [Quickstart](#quickstart)
+- [Command-line usage](#command-line-usage)
+- [Build requirements](#build-requirements)
+ - [Tested release matrix](#tested-release-matrix)
+ - [Prerequisites](#prerequisites)
+- [C++ API](#c-api)
+- [Release scope and limitations](#release-scope-and-limitations)
+- [Reproducibility](#reproducibility)
+- [Documentation](#documentation)
+- [Visualization](#visualization)
+- [Citing CDT++](#citing-cdt)
+- [Security and support](#security-and-support)
+- [Contributing](#contributing)
+- [License](#license)
## Introduction
@@ -69,601 +60,328 @@ The implementation uses the
[Computational Geometry Algorithms Library][CGAL], [Boost], and [TBB].
Arbitrary-precision numbers and functions are by [MPFR] and [GMP].
[Melissa E. O'Neill's Permuted Congruential Generators][PCG] library provides high-quality RNGs that pass L'Ecuyer's
-[TestU01] statistical tests.
-[doctest] provides [BDD]/[TDD].
-[vcpkg] provides library management and building.
-[Doxygen] provides automated document generation.
-The supported C++ namespace and per-header contract are recorded in the
-[C++ API boundary](docs/api-boundary.md).
-The compiled [C++ API quickstart](docs/cpp-api-quickstart.md) demonstrates
-validated construction, ten reported Metropolis proposals with separate
-candidate-success and acceptance results, aggregate accounting, and a verified
-persistence round trip.
-The exact CGAL version, kernel, TDS, metadata, lifetime, TBB, benchmark, and
-upgrade policies are recorded in the
-[CGAL 6.2 integration contract](docs/cgal-integration.md).
-The evidence and migration gates for replacing GMP/MPFR with
-Boost.Multiprecision are recorded in the
-[arithmetic backend evaluation](docs/arithmetic-backend-evaluation.md).
-The opt-in operations, ownership and synchronization rules, replayable stress
-inputs, sanitizer boundary, and matched scaling protocol are recorded in the
-[multithreaded CGAL contract](docs/multithreading.md).
-[{fmt}] provides a safe and fast alternative to `iostream`.
-[spdlog] provides fast, multithreaded logging.
-Python and JSON Schema provide the local, offline cross-implementation comparison boundary.
-
-### Regression-oracle scope
-
-The principal reason to preserve this implementation is its causality-filtering Delaunay construction path in
-[`include/Foliated_triangulation.hpp`](https://github.com/acgetchell/CDT-plusplus/blob/main/include/Foliated_triangulation.hpp). `find_invalid_timevalue_cells` classifies
-cells from stored vertex time labels, `has_valid_timevalues` provides the predicate, `find_bad_vertex` selects a vertex
-responsible for an acausal local configuration, and `fix_timevalues` removes offending vertices through CGAL so the
-cavity is retriangulated until the foliation contract is satisfied.
-
-The deterministic doctest scenario **"Detecting and fixing problems with vertices and cells"** in
-[`tests/Foliated_triangulation_test.cpp`](https://github.com/acgetchell/CDT-plusplus/blob/main/tests/Foliated_triangulation_test.cpp) exercises this path with fixed points
-and time labels. Its inputs, detected bad vertex, final initialization state, cell counts, and causal classification
-are the first comparison fixture for `causal-triangulations`; exact Monte Carlo trajectories are not required to
-match.
-
-The versioned, language-neutral fixtures, canonical C++ results, run
-manifests, raw outputs, and Rust consumption rules are published in the
-[`reference/`](reference/README.md) package.
-
-After building, run that fixture directly with:
-
-```bash
-./out/build/reference/tests/CDT_unit_tests \
- --test-case='*Detecting and fixing problems with vertices and cells*'
-```
-
-## Usage
-
-The supported build produces `cdt` and `initialize` in
-`out/build/reference/src`. Run the primary simulation through Just and pass its
-arguments after the recipe name:
-
-```bash
-just run --help
-```
-
-For troubleshooting, the equivalent direct command is
-`./out/build/reference/src/cdt --help`.
-
-Use `--no-output` for batch, debugging, or scripted runs that should print
-results without writing checkpoint or final triangulation files:
-
-```bash
-just run -s -n256 -t4 -a0.6 -k1.1 -l0.1 -p10 -c10 --seed 92 --no-output
-```
-
-With output enabled, every generated `.off` triangulation is accompanied by a
-`.off.meta` provenance manifest containing the effective seed, configuration,
-version/toolchain identity, transition-trace fingerprint, and payload checksum.
-Checkpoint files are validated snapshots, not resumable simulation states; see
-[`docs/reproducibility.md`](docs/reproducibility.md) for the replay and
-persistence contract. Same-seed generation replays the random inputs, while
-exact transition replay requires an identical starting manifold; CDT++ does
-not alter its spherical construction to force CGAL to reproduce one of several
-valid cospherical tetrahedralizations.
-
-- `cdt-viewer` is an opt-in macOS archival target. Build it with `just viewer-build`, regenerate the tracked hero image
- with `just viewer-render`, and see the [viewer contract](docs/viewer.md) for its narrower support boundary.
-- `initialize` is also used by the dependency-free local parameter sweep described under
- [offline comparison](#offline-comparison).
-
-See the [command-line reference](#command-line-reference) for every option.
-Build and dependency instructions begin at [Quickstart](#quickstart).
-
-## Release scope and limitations
-
-The v1.0.0 release supports one scientific model: spherical 2+1-dimensional CDT represented by three-dimensional
-foliated triangulations. It includes the complete audited `(2,3)`, `(3,2)`, `(2,6)`, `(6,2)`, and `(4,4)` move set,
-the Regge action, Metropolis-Hastings evolution, the headless `cdt` and `initialize` programs, deterministic reference
-fixtures, an offline C++/Rust comparison harness, and an opt-in macOS archival viewer.
-
-The release boundary is intentionally narrow:
-
-- Toroidal slices, dimensions other than three, and 3+1D/4D simulations are unsupported. The retained periodic and
- toroidal headers are historical prototypes under `cdt::experimental`, not public v1.0.0 APIs.
-- Requested simplex counts drive a monotone spherical population heuristic; randomized post-repair counts are not an
- exact topology oracle or a calibrated phase-distribution result.
-- A seed replays PCG inputs. Fresh cospherical CGAL construction can still choose another valid tetrahedralization,
- so exact fresh topology and cross-toolchain trajectory identity are not promised.
-- Checkpoints are validated snapshots, not resumable simulations. Exact transition replay requires an identical
- starting manifold and the recorded toolchain contract.
-- Optional oneTBB parallelism is limited to eligible CGAL Delaunay insertion and removal. Pachner moves,
- Metropolis-Hastings, persistence, and access to one manifold remain sequential and externally serialized.
-- CDT++ publishes a C++23 source boundary, not a stable binary ABI or package-registry distribution.
-
-The detailed evidence and failure boundaries are in the [CGAL integration](docs/cgal-integration.md),
-[ergodic-move](docs/ergodic-moves.md), [Metropolis-Hastings](docs/metropolis-hastings.md),
-[reproducibility and persistence](docs/reproducibility.md), and [multithreading](docs/multithreading.md) contracts.
+[TestU01] statistical tests. [{fmt}] provides a safe and fast alternative to `iostream`, and [spdlog] provides fast,
+multithreaded logging. [vcpkg] provides library management and building, and [Doxygen] provides automated document
+generation. Python and JSON Schema provide the local, offline cross-implementation comparison boundary.
+
+The primary `cdt` program generates and evolves spacetime ensembles. The `initialize` program produces initial
+foliated triangulations and supports the retained local parameter sweep. The opt-in `cdt-viewer` program renders
+tracked triangulations on macOS without adding Qt to the default headless build.
+
+## Features
+
+- [x] 3D simplex representation.
+- [x] 3D spherical triangulations with 2+1 foliation.
+- [x] S3 bulk action and the complete audited `(2,3)`, `(3,2)`, `(2,6)`, `(6,2)`, and `(4,4)` ergodic move set.
+- [x] 3D Metropolis-Hastings evolution with separate candidate-success and acceptance results.
+- [x] High-quality random number generation with M.E. O'Neill's [PCG] library and named replayable streams.
+- [x] Validated CDT++ `.off` persistence with provenance sidecars, payload checksums, geometry and topology
+ fingerprints, and transition-trace fingerprints.
+- [x] Reusable initial-state generation: `initialize` writes a manifested triangulation that `cdt --input` validates
+ and evolves with a new transition seed.
+- [x] Exact checkpoint continuation for interrupted [Slurm] and HPC runs: `cdt --resume` restores the triangulation,
+ global pass, PCG state, transition trace, and cumulative counters, then continues the identical Markov chain on
+ the recorded producer toolchain.
+- [x] A documented C++23 source API plus the headless `cdt` and `initialize` programs.
+- [x] Versioned, language-neutral reference fixtures and a lightweight local/offline C++/Rust comparison harness.
+- [x] Optional parallel triangulation with [TBB] for eligible CGAL Delaunay insertion and removal.
+- [x] Optional visualization with [Qt] and a reproducible repository-owned hero artifact.
+- [x] Cross-platform support on Linux, macOS, and Windows and cross-compiler support on GCC, Clang, AppleClang, and
+ MSVC through the tested release matrix.
## Quickstart
From a fresh checkout, the primary supported headless build, dependency bootstrap, and test path is:
```bash
+git clone https://github.com/acgetchell/CDT-plusplus.git
+cd CDT-plusplus
just build
```
The `build` recipe uses the Release configuration and the pkgx launcher on Unix when pkgx is available, then delegates
-to `scripts/build.sh` on Unix or `scripts/build.bat` on Windows. Its
-first run creates an ignored `.cache/vcpkg` checkout at the exact `builtin-baseline` recorded in `vcpkg.json`,
-bootstraps vcpkg, installs the manifest dependencies, builds in `out/build/reference`, and runs the supported CTest
-smoke suite. The first dependency build can take several minutes; subsequent runs reuse both vcpkg dependencies and
-CMake/Ninja outputs, so an unchanged build is a no-op apart from configuration and tests.
+to `scripts/build.sh` on Unix or `scripts/build.bat` on Windows. Its first run creates an ignored `.cache/vcpkg`
+checkout at the exact `builtin-baseline` recorded in `vcpkg.json`, bootstraps vcpkg, installs the manifest
+dependencies, builds in `out/build/reference`, and runs the supported CTest smoke suite. The first dependency build
+can take several minutes; subsequent runs reuse both vcpkg dependencies and CMake/Ninja outputs, so an unchanged
+build is a no-op apart from configuration and tests.
-For a production build with CDT++ assertions enabled, use the focused Debug
-workflow:
+The supported build products are:
-```bash
-just build-debug
+```text
+out/build/reference/src/cdt
+out/build/reference/src/initialize
```
-The Debug build preset compiles the `cdt` and `initialize` production targets,
-then the `debug-cli` test preset runs the 21 Debug-compatible CTest entries
-labeled `integration`. It defines `CGAL_NDEBUG` because supported move paths
-deliberately traverse invalid intermediate triangulations, while leaving
-CDT++'s own assertions enabled. The `cdt` and `cdt-no-output` simulation tests
-and the doctest unit suite are excluded because those paths trip project
-invariant assertions on the intermediate state. Release remains the canonical
-complete test configuration.
+The underlying `./scripts/build.sh` and `scripts\build.bat` entry points remain available for troubleshooting and
+native Windows use. Direct script invocations must expose CMake 4.4.0 or newer on `PATH`; the canonical pkgx-backed
+Just recipes select the tested 4.4.1 toolchain automatically.
+
+Run `just --list` for the complete list of repository commands and their one-line descriptions. The primary user
+entry points are `just initialize`, `just load`, `just resume`, and `just run`.
+
+## Command-line usage
-On Unix, compiler caching is optional. The pkgx launcher supplies the repository-pinned ccache binary when
-`CDT_COMPILER_CACHE=ccache`; leaving the variable unset or setting it to `off` preserves the uncached build:
+The supported build produces `cdt` and `initialize` in
+`out/build/reference/src`. Run either program through Just and pass its
+arguments after the recipe name:
```bash
-CDT_COMPILER_CACHE=ccache just build
+just initialize --help
+just run --help
```
-Compiler caching covers project compilation; vcpkg binary caching remains responsible for reusing compiled
-third-party packages.
+For troubleshooting, the equivalent direct commands are
+`./out/build/reference/src/initialize --help` and
+`./out/build/reference/src/cdt --help`.
-The optional [pkgx](https://pkgx.sh/) entry point supplies the complete Unix developer-tool environment ephemerally
-and invokes the same build contract directly:
+### Run a simulation
+
+The supported simulation surface is the spherical, three-dimensional form:
```bash
-./scripts/pkgx-build.sh
+just run --spherical \
+ --simplices 32000 \
+ --timeslices 11 \
+ --alpha 0.6 \
+ --k 1.1 \
+ --lambda 0.1 \
+ --passes 1000 \
+ --seed 92
```
-To expose the same dependency-build tools to an interactive shell, source the
-reusable environment script:
+Use `--no-output` for batch, debugging, or scripted runs that should print
+results without writing checkpoint or final triangulation files:
```bash
-source scripts/pkgx-env.sh
+just run -s -n256 -t4 -a0.6 -k1.1 -l0.1 -p10 -c10 --seed 92 --no-output
```
-In CLion, open **Settings | Build, Execution, Deployment | Toolchains**, select
-the local toolchain, choose **Add environment | From file**, and select
-`scripts/pkgx-env.sh`. The script supplies the pkgx tool environment and points
-`VCPKG_ROOT` at the repository-owned `.cache/vcpkg` checkout while keeping CGAL
-and the other project libraries under vcpkg control.
-
-pkgx does not install CGAL or any other project library; those remain owned by the pinned vcpkg manifest. The
-underlying `./scripts/build.sh` and `scripts\build.bat` entry points remain available for troubleshooting and native
-Windows development.
-
-### Current reference-suite status
-
-With the pinned baseline, the reference configuration and build succeed on
-macOS with AppleClang. The cross-platform `just build` command runs all 131
-CTest registrations through `scripts/build.sh` on Unix and `scripts/build.bat`
-on Windows: 106 doctest scenarios, 23 CLI integration tests, one compiled C++
-API example, and one arithmetic-backend correctness test. The same
-`reference-smoke` preset is the
-supported local and CI contract; there are no overlapping focused
-registrations that can pass while omitting another doctest suite. The current
-`parallel` preset also registers 132 tests: 106 ordinary doctest scenarios, one
-parallel launcher containing five scenarios, the same 23 CLI integration
-tests, the C++ API example, and the arithmetic correctness test. The
-parallel-enabled
-AddressSanitizer configuration exercises the same replayable stress contract.
-
-## Setup
-
-This project uses [CMake]+[Ninja] to build C++23 sources and [vcpkg] manifest mode to manage C++ libraries. The
-v1.0.0 release matrix is defined by
-[`.github/workflows/ci.yml`](https://github.com/acgetchell/CDT-plusplus/blob/main/.github/workflows/ci.yml) and the
-pinned tool versions in the
-[`Justfile`](https://github.com/acgetchell/CDT-plusplus/blob/main/Justfile).
-
-### Tested release matrix
-
-| CI cell | Host | Compiler | Standard library | Required contract |
-| --- | --- | --- | --- | --- |
-| Ubuntu GCC | `ubuntu-latest` | GCC 16 | libstdc++ | `just ci` and `just build-parallel` |
-| Ubuntu Clang | `ubuntu-latest` | Clang 22 | libstdc++ | `just ci` and `just build-parallel` |
-| macOS AppleClang | `macos-latest` | Runner AppleClang | libc++ | `just ci` and `just viewer-build` |
-| Windows MSVC | `windows-latest`, x64 | Runner MSVC | MSVC STL | `just ci` |
-
-Linux compiler packages are pinned by the Justfile. The native macOS and Windows compilers follow the GitHub-hosted
-runner images, while CMake enforces the minimum C++23 floor: GCC 13.3, Clang 22, AppleClang 15, and MSVC 19.34.
-These are tested release cells, not a claim that every distribution, operating-system version, architecture, or
-compiler/standard-library pairing is supported.
-
-### Prerequisites
-
-The smallest pkgx-assisted host setup is:
-
-- Xcode Command Line Tools on macOS, or a C++23 compiler and base build environment on Linux
-- pkgx
-- Just, used by the recipes and `scripts/pkgx-build.sh` to resolve the repository's tool-version pins
-- Python 3.14 for native dependency bootstrap, and uv when checking or running the Python support scripts
-- Doxygen 1.16.1 and Graphviz 15.1.0 when checking or generating API documentation; pkgx can supply both
+### Generate and load an initial triangulation
-The pkgx build and documentation launchers supply their required tools ephemerally, including Git, Bash, CMake,
-Ninja, Python, Doxygen, Graphviz, M4, Autoconf, Autoconf Archive, Automake, GNU Libtool, Texinfo, and pkg-config. If
-pkgx is not installed, provide these tools conventionally through a package manager such as [Homebrew] or apt:
+To generate an initial triangulation once and start a separate CDT run from
+that exact state:
-- Git
-- Bash
-- build-essential (Linux only)
-- m4
-- automake
-- autoconf
-- autoconf-archive
-- libtool (macOS) or libtool-bin (Linux)
-- pkg-config
-- texinfo
-- ninja (macOS) or ninja-build (Linux)
+```bash
+just initialize \
+ --spherical --simplices 640 --timeslices 4 --output --seed 92
-The build does not require a pre-existing personal vcpkg checkout, a fork, a submodule, Docker, or a hosted
-development environment.
+just load /path/to/the/generated-file.off \
+ --alpha 0.6 --k 1.1 --lambda 0.1 --passes 1000 --seed 93
+```
-### Developer workflow
+Keep the generated `.off` and `.off.meta` files together. `cdt --input`
+verifies the pair, reconstructs the same initial causal triangulation, and
+starts a new Metropolis-Hastings transition stream from the second command's
+seed. It accepts only an `initial-triangulation` artifact from `initialize`;
+checkpoint and final artifacts cannot start a new chain through `--input`.
-The repository-root `Justfile` provides the same small command vocabulary used by the related projects:
+`just load` makes the new-series intent explicit. Its general equivalent is
+`just run --input PATH`; in both forms, `cdt` validates the manifest's artifact
+role rather than asking the Justfile to interpret persistence metadata.
-```bash
-just check # Fast, non-mutating local checks
-just build-debug # Build Debug targets and run compatible CLI integration tests
-just build-parallel # Build and test the opt-in CGAL/oneTBB configuration
-just codeql-prepare # Configure dependencies before CodeQL tracing
-just codeql-build # Build production targets for CodeQL extraction
-just fix # Format C++/Python source and the Justfile
-just clang-tidy # Analyze C++ with LLVM 22
-just sanitize asan # Build and exercise one Linux sanitizer preset
-just build # Bootstrap, configure, build, and smoke-test
-just run --help # Build as needed and run cdt with forwarded arguments
-just ci # Comprehensive pre-commit/pre-push validation
-just docs-check # Validate Doxygen output without changing the worktree
-just docs # Generate publishable documentation in docs/html
-just coverage # Generate Linux GCC LCOV and HTML coverage reports
-just release-check # Validate release metadata and citation fields
-just changelog-unreleased vX.Y.Z # Generate a pending release changelog
-just tag-check vX.Y.Z # Preview and validate an annotated release tag
-just update-actions # Update and repin Actions with pinact, then validate
-just sync-vcpkg-tool-pins # Sync the vcpkg tool release and Windows hashes
-just python-sync # Install the locked Python development environment
-just python-check # Check Python formatting, lint, and types
-just python-fix # Apply safe Ruff fixes and formatting
-just comparison-run /path/to/rust-fixture out/comparisons/run-1 # Run and retain one comparison
-just comparison-analyze out/comparisons/run-1 # Reanalyze without executing C++ or Rust
-just spell-check # Check repository text and identifiers for typos
-```
+### Resume a checkpoint
-`check` covers repository-wide C++ formatting, Python formatting/lint/type checks, spelling, release metadata and
-citation fields, YAML, GitHub Actions syntax and security, whitespace, and CMake preset parsing. `ci` adds the pinact policy
-check and the supported build/test contract. Documentation validation remains available separately through
-`just docs-check`. The GitHub Actions Ubuntu GCC, Ubuntu Clang, macOS
-AppleClang, and Windows MSVC jobs all run `just ci`; the two Ubuntu jobs also
-run `just build-parallel`, while the macOS job builds and smoke-tests the opt-in
-viewer. Pull requests also run the distinct coverage and generated-documentation
-gates. The Ubuntu compiler jobs use the pinned pkgx ccache package with a
-compiler-specific persistent cache. Sanitizer builds use Release
-semantics with sanitizer-provided `-O1 -g` flags, while coverage uses Release
-semantics with coverage-provided `-O0 -g` flags; no duplicate full-suite Debug
-job is needed. Windows continues to compile with native MSVC; the
-locked Python environment supplies `clang-format` only as a source formatter.
-Install the developer tools with Homebrew, use equivalent system packages, or
-let pkgx supply the Unix environment ephemerally; pkgx remains optional. For
-example:
+To continue an interrupted run from a checkpoint, keep its `.off` and
+`.off.meta` files together and pass the payload to `--resume`:
```bash
-just python-sync
-pkgx +just.systems@1.58.0 +git-scm.org +cmake.org@4.4.1 +ninja-build.org +python.org just check
+just resume /path/to/checkpoint-pass-500.off
```
-All configure paths require CMake 4.4.0 or newer. The Justfile owns the tested
-4.4.1 toolchain pin: pkgx-backed recipes remain reproducible at that version,
-while direct configure paths accept newer compatible CMake releases. CI is
-pkgx-first; because pkgx does not currently publish its CMake and Ninja packages
-for Windows, that job uses the exact Justfile pins available as PyPI wheels
-through `uv tool install --no-build`.
+The saved seed, action parameters, thread limit, checkpoint cadence, complete
+PCG state, transition trace, and cumulative move counters are restored. By
+default, the run continues to its originally configured total pass count. Use
+`--passes TOTAL` to extend that target; `TOTAL` is the global target, not a
+number of additional passes. Exact resume requires the same CDT++ source
+revision, compiler and standard library, build configuration and parallel
+feature, platform, and CGAL version that produced the checkpoint.
-[pinact](https://github.com/suzuki-shunsuke/pinact) uses [`.pinact.yaml`](https://github.com/acgetchell/CDT-plusplus/blob/main/.pinact.yaml) to retain immutable action
-SHAs, readable release comments, and a seven-day release cooldown. `just update-actions` uses an installed pinact,
-Go, or a pkgx-provided Go fallback, then requires `yamllint`, `actionlint`, and `zizmor` to pass. Direct third-party
-Python dependencies install only from locked wheels. The uv environment provides `clang-format` and `yamllint`;
-actionlint uses its pinned upstream version, and zizmor uses its pinned PyPI wheel through `uvx`.
+`just resume` makes the identical-continuation intent explicit. Its general
+equivalent is `just run --resume PATH`.
-### vcpkg maintenance
+### Generate many initial triangulations
-`vcpkg.json` is the dependency source of truth. Its `builtin-baseline` pins the official
-[`microsoft/vcpkg`](https://github.com/microsoft/vcpkg) registry commit used locally and in CI. The repository-local
-`.cache/vcpkg` checkout is disposable tool/cache infrastructure and must not be edited or committed.
-The native build entry points delegate checkout provenance, baseline, and executable-integrity validation directly
-to `scripts/bootstrap_vcpkg.py`, whose cross-platform fixtures run under `just check`.
-
-To update dependencies intentionally, bootstrap the current checkout, run the vcpkg baseline updater, synchronize
-the independently reviewed tool pins, review both diffs, and then rerun the complete build:
+For a collection of random starting states, give each seed its own directory
+so timestamped output names cannot collide:
```bash
-python3 scripts/bootstrap_vcpkg.py
-export VCPKG_ROOT="$PWD/.cache/vcpkg"
-"$VCPKG_ROOT/vcpkg" x-update-baseline
-just sync-vcpkg-tool-pins
-./scripts/build.sh
+for seed in $(seq 1 100); do
+ mkdir -p "out/initial/seed-$seed"
+ (
+ cd "out/initial/seed-$seed"
+ ../../build/reference/src/initialize \
+ --spherical --simplices 640 --timeslices 4 --output --seed "$seed"
+ )
+done
```
-`just sync-vcpkg-tool-pins` reads the new manifest baseline, fetches that exact upstream commit's tool metadata,
-downloads the official Windows amd64 and arm64 release assets, and atomically updates the release and SHA-256 pins in
-`scripts/bootstrap_vcpkg.py`. It leaves the existing pins unchanged if any input cannot be fetched or validated, and
-only writes when the rendered release and hash assignments differ from the bootstrap source.
-
-On Windows, invoke the synchronizer with `python.exe scripts\sync_vcpkg_tool_pins.py`; `scripts\build.bat` and
-`scripts\fast-build.bat` already invoke the bootstrap implementation directly.
-
-CI uses the repository's cached-vcpkg action, which derives the vcpkg checkout commit from the same manifest baseline,
-stores compiler-specific binary archives through `actions/cache`, and separately caches downloaded source archives.
-No separately maintained checkout SHA is required; the bootstrap script retains an independent tool release and
-Windows executable hashes as a supply-chain review gate.
-
-CodeQL keeps third-party implementation findings out of CDT++ results through a two-phase manual build.
-`just codeql-prepare` configures the project, installs manifest dependencies before CodeQL starts tracing, and uses a
-build directory under the host temporary directory so installed headers are outside the checkout. After CodeQL
-initialization, `just codeql-build` compiles only the `cdt` and `initialize` production targets with tests disabled.
-The regular `just build` and `just ci` contracts continue to build and run the complete test suite.
-
-## Build
-
-Run `just build` from the repository root. It delegates to `./scripts/build.sh` on Unix and `scripts\build.bat` on
-Windows; either platform-specific script can itself be run from any working directory for troubleshooting. If
-`VCPKG_ROOT` already names the clean official checkout at the manifest baseline, the script respects it; otherwise it
-uses the pinned disposable checkout described above. Both scripts invoke the `reference` configure and build presets
-followed by the `reference-smoke` test preset; products and tests are isolated under `out/build/reference`, while
-`scripts\fast-build.bat` configures the same reference tree and builds only the primary `cdt` target. All entry points
-preserve a compatible CMake cache and refresh it only when the selected vcpkg toolchain path changes. Direct script
-invocations must expose CMake 4.4.0 or newer on `PATH`; the canonical pkgx-backed `just` recipes select the tested
-4.4.1 toolchain automatically.
-
-The archival Qt viewer is deliberately outside that cross-platform headless contract. On macOS, `just viewer-build`
-selects the separate `viewer` preset and vcpkg feature, builds `cdt-viewer` under `out/build/viewer`, and runs its
-noninteractive image smoke test. The macOS CI cell runs that same focused recipe after the headless contract; Qt and
-Eigen are installed only in the viewer build tree. See
-[`docs/viewer.md`](docs/viewer.md) for regeneration, version pins, and portability rules.
-
-### Project Layout
-
-The repository-owned source and generated-output boundaries are:
-
-- .github - GitHub specific settings
-- out/build/reference - Ephemeral supported headless build directory
-- cmake - Cmake configurations
-- docs - Documentation
-- external - Includes submodules of external projects (none so far, all using [vcpkg])
-- include - Header files
-- scripts - Build, test, and run scripts
-- src - Source files
-- tests - Unit tests
-- viewer - Versioned archival render fixtures, manifests, and schemas
-
-## Command-line reference
+The cross-executable `initialize-to-cdt` CTest exercises this handoff. The
+[reference fixture package](reference/README.md) explains how the pair can
+serve as the input boundary for a future native `causal-triangulations`
+importer without treating the payload as generic mesh OFF.
-The supported simulation surface is the spherical, three-dimensional form:
-
-```bash
-just run --spherical \
- --simplices 32000 \
- --timeslices 11 \
- --alpha 0.6 \
- --k 1.1 \
- --lambda 0.1 \
- --passes 1000 \
- --seed 92
-```
+### Options and constraints
Run `just run --help` for the executable-owned option list and `just run --version` for the synchronized product
version. Long options and their defined short forms are parsed by [Boost.Program_options][program_options]. The
legacy parser still names toroidal topology and dimensionality, but runtime validation rejects toroidal input and
every dimension other than three; they are not supported release modes.
-`--threads` is a maximum concurrency limit for CGAL/oneTBB bulk Delaunay
-operations. It defaults to 1. Zero and negative values are rejected. The
-canonical reference build accepts only 1; values greater than 1 require the
-`parallel` preset. This option does not parallelize Metropolis-Hastings,
-Pachner moves, persistence, or concurrent access to one manifold.
+`--input` cannot be combined with `--spherical`, `--toroidal`, `--simplices`, `--timeslices`, `--dimensions`,
+`--init`, or `--foliate`; those construction values come from the validated artifact. Action parameters, pass and
+checkpoint cadence, output control, thread limit, and the new run seed remain run-specific options.
+
+`--resume` cannot be combined with replacements for the restored seed, action parameters, thread limit, and
+checkpoint cadence, or with topology and construction options. An explicitly supplied `--passes TOTAL` may retain
+or extend the global target; it cannot be less than the checkpoint's completed pass. Checkpoint numbering and
+cadence remain global across the interruption.
+
+`--threads` is a maximum concurrency limit for CGAL/oneTBB bulk Delaunay operations. It defaults to 1. Zero and
+negative values are rejected. The canonical reference build accepts only 1; values greater than 1 require the
+`parallel` preset. This option does not parallelize Metropolis-Hastings, Pachner moves, persistence, or concurrent
+access to one manifold.
With `--dimensions 3`, every spatial slice is two-dimensional and the third dimension is the global time foliation.
The accepted runtime boundary requires positive simplex and timeslice counts, finite physical parameters with
`alpha > 1/2`, and a positive thread limit. Invalid configurations fail before construction.
-## Documentation
+With output enabled, every generated `.off` triangulation is accompanied by a `.off.meta` provenance manifest
+containing the effective seed, configuration, version/toolchain identity, transition-trace fingerprint, and payload
+checksum. Checkpoint manifests additionally preserve the exact transition-engine state and cumulative accounting
+needed by `--resume`; see [`docs/reproducibility.md`](docs/reproducibility.md) for the resume, replay, and persistence
+contracts.
-Online documentation is at .
+## Build requirements
-The compiled [C++ API quickstart](docs/cpp-api-quickstart.md) is the canonical
-end-to-end public API example and is embedded verbatim in the generated site.
-
-The scientific transition, proposal-ratio, geometry-delta, counter, and
-precision contracts are recorded in
-[`docs/metropolis-hastings.md`](docs/metropolis-hastings.md).
-The cross-language schema, exact-versus-numerical comparison policy, and raw
-archival records are documented in
-[`reference/README.md`](reference/README.md).
-The literature-backed contracts, exact deltas, inverse relationships, and
-failure-atomicity rules for the complete 2+1D move set are recorded in
-[`docs/ergodic-moves.md`](docs/ergodic-moves.md).
-Seed replay, PCG stream ownership, checkpoint metadata, and the parallel stream
-policy are recorded in [`docs/reproducibility.md`](docs/reproducibility.md).
-The repository-wide scientific bibliography is
-[`REFERENCES.md`](REFERENCES.md).
-
-Validate the generated API documentation without modifying the worktree:
+### Tested release matrix
-```bash
-just docs-check
-```
+| CI cell | Host | Compiler | Standard library | Required contract |
+| --- | --- | --- | --- | --- |
+| Ubuntu GCC | `ubuntu-latest` | [GCC] 16 | libstdc++ | `just ci` and `just build-parallel` |
+| Ubuntu Clang | `ubuntu-latest` | [Clang] 22 | libstdc++ | `just ci` and `just build-parallel` |
+| macOS AppleClang | `macos-latest` | [Runner AppleClang][Xcode] | libc++ | `just ci` and `just viewer-build` |
+| Windows MSVC | `windows-latest`, x64 | [Runner MSVC][MSVC] | MSVC STL | `just ci` |
-To generate the same publishable output used by the documentation workflow, run `just docs`; it writes `docs/html/`
-only after strict generation and generated-site validation succeed. Both recipes require Doxygen 1.16.1 and Graphviz
-15.1.0 and use pkgx ephemerally when matching local tools are unavailable. Doxygen 1.16.1 is the archival pin because
-1.17.0 duplicates linked labels, emits broken alphabetical-index fragments for this repository, and injects an unused
-Mermaid CDN dependency. `scripts/validate_generated_site.py` preserves that bounded workaround by checking the actual
-HTML, local links and fragments, duplicate IDs and link labels, and required assets. `USE_MATHJAX` allows [MathJax] to render LaTeX formulae, and
-`HAVE_DOT` enables [GraphViz] diagrams. Documentation validation is intentionally separate from the cross-platform
-`just ci` contract. The documentation workflow runs `just docs-check` as the stable `docs` pull-request gate. After a
-successful `main` validation, a separate least-privilege job runs `just docs` and publishes its output to the
-`gh-pages` branch.
+Linux compiler packages are pinned by the Justfile. The native macOS and Windows compilers follow the GitHub-hosted
+runner images, while CMake enforces the minimum C++23 floor: GCC 13.3, Clang 22, AppleClang 15, and MSVC 19.34.
+These are tested release cells, not a claim that every distribution, operating-system version, architecture, or
+compiler/standard-library pairing is supported.
-## Citing CDT++
+### Prerequisites
-If CDT++ contributes to published work, cite the software using
-[`CITATION.cff`](https://github.com/acgetchell/CDT-plusplus/blob/main/CITATION.cff) and cite the scientific methods relevant to the
-work from [`REFERENCES.md`](REFERENCES.md). The software citation records the
-final release, `1.0.0`, and the all-versions Zenodo concept DOI
-[`10.5281/zenodo.21487043`](https://doi.org/10.5281/zenodo.21487043). The concrete v1.0.0 record DOI will be added to
-`CITATION.cff` as a version-specific identifier after Zenodo creates the stable deposit during issue #97.
-
-## Testing
-
-Run `just build`; it selects `scripts/build.sh` on Unix or `scripts\build.bat`
-on Windows, builds the test target, and executes all 131 CTest entries: 106
-doctest scenarios, 23 executable integration tests covering normal CLI use and
-invalid-boundary rejection, one compiled C++ API example, and one
-arithmetic-backend correctness test
-labeled `scientific`. The parallel-enabled AddressSanitizer and `parallel`
-configurations register 106 ordinary doctest scenarios, one launcher
-containing five scenarios labeled `unit`, `parallel`, and `configuration`, the
-same 23 integration tests, the C++ API example, and the arithmetic test, for 132
-CTest entries.
-Every process-level test is labeled `integration`, and invalid-input tests also
-carry the `cli-boundary` subcategory. Run `just ci` for the complete local
-validation gate.
-
-`just check` also runs the repository-owned Semgrep policy and its annotated
-fixtures. Use `just semgrep-test` while changing the rules and `just semgrep` to
-scan the real source tree for false positives.
-
-The doctest executable can also be run directly:
+The smallest pkgx-assisted host setup is:
-```bash
-./out/build/reference/tests/CDT_unit_tests
-```
+- [Xcode Command Line Tools] on macOS, or a [C++23] compiler and base build environment on Linux
+- [pkgx]
+- [Just], used by the recipes and `scripts/pkgx-build.sh` to resolve the repository's tool-version pins
+- [Python] 3.14 for native dependency bootstrap, and [uv] when checking or running the Python support scripts
-To rerun the complete suite without rebuilding:
+The pkgx build launcher supplies its required tools ephemerally, including [Git], [Bash], [CMake], [Ninja], Python, M4,
+Autoconf, Autoconf Archive, Automake, GNU Libtool, Texinfo, and pkg-config. If pkgx is not installed, provide these
+tools conventionally through a package manager such as [Homebrew] or [apt]. The build does not require a pre-existing
+personal vcpkg checkout, a fork, a submodule, Docker, or a hosted development environment.
-```bash
-ctest --preset reference-smoke
-```
+On Windows, use an x64 [Developer Command Prompt or Developer PowerShell] for [Visual Studio] 2022 17.4 or newer, with
+[MSVC] 19.34 or newer available. Install [Git for Windows] and expose [Git Bash] on `PATH`, because the Just recipes use
+Bash. Native builds also require Just 1.58.0 or newer, Python 3.14 with `python.exe` on `PATH`, CMake 4.4.0 or newer,
+and Ninja. The tested Windows cell uses Python 3.14.6, CMake 4.4.1, and Ninja 1.13.0. Run `just build` from the
+repository root, or use `scripts\build.bat reference` directly; both bootstrap the repository-pinned vcpkg checkout.
-To run a specific test category, use:
+Contributor validation, Debug and parallel builds, documentation generation, compiler caching, IDE setup, vcpkg
+maintenance, coverage, static analysis, and sanitizer workflows are documented in
+[CONTRIBUTING.md](https://github.com/acgetchell/CDT-plusplus/blob/main/.github/CONTRIBUTING.md).
-```bash
-ctest --preset reference-smoke -L unit
-ctest --preset reference-smoke -L integration
-```
+## C++ API
-In addition to the command line output, you can see detailed results in the
-`out/build/reference/Testing` directory generated by CTest.
-
-### Coverage
+The supported C++ namespace and per-header contract are recorded in the
+[C++ API boundary](docs/api-boundary.md). CDT++ publishes a C++23 source boundary, not a stable binary ABI or
+package-registry distribution.
-Coverage reporting is supported on Linux with GNU GCC and its matching gcov,
-CMake, Ninja, LCOV 2.5 or newer, and `genhtml`. Distribution packages may
-provide an older LCOV that cannot parse coverage from current GCC releases; use
-the [upstream LCOV release](https://github.com/linux-test-project/lcov/releases/tag/v2.5)
-when necessary. Generate the same reports used by Codecov with:
+The compiled [C++ API quickstart](docs/cpp-api-quickstart.md) demonstrates
+validated construction, ten reported Metropolis proposals with separate
+candidate-success and acceptance results, aggregate accounting, and a verified
+persistence round trip. Build and run it with:
```bash
-CXX=g++ GCOV=gcov just coverage
+cmake --preset reference
+cmake --build --preset reference --target CDT_cpp_api_quickstart
+./out/build/reference/examples/CDT_cpp_api_quickstart /tmp/cdt-quickstart.off
```
-The command uses an isolated `build/coverage` CMake tree and writes the filtered
-LCOV tracefile to `build/coverage.info` and the browsable report to
-`build/coverage-html/index.html`. Only project-owned `include/` and `src/`
-paths are retained, consistently excluding tests, generated files, system
-headers, and vcpkg dependencies. Reports include line and branch coverage;
-function coverage is disabled because GCC can emit inconsistent function and
-line records for generated lambda bodies. The raw LCOV capture temporarily
-retains function records because LCOV 2.5 requires them while filtering GCC 16
-data; the project-only extraction removes them from both published reports. If
-coverage collection fails, verify that `g++ -dumpfullversion -dumpversion` and
-`gcov --version` report the same major version.
-
-GCC 16 also emits three known line-hit/branch-unhit records for templated
-assignments in `Utilities.hpp`. The coverage recipe keeps LCOV's warnings
-visible and requires that exact count during extraction and report generation;
-new or removed inconsistencies therefore fail the command for review.
-
-The Codecov workflow runs this recipe for pull requests and `main`, uploads only `build/coverage.info` with OIDC, and
-preserves both reports as a 14-day GitHub Actions artifact for local
-diagnosis. If report generation fails, the workflow also preserves the gcov
-inputs and CTest diagnostics for seven days. It does not rely on Codecov's
-automatic gcov discovery. Codecov retains the LCOV branch detail but counts an
-executed line with an uncovered branch as a line hit, keeping its project
-percentage comparable to LCOV's line rate; use the LCOV artifact for the
-separate branch-coverage rate.
-
-### Static Analysis
-
-Python 3.14 is selected by [`.python-version`](https://github.com/acgetchell/CDT-plusplus/blob/main/.python-version), uv locks the environment in `uv.lock`, Ruff owns
-Python formatting and linting, and ty owns static type checking. Run `just python-sync` once and then use
-`just python-check` or `just python-fix`; both commands are also part of the repository-wide validation recipes.
-
-This project follows the [CppCore Guidelines][guidelines] as enforced by [ClangTidy]. The repository pins LLVM 22;
-run Clang-Tidy through its Just recipe:
-
-```bash
-just clang-tidy
-```
+CGAL handles and facet or edge descriptors borrow from the exact triangulation that produced them. Do not use them
+with a copied triangulation or after an invalidating topology mutation. `delaunay_snapshot()` instead returns an
+owning, detached triangulation suitable for persistence or transfer across an ownership boundary. See the
+[multithreaded CGAL contract](docs/multithreading.md) for the full lifetime and synchronization policy.
-(Or use your favorite linter plugin for your editor/IDE.)
+## Release scope and limitations
-### Sanitizers
+The v1.0.0 release supports one scientific model: spherical 2+1-dimensional CDT represented by three-dimensional
+foliated triangulations. It includes the complete audited `(2,3)`, `(3,2)`, `(2,6)`, `(6,2)`, and `(4,4)` move set,
+the Regge action, Metropolis-Hastings evolution, the headless `cdt` and `initialize` programs, deterministic reference
+fixtures, an offline C++/Rust comparison harness, and an opt-in macOS archival viewer.
-[AddressSanitizer] + [UndefinedBehaviorSanitizer], [LeakSanitizer], [MemorySanitizer], and [ThreadSanitizer] share the
-repository-owned Linux driver and CMake presets. Run one locally with `just sanitize asan`, `just sanitize lsan`,
-`just sanitize msan`, or `just sanitize tsan`; the GitHub Actions workflows invoke the same commands. MemorySanitizer
-remains experimental because third-party dependencies are not instrumented. AddressSanitizer enables the optional
-CGAL/TBB path and its parallel contract; ThreadSanitizer exercises the default sequential configuration.
+The release boundary is intentionally narrow:
-## Offline Comparison
+- Toroidal slices, dimensions other than three, and 3+1D/4D simulations are unsupported. The retained periodic and
+ toroidal headers are historical prototypes under `cdt::experimental`, not public v1.0.0 APIs.
+- Requested simplex counts drive a monotone spherical population heuristic; randomized post-repair counts are not an
+ exact topology oracle or a calibrated phase-distribution result.
+- A seed replays PCG inputs. Fresh cospherical CGAL construction can still choose another valid tetrahedralization,
+ so exact fresh topology and cross-toolchain trajectory identity are not promised.
+- Resumable checkpoints continue the identical Markov chain from a completed pass boundary only when loaded by the
+ recorded CDT++ source revision and toolchain. They are restart artifacts, not portable interchange files.
+- Optional oneTBB parallelism is limited to eligible CGAL Delaunay insertion and removal. Pachner moves,
+ Metropolis-Hastings, persistence, and access to one manifold remain sequential and externally serialized.
+- CDT++ publishes a C++23 source boundary, not a stable binary ABI or package-registry distribution.
-The [`cdt-compare`](docs/comparison-harness.md) command launches explicit CDT++ and `causal-triangulations`
-commands without a shell. Both receive copied versions of the #94 protocol, result schema, and reference manifest.
-The harness retains exact stdout, stderr, exit status, executable digest, command, working directory, process time,
-and host provenance. It first anchors the live C++ output to #94's committed canonical result, then compares exact
-fields and named tolerance-based fields with Rust. Any live C++ transition observations are independently anchored
-to the committed #94 protocol before becoming the Rust reference. It classifies
-implementation-specific and unsupported fields without interpreting either implementation as ground truth.
+The detailed evidence and failure boundaries are in the [CGAL integration](docs/cgal-integration.md),
+[ergodic-move](docs/ergodic-moves.md), [Metropolis-Hastings](docs/metropolis-hastings.md),
+[reproducibility and persistence](docs/reproducibility.md), and [multithreading](docs/multithreading.md) contracts.
-After building CDT++ and a compatible Rust fixture producer, run one bounded comparison and retain it locally:
+## Reproducibility
-```bash
-just comparison-run /absolute/path/to/causal-triangulations-fixture out/comparisons/run-1
-```
+With output enabled, every generated `.off` triangulation is accompanied by a `.off.meta` provenance manifest
+containing the effective seed, configuration, version/toolchain identity, transition-trace fingerprint, and payload
+checksum. A run started with `--input` also records the source artifact's seed, initialization stream, placement
+fingerprint, and topology fingerprint. Resumable checkpoint manifests also contain the complete transition PCG state
+and cumulative move accounting. `cdt --resume` validates that state and the recorded producer contract before
+continuing. The scientific test suite compares an uninterrupted run with the same run split across checkpoint and
+resume, including the ordered transition trace, all move counters, and final canonical topology. Same-seed
+generation replays the random inputs, while exact transition replay requires an identical starting manifold; CDT++
+does not alter its spherical construction to force CGAL to reproduce one of several valid cospherical
+tetrahedralizations.
-Reproduce `summary.json` entirely from the stored raw artifacts, without running either executable:
+The versioned, language-neutral fixtures, canonical C++ results, run manifests, raw outputs, and Rust consumption
+rules are published in the [`reference/`](reference/README.md) package. That package owns the detailed
+cross-implementation comparison and reference-fixture contract.
-```bash
-just comparison-analyze out/comparisons/run-1
-```
+## Documentation
-The bundle under `out/comparisons/run-1` is canonical and is published atomically only after analysis and manifest
-creation finish. Preserve its `inputs/`, `raw/`, `manifest.json`, and `summary.json` together. Python validates the
-complete artifact inventory and its digests, validates schemas, constructs commands, classifies comparisons, and
-renders a small text table; it does not implement topology, action, move-legality, or acceptance rules. See the
-[comparison-harness contract](docs/comparison-harness.md) for producer configuration, placeholders, artifact layout,
-failure records, and the C++ reference/Rust result boundary.
+Online documentation is at .
-The retained `cdt-optimize-initialize` command is also entirely local and dependency-free. It writes one directory
-per parameter pair under `out/experiments/initialize`, including configuration JSON, raw stdout, a tab-separated
-volume profile, metrics, artifact digests, and source/executable provenance. Each invocation requires a nonexistent
-output path. Seed `92` is the default; use `--seed` and `--output-directory` for another replayable record. Fresh CGAL
-triangulations remain subject to the [reproducibility contract](docs/reproducibility.md).
+The compiled [C++ API quickstart](docs/cpp-api-quickstart.md) is the canonical end-to-end public API example and is
+embedded verbatim in the generated site.
+
+- The complete supported public source surface is recorded in the [C++ API boundary](docs/api-boundary.md).
+- The scientific transition, proposal-ratio, geometry-delta, counter, and precision contracts are recorded in
+ [`docs/metropolis-hastings.md`](docs/metropolis-hastings.md).
+- The literature-backed contracts, exact deltas, inverse relationships, and failure-atomicity rules for the complete
+ 2+1D move set are recorded in [`docs/ergodic-moves.md`](docs/ergodic-moves.md).
+- Seed replay, PCG stream ownership, checkpoint metadata, and the parallel stream policy are recorded in
+ [`docs/reproducibility.md`](docs/reproducibility.md).
+- The exact CGAL version, kernel, triangulation data structure, metadata, lifetime, TBB, benchmark, and upgrade
+ policies are recorded in [`docs/cgal-integration.md`](docs/cgal-integration.md).
+- The opt-in operations, ownership and synchronization rules, replayable stress inputs, sanitizer boundary, and
+ matched scaling protocol are recorded in [`docs/multithreading.md`](docs/multithreading.md).
+- The initial-state interchange, cross-language comparison, local comparison harness, schemas, and raw archival
+ records are documented in [`reference/README.md`](reference/README.md).
+- The reproducible image, tracked fixture, and macOS renderer boundary are documented in
+ [`docs/viewer.md`](docs/viewer.md).
+- The repository-wide scientific bibliography is [`REFERENCES.md`](REFERENCES.md).
+
+Documentation generation and validation are documented in
+[CONTRIBUTING.md](https://github.com/acgetchell/CDT-plusplus/blob/main/.github/CONTRIBUTING.md).
## Visualization
@@ -672,6 +390,12 @@ noninteractive smoke test, exact canonical-image policy, and inventory of histor
[viewer and visual-artifact contract](docs/viewer.md). The default build remains headless and does not install Qt or
Eigen.
+## Citing CDT++
+
+If you use CDT++ in your work, please cite it using
+[`CITATION.cff`](https://github.com/acgetchell/CDT-plusplus/blob/main/CITATION.cff). The papers and software on which
+CDT++ is based are collected in [`REFERENCES.md`](REFERENCES.md).
+
## Security and support
CDT++ v1.0.0 is an archival scientific reference, not an actively maintained product. The reporting boundary for
@@ -683,44 +407,49 @@ archive-specific vulnerabilities and issues that also affect the active successo
Active development has moved to
[causal-triangulations](https://github.com/acgetchell/causal-triangulations). Before CDT++ is archived, only
release-blocking corrections within the maintenance-only stabilization scope are accepted. After archival, GitHub
-will make this repository read-only. See [CONTRIBUTING.md] for the correction workflow and
-[CODE_OF_CONDUCT.md] for the preserved participation policy.
+will make this repository read-only. See
+[CONTRIBUTING.md](https://github.com/acgetchell/CDT-plusplus/blob/main/.github/CONTRIBUTING.md) for the correction
+scope, environment setup, developer commands, test and documentation validation, dependency maintenance, and pull
+request requirements. The preserved participation policy is in
+[CODE_OF_CONDUCT.md](https://github.com/acgetchell/CDT-plusplus/blob/main/.github/CODE_OF_CONDUCT.md).
-## Issues
+## License
-The [CDT++ issue tracker](https://github.com/acgetchell/CDT-plusplus/issues) remains available during the
-maintenance-only stabilization window. Issue #155 will close or disposition the remaining trackers and archive this
-repository after the owner determines that no blockers remain. New development, support, and scientific work belong
-in the [causal-triangulations issue tracker](https://github.com/acgetchell/causal-triangulations/issues).
+CDT++ is distributed under the
+[BSD 3-Clause License](https://github.com/acgetchell/CDT-plusplus/blob/main/LICENSE.md).
[CDT]: REFERENCES.md#cdt-framework-2001
[CGAL]: REFERENCES.md#cgal-triangulations
-[CMake]: https://cmake.org
-[doctest]: https://github.com/doctest/doctest
-[guidelines]: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
[Doxygen]: https://www.doxygen.nl
[Homebrew]: https://brew.sh
+[C++23]: https://en.cppreference.com/w/cpp/23
+[GCC]: https://gcc.gnu.org
+[Clang]: https://clang.llvm.org
+[Xcode]: https://developer.apple.com/xcode/
+[Xcode Command Line Tools]: https://developer.apple.com/xcode/resources/
+[pkgx]: https://pkgx.sh
+[Just]: https://just.systems
+[Python]: https://www.python.org/downloads/
+[uv]: https://docs.astral.sh/uv/
+[Git]: https://git-scm.com
+[Bash]: https://www.gnu.org/software/bash/
+[CMake]: https://cmake.org
[Ninja]: https://ninja-build.org
+[apt]: https://ubuntu.com/server/docs/package-management
+[Developer Command Prompt or Developer PowerShell]: https://learn.microsoft.com/cpp/build/building-on-the-command-line
+[Visual Studio]: https://visualstudio.microsoft.com/vs/
+[MSVC]: https://learn.microsoft.com/cpp/overview/visual-cpp-in-visual-studio
+[Git Bash]: https://gitforwindows.org
+[Git for Windows]: https://gitforwindows.org
+[Slurm]: https://slurm.schedmd.com
[program_options]: https://www.boost.org/doc/libs/1_91_0/doc/html/program_options.html
-[Mathjax]: https://www.mathjax.org
-[GraphViz]: https://www.graphviz.org
[MPFR]: https://www.mpfr.org
[GMP]: https://gmplib.org
[TBB]: https://uxlfoundation.github.io/oneTBB/
[Boost]: https://www.boost.org
-[ClangTidy]: https://clang.llvm.org/extra/clang-tidy/
-[BDD]: https://en.wikipedia.org/wiki/Behavior-driven_development
-[TDD]: https://en.wikipedia.org/wiki/Test-driven_development
[vcpkg]: https://github.com/Microsoft/vcpkg
[PCG]: REFERENCES.md#pcg-random-number-generators
[TestU01]: https://doi.org/10.1145/1268776.1268777
-[CONTRIBUTING.md]: https://github.com/acgetchell/CDT-plusplus/blob/main/.github/CONTRIBUTING.md
-[CODE_OF_CONDUCT.md]: https://github.com/acgetchell/CDT-plusplus/blob/main/.github/CODE_OF_CONDUCT.md
[{fmt}]: https://github.com/fmtlib/fmt
-[AddressSanitizer]: https://github.com/google/sanitizers/wiki/AddressSanitizer
-[LeakSanitizer]: https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer
-[ThreadSanitizer]: https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual
-[MemorySanitizer]: https://github.com/google/sanitizers/wiki/MemorySanitizer
-[UndefinedBehaviorSanitizer]: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
[spdlog]: https://github.com/gabime/spdlog
[Qt]: https://www.qt.io
diff --git a/cmake/RunCheckpointResumeTest.cmake b/cmake/RunCheckpointResumeTest.cmake
new file mode 100644
index 000000000..3d98a6894
--- /dev/null
+++ b/cmake/RunCheckpointResumeTest.cmake
@@ -0,0 +1,348 @@
+if(NOT DEFINED INITIALIZE_EXECUTABLE OR NOT EXISTS "${INITIALIZE_EXECUTABLE}")
+ message(FATAL_ERROR "INITIALIZE_EXECUTABLE must name a built initialize program")
+endif()
+if(NOT DEFINED CDT_EXECUTABLE OR NOT EXISTS "${CDT_EXECUTABLE}")
+ message(FATAL_ERROR "CDT_EXECUTABLE must name a built cdt program")
+endif()
+if(NOT DEFINED TEST_ROOT OR TEST_ROOT STREQUAL "")
+ message(FATAL_ERROR "TEST_ROOT must name the owning test root")
+endif()
+if(NOT DEFINED TEST_DIRECTORY OR TEST_DIRECTORY STREQUAL "")
+ message(FATAL_ERROR "TEST_DIRECTORY must name a dedicated test directory")
+endif()
+
+set(normalized_test_root "${TEST_ROOT}")
+set(normalized_test_directory "${TEST_DIRECTORY}")
+cmake_path(ABSOLUTE_PATH normalized_test_root NORMALIZE)
+cmake_path(ABSOLUTE_PATH normalized_test_directory NORMALIZE)
+cmake_path(
+ IS_PREFIX normalized_test_root "${normalized_test_directory}" NORMALIZE
+ test_directory_is_owned)
+if(NOT test_directory_is_owned OR normalized_test_directory STREQUAL normalized_test_root)
+ message(FATAL_ERROR "TEST_DIRECTORY must be a strict descendant of TEST_ROOT")
+endif()
+
+set(initialize_directory "${normalized_test_directory}/initialize")
+set(uninterrupted_directory "${normalized_test_directory}/uninterrupted")
+set(resumed_directory "${normalized_test_directory}/resumed")
+set(extended_directory "${normalized_test_directory}/extended")
+set(rejected_directory "${normalized_test_directory}/rejected")
+set(completed_directory "${normalized_test_directory}/completed")
+set(corrupt_directory "${normalized_test_directory}/corrupt")
+set(thread_limit_directory "${normalized_test_directory}/thread-limit")
+file(REMOVE_RECURSE "${normalized_test_directory}")
+file(MAKE_DIRECTORY
+ "${initialize_directory}"
+ "${uninterrupted_directory}"
+ "${resumed_directory}"
+ "${extended_directory}"
+ "${rejected_directory}"
+ "${completed_directory}"
+ "${corrupt_directory}"
+ "${thread_limit_directory}")
+
+execute_process(
+ COMMAND "${INITIALIZE_EXECUTABLE}" -s -n64 -t3 -o --seed 92
+ WORKING_DIRECTORY "${initialize_directory}"
+ RESULT_VARIABLE initialize_result
+ OUTPUT_VARIABLE initialize_output
+ ERROR_VARIABLE initialize_error)
+if(NOT initialize_result EQUAL 0)
+ message(FATAL_ERROR "initialize failed:\n${initialize_output}\n${initialize_error}")
+endif()
+file(GLOB initial_payloads LIST_DIRECTORIES false "${initialize_directory}/*.off")
+list(LENGTH initial_payloads initial_payload_count)
+if(NOT initial_payload_count EQUAL 1)
+ message(FATAL_ERROR "Expected one initialized payload, found ${initial_payload_count}")
+endif()
+list(GET initial_payloads 0 initial_payload)
+
+execute_process(
+ COMMAND
+ "${CDT_EXECUTABLE}" --input "${initial_payload}" -a0.6 -k1.1 -l0.1
+ -p4 -c2 --seed 93
+ WORKING_DIRECTORY "${uninterrupted_directory}"
+ RESULT_VARIABLE uninterrupted_result
+ OUTPUT_VARIABLE uninterrupted_output
+ ERROR_VARIABLE uninterrupted_error)
+if(NOT uninterrupted_result EQUAL 0)
+ message(
+ FATAL_ERROR
+ "Uninterrupted run failed:\n${uninterrupted_output}\n${uninterrupted_error}")
+endif()
+
+file(GLOB pass_two_payloads LIST_DIRECTORIES false "${uninterrupted_directory}/*-pass-2.off")
+file(GLOB pass_four_payloads LIST_DIRECTORIES false "${uninterrupted_directory}/*-pass-4.off")
+list(LENGTH pass_two_payloads pass_two_count)
+list(LENGTH pass_four_payloads pass_four_count)
+if(NOT pass_two_count EQUAL 1 OR NOT pass_four_count EQUAL 1)
+ message(
+ FATAL_ERROR
+ "Expected pass-2 and pass-4 checkpoints; found ${pass_two_count} and ${pass_four_count}")
+endif()
+list(GET pass_two_payloads 0 pass_two_checkpoint)
+list(GET pass_four_payloads 0 pass_four_checkpoint)
+file(READ "${pass_two_checkpoint}.meta" pass_two_metadata)
+foreach(
+ required
+ "artifact=checkpoint"
+ "resume_supported=true"
+ "configured_passes=4"
+ "completed_passes=2"
+ "random.transition_state="
+ "moves.proposed="
+ "moves.accepted="
+ "moves.rejected="
+ "moves.attempted="
+ "moves.succeeded="
+ "moves.failed=")
+ if(NOT pass_two_metadata MATCHES "${required}")
+ message(FATAL_ERROR "Checkpoint metadata is missing '${required}':\n${pass_two_metadata}")
+ endif()
+endforeach()
+
+execute_process(
+ COMMAND "${CDT_EXECUTABLE}" --resume "${pass_two_checkpoint}"
+ WORKING_DIRECTORY "${resumed_directory}"
+ RESULT_VARIABLE resumed_result
+ OUTPUT_VARIABLE resumed_output
+ ERROR_VARIABLE resumed_error)
+if(NOT resumed_result EQUAL 0)
+ message(FATAL_ERROR "Resumed run failed:\n${resumed_output}\n${resumed_error}")
+endif()
+foreach(
+ expected
+ "Completed checkpoint passes: 2"
+ "Target total passes: 4"
+ "Number of passes to execute: 2"
+ "=== Pass 3 ==="
+ "=== Pass 4 ==="
+ "Writing checkpoint for pass 4")
+ if(NOT resumed_output MATCHES "${expected}")
+ message(FATAL_ERROR "Resume output is missing '${expected}':\n${resumed_output}")
+ endif()
+endforeach()
+file(GLOB resumed_pass_two_payloads LIST_DIRECTORIES false "${resumed_directory}/*-pass-2.off")
+file(GLOB resumed_pass_four_payloads LIST_DIRECTORIES false "${resumed_directory}/*-pass-4.off")
+list(LENGTH resumed_pass_two_payloads resumed_pass_two_count)
+list(LENGTH resumed_pass_four_payloads resumed_pass_four_count)
+if(NOT resumed_pass_two_count EQUAL 0 OR NOT resumed_pass_four_count EQUAL 1)
+ message(
+ FATAL_ERROR
+ "Resume did not preserve global checkpoint numbering: pass-2=${resumed_pass_two_count}, pass-4=${resumed_pass_four_count}")
+endif()
+
+function(find_final_manifest directory output_variable)
+ file(GLOB candidate_manifests LIST_DIRECTORIES false "${directory}/*.off.meta")
+ set(final_manifest "")
+ foreach(candidate IN LISTS candidate_manifests)
+ file(READ "${candidate}" candidate_metadata)
+ if(candidate_metadata MATCHES "(^|[\r\n])artifact=final-triangulation([\r\n]|$)")
+ if(NOT final_manifest STREQUAL "")
+ message(FATAL_ERROR "Found more than one final manifest in ${directory}")
+ endif()
+ set(final_manifest "${candidate}")
+ endif()
+ endforeach()
+ if(final_manifest STREQUAL "")
+ message(FATAL_ERROR "Did not find a final manifest in ${directory}")
+ endif()
+ set("${output_variable}" "${final_manifest}" PARENT_SCOPE)
+endfunction()
+
+find_final_manifest("${uninterrupted_directory}" uninterrupted_final_manifest)
+find_final_manifest("${resumed_directory}" resumed_final_manifest)
+file(READ "${uninterrupted_final_manifest}" uninterrupted_final_metadata)
+file(READ "${resumed_final_manifest}" resumed_final_metadata)
+
+function(compare_metadata_field field)
+ string(REPLACE "." "[.]" field_pattern "${field}")
+ if(NOT uninterrupted_final_metadata MATCHES "(^|[\r\n])${field_pattern}=([^\r\n]+)")
+ message(FATAL_ERROR "Uninterrupted metadata is missing ${field}")
+ endif()
+ set(uninterrupted_value "${CMAKE_MATCH_2}")
+ if(NOT resumed_final_metadata MATCHES "(^|[\r\n])${field_pattern}=([^\r\n]+)")
+ message(FATAL_ERROR "Resumed metadata is missing ${field}")
+ endif()
+ set(resumed_value "${CMAKE_MATCH_2}")
+ if(NOT uninterrupted_value STREQUAL resumed_value)
+ message(
+ FATAL_ERROR
+ "Scientific resume mismatch for ${field}: uninterrupted='${uninterrupted_value}', resumed='${resumed_value}'")
+ endif()
+endfunction()
+
+foreach(
+ field
+ "topology.fnv1a64"
+ "placement.fnv1a64"
+ "transition_trace.fnv1a64"
+ "transition_trace.count"
+ "moves.proposed"
+ "moves.accepted"
+ "moves.rejected"
+ "moves.attempted"
+ "moves.succeeded"
+ "moves.failed"
+ "configured_passes"
+ "completed_passes"
+ "random.seed"
+ "random.transition_stream"
+ "alpha"
+ "k"
+ "lambda")
+ compare_metadata_field("${field}")
+endforeach()
+
+execute_process(
+ COMMAND "${CDT_EXECUTABLE}" --resume "${pass_four_checkpoint}"
+ WORKING_DIRECTORY "${completed_directory}"
+ RESULT_VARIABLE completed_result
+ OUTPUT_VARIABLE completed_output
+ ERROR_VARIABLE completed_error)
+if(NOT completed_result EQUAL 0
+ OR NOT completed_output MATCHES "no transitions remain")
+ message(
+ FATAL_ERROR
+ "A completed checkpoint did not finalize cleanly:\n${completed_output}\n${completed_error}")
+endif()
+find_final_manifest("${completed_directory}" completed_final_manifest)
+
+execute_process(
+ COMMAND "${CDT_EXECUTABLE}" --resume "${pass_two_checkpoint}" --passes 6
+ WORKING_DIRECTORY "${extended_directory}"
+ RESULT_VARIABLE extended_result
+ OUTPUT_VARIABLE extended_output
+ ERROR_VARIABLE extended_error)
+if(NOT extended_result EQUAL 0)
+ message(
+ FATAL_ERROR
+ "Extended resume failed with result '${extended_result}':\n${extended_output}\n${extended_error}")
+endif()
+foreach(
+ expected
+ "Completed checkpoint passes: 2"
+ "Target total passes: 6"
+ "Number of passes to execute: 4"
+ "=== Pass 3 ==="
+ "=== Pass 4 ==="
+ "=== Pass 5 ==="
+ "=== Pass 6 ==="
+ "Writing checkpoint for pass 4"
+ "Writing checkpoint for pass 6")
+ if(NOT extended_output MATCHES "${expected}")
+ message(
+ FATAL_ERROR
+ "Extended resume output is missing '${expected}':\n${extended_output}")
+ endif()
+endforeach()
+file(GLOB extended_pass_two_payloads LIST_DIRECTORIES false "${extended_directory}/*-pass-2.off")
+file(GLOB extended_pass_four_payloads LIST_DIRECTORIES false "${extended_directory}/*-pass-4.off")
+file(GLOB extended_pass_six_payloads LIST_DIRECTORIES false "${extended_directory}/*-pass-6.off")
+list(LENGTH extended_pass_two_payloads extended_pass_two_count)
+list(LENGTH extended_pass_four_payloads extended_pass_four_count)
+list(LENGTH extended_pass_six_payloads extended_pass_six_count)
+if(NOT extended_pass_two_count EQUAL 0
+ OR NOT extended_pass_four_count EQUAL 1
+ OR NOT extended_pass_six_count EQUAL 1)
+ message(
+ FATAL_ERROR
+ "Extended resume did not preserve global checkpoint numbering: pass-2=${extended_pass_two_count}, pass-4=${extended_pass_four_count}, pass-6=${extended_pass_six_count}")
+endif()
+find_final_manifest("${extended_directory}" extended_final_manifest)
+file(READ "${extended_final_manifest}" extended_final_metadata)
+foreach(required "configured_passes=6" "completed_passes=6")
+ if(NOT extended_final_metadata MATCHES "(^|[\r\n])${required}([\r\n]|$)")
+ message(
+ FATAL_ERROR
+ "Extended final metadata is missing '${required}':\n${extended_final_metadata}")
+ endif()
+endforeach()
+
+execute_process(
+ COMMAND "${CDT_EXECUTABLE}" --resume "${pass_two_checkpoint}" --passes 1
+ WORKING_DIRECTORY "${rejected_directory}"
+ RESULT_VARIABLE rejected_result
+ OUTPUT_VARIABLE rejected_output
+ ERROR_VARIABLE rejected_error)
+set(rejected_log "${rejected_output}\n${rejected_error}")
+if(rejected_result EQUAL 0
+ OR NOT rejected_log MATCHES
+ "Resume target passes must be at least the completed checkpoint pass")
+ message(
+ FATAL_ERROR
+ "cdt accepted a resume target below the completed pass:\n${rejected_log}")
+endif()
+file(GLOB rejected_artifacts LIST_DIRECTORIES false "${rejected_directory}/*")
+if(rejected_artifacts)
+ list(JOIN rejected_artifacts "\n " rejected_artifact_list)
+ message(
+ FATAL_ERROR
+ "Rejected resume published output artifacts:\n ${rejected_artifact_list}")
+endif()
+
+set(corrupt_checkpoint "${corrupt_directory}/checkpoint.off")
+file(COPY_FILE "${pass_two_checkpoint}" "${corrupt_checkpoint}" ONLY_IF_DIFFERENT)
+file(COPY_FILE "${pass_two_checkpoint}.meta" "${corrupt_checkpoint}.meta" ONLY_IF_DIFFERENT)
+file(READ "${corrupt_checkpoint}.meta" corrupt_metadata)
+string(
+ REGEX REPLACE
+ "random[.]transition_state=[^\r\n]+"
+ "random.transition_state=bad"
+ corrupt_metadata
+ "${corrupt_metadata}")
+file(WRITE "${corrupt_checkpoint}.meta" "${corrupt_metadata}")
+execute_process(
+ COMMAND "${CDT_EXECUTABLE}" --resume "${corrupt_checkpoint}"
+ WORKING_DIRECTORY "${corrupt_directory}"
+ RESULT_VARIABLE corrupt_result
+ OUTPUT_VARIABLE corrupt_output
+ ERROR_VARIABLE corrupt_error)
+set(corrupt_log "${corrupt_output}\n${corrupt_error}")
+if(corrupt_result EQUAL 0 OR NOT corrupt_log MATCHES "invalid PCG state")
+ message(FATAL_ERROR "cdt accepted corrupt resume state:\n${corrupt_log}")
+endif()
+
+execute_process(
+ COMMAND "${CDT_EXECUTABLE}" --resume "${pass_two_checkpoint}" --seed 94
+ WORKING_DIRECTORY "${resumed_directory}"
+ RESULT_VARIABLE conflict_result
+ OUTPUT_VARIABLE conflict_output
+ ERROR_VARIABLE conflict_error)
+set(conflict_log "${conflict_output}\n${conflict_error}")
+if(conflict_result EQUAL 0 OR NOT conflict_log MATCHES "--resume restores seed")
+ message(FATAL_ERROR "cdt accepted a conflicting resume seed:\n${conflict_log}")
+endif()
+
+set(thread_limit_checkpoint "${thread_limit_directory}/checkpoint.off")
+file(COPY_FILE "${pass_two_checkpoint}" "${thread_limit_checkpoint}" ONLY_IF_DIFFERENT)
+file(COPY_FILE "${pass_two_checkpoint}.meta" "${thread_limit_checkpoint}.meta"
+ ONLY_IF_DIFFERENT)
+file(READ "${thread_limit_checkpoint}.meta" thread_limit_metadata)
+string(
+ REGEX REPLACE
+ "parallel[.]max_threads=[^\r\n]+"
+ "parallel.max_threads=18446744073709551615"
+ thread_limit_metadata
+ "${thread_limit_metadata}")
+file(WRITE "${thread_limit_checkpoint}.meta" "${thread_limit_metadata}")
+execute_process(
+ COMMAND "${CDT_EXECUTABLE}" --resume "${thread_limit_checkpoint}"
+ WORKING_DIRECTORY "${thread_limit_directory}"
+ RESULT_VARIABLE thread_limit_result
+ OUTPUT_VARIABLE thread_limit_output
+ ERROR_VARIABLE thread_limit_error)
+set(thread_limit_log "${thread_limit_output}\n${thread_limit_error}")
+if(thread_limit_result EQUAL 0
+ OR NOT thread_limit_log MATCHES
+ "Saved thread count exceeds the supported range")
+ message(
+ FATAL_ERROR
+ "cdt accepted an unrepresentable saved thread count:\n${thread_limit_log}")
+endif()
+
+file(GLOB temporary_files LIST_DIRECTORIES false "${normalized_test_directory}/*/*.tmp")
+if(temporary_files)
+ list(JOIN temporary_files "\n " temporary_file_list)
+ message(FATAL_ERROR "Resume workflow left temporary files behind:\n ${temporary_file_list}")
+endif()
diff --git a/cmake/RunInitializeToCdtTest.cmake b/cmake/RunInitializeToCdtTest.cmake
new file mode 100644
index 000000000..8d66e1dd7
--- /dev/null
+++ b/cmake/RunInitializeToCdtTest.cmake
@@ -0,0 +1,261 @@
+if(NOT DEFINED INITIALIZE_EXECUTABLE OR NOT EXISTS "${INITIALIZE_EXECUTABLE}")
+ message(FATAL_ERROR "INITIALIZE_EXECUTABLE must name the built initialize program")
+endif()
+
+if(NOT DEFINED CDT_EXECUTABLE OR NOT EXISTS "${CDT_EXECUTABLE}")
+ message(FATAL_ERROR "CDT_EXECUTABLE must name the built cdt program")
+endif()
+
+if(NOT DEFINED TEST_ROOT OR TEST_ROOT STREQUAL "")
+ message(FATAL_ERROR "TEST_ROOT must name the owning test root")
+endif()
+
+if(NOT DEFINED TEST_DIRECTORY OR TEST_DIRECTORY STREQUAL "")
+ message(FATAL_ERROR "TEST_DIRECTORY must name a dedicated test directory")
+endif()
+
+set(normalized_test_root "${TEST_ROOT}")
+set(normalized_test_directory "${TEST_DIRECTORY}")
+cmake_path(ABSOLUTE_PATH normalized_test_root NORMALIZE)
+cmake_path(ABSOLUTE_PATH normalized_test_directory NORMALIZE)
+cmake_path(
+ IS_PREFIX normalized_test_root "${normalized_test_directory}" NORMALIZE
+ test_directory_is_owned)
+if(NOT test_directory_is_owned OR normalized_test_directory STREQUAL normalized_test_root)
+ message(FATAL_ERROR "TEST_DIRECTORY must be a strict descendant of TEST_ROOT")
+endif()
+
+set(initialize_directory "${normalized_test_directory}/initialize")
+set(cdt_directory "${normalized_test_directory}/cdt")
+set(checkpoint_directory "${normalized_test_directory}/checkpoint")
+set(unmanifested_directory "${normalized_test_directory}/unmanifested")
+file(REMOVE_RECURSE "${normalized_test_directory}")
+file(MAKE_DIRECTORY
+ "${initialize_directory}"
+ "${cdt_directory}"
+ "${checkpoint_directory}"
+ "${unmanifested_directory}")
+
+execute_process(
+ COMMAND "${INITIALIZE_EXECUTABLE}" -s -n64 -t3 -o --seed 92
+ WORKING_DIRECTORY "${initialize_directory}"
+ RESULT_VARIABLE initialize_result
+ OUTPUT_VARIABLE initialize_output
+ ERROR_VARIABLE initialize_error)
+if(NOT initialize_result EQUAL 0)
+ message(
+ FATAL_ERROR
+ "initialize failed:\n${initialize_output}\n${initialize_error}")
+endif()
+
+file(GLOB initial_payloads LIST_DIRECTORIES false "${initialize_directory}/*.off")
+file(GLOB initial_manifests LIST_DIRECTORIES false "${initialize_directory}/*.off.meta")
+list(LENGTH initial_payloads initial_payload_count)
+list(LENGTH initial_manifests initial_manifest_count)
+if(NOT initial_payload_count EQUAL 1 OR NOT initial_manifest_count EQUAL 1)
+ message(
+ FATAL_ERROR
+ "initialize must publish exactly one OFF/metadata pair; found ${initial_payload_count} payloads and ${initial_manifest_count} manifests")
+endif()
+list(GET initial_payloads 0 initial_payload)
+list(GET initial_manifests 0 initial_manifest)
+if(NOT initial_manifest STREQUAL "${initial_payload}.meta")
+ message(FATAL_ERROR "initialize metadata is not paired with its payload")
+endif()
+
+file(READ "${initial_manifest}" initial_metadata)
+if(NOT initial_metadata MATCHES "(^|[\r\n])artifact=initial-triangulation([\r\n]|$)")
+ message(FATAL_ERROR "initialize did not publish an initial-triangulation artifact")
+endif()
+if(NOT initial_metadata MATCHES "(^|[\r\n])topology[.]fnv1a64=([0-9a-f]+)([\r\n]|$)")
+ message(FATAL_ERROR "initialize metadata is missing its topology fingerprint")
+endif()
+set(initial_topology_fingerprint "${CMAKE_MATCH_2}")
+if(NOT initial_metadata MATCHES "(^|[\r\n])placement[.]fnv1a64=([0-9a-f]+)([\r\n]|$)")
+ message(FATAL_ERROR "initialize metadata is missing its placement fingerprint")
+endif()
+set(initial_placement_fingerprint "${CMAKE_MATCH_2}")
+
+execute_process(
+ COMMAND "${CDT_EXECUTABLE}" --input "${initial_payload}" -p1 --seed 93
+ --no-output
+ WORKING_DIRECTORY "${cdt_directory}"
+ RESULT_VARIABLE missing_action_result
+ OUTPUT_VARIABLE missing_action_output
+ ERROR_VARIABLE missing_action_error)
+set(missing_action_log "${missing_action_output}\n${missing_action_error}")
+if(missing_action_result EQUAL 0
+ OR NOT missing_action_log MATCHES "Alpha, K, and Lambda must be specified")
+ message(
+ FATAL_ERROR
+ "cdt accepted an initial artifact without action parameters:\n${missing_action_log}")
+endif()
+
+set(unmanifested_payload "${unmanifested_directory}/initial.off")
+file(COPY_FILE "${initial_payload}" "${unmanifested_payload}" ONLY_IF_DIFFERENT)
+execute_process(
+ COMMAND
+ "${CDT_EXECUTABLE}" --input "${unmanifested_payload}" -a0.6 -k1.1 -l0.1
+ -p1 --seed 93 --no-output
+ WORKING_DIRECTORY "${unmanifested_directory}"
+ RESULT_VARIABLE unmanifested_result
+ OUTPUT_VARIABLE unmanifested_output
+ ERROR_VARIABLE unmanifested_error)
+set(unmanifested_log "${unmanifested_output}\n${unmanifested_error}")
+if(unmanifested_result EQUAL 0
+ OR NOT unmanifested_log MATCHES "requires a persistence metadata sidecar")
+ message(
+ FATAL_ERROR
+ "cdt accepted an unmanifested initial payload:\n${unmanifested_output}\n${unmanifested_error}")
+endif()
+
+execute_process(
+ COMMAND
+ "${CDT_EXECUTABLE}" --input "${initial_payload}" -s -a0.6 -k1.1 -l0.1
+ -p1 --seed 93 --no-output
+ WORKING_DIRECTORY "${cdt_directory}"
+ RESULT_VARIABLE conflict_result
+ OUTPUT_VARIABLE conflict_output
+ ERROR_VARIABLE conflict_error)
+set(conflict_log "${conflict_output}\n${conflict_error}")
+if(conflict_result EQUAL 0
+ OR NOT conflict_log MATCHES "--input cannot be combined with")
+ message(
+ FATAL_ERROR
+ "cdt accepted conflicting input/construction options:\n${conflict_output}\n${conflict_error}")
+endif()
+
+execute_process(
+ COMMAND
+ "${CDT_EXECUTABLE}" --input "${initial_payload}" -a0.6 -k1.1 -l0.1
+ -p1 -c10 --seed 93
+ WORKING_DIRECTORY "${cdt_directory}"
+ RESULT_VARIABLE cdt_result
+ OUTPUT_VARIABLE cdt_output
+ ERROR_VARIABLE cdt_error)
+if(NOT cdt_result EQUAL 0)
+ message(FATAL_ERROR "cdt input run failed:\n${cdt_output}\n${cdt_error}")
+endif()
+if(NOT cdt_output MATCHES "Input initial triangulation:")
+ message(FATAL_ERROR "cdt did not report the loaded initial triangulation")
+endif()
+if(NOT cdt_output MATCHES "Input initialization seed: 92([\r\n]|$)")
+ message(FATAL_ERROR "cdt did not report the input initialization seed")
+endif()
+if(NOT cdt_output MATCHES "Effective random seed: 93([\r\n]|$)")
+ message(FATAL_ERROR "cdt did not report the new transition-run seed")
+endif()
+
+file(GLOB final_payloads LIST_DIRECTORIES false "${cdt_directory}/*.off")
+file(GLOB final_manifests LIST_DIRECTORIES false "${cdt_directory}/*.off.meta")
+list(LENGTH final_payloads final_payload_count)
+list(LENGTH final_manifests final_manifest_count)
+if(NOT final_payload_count EQUAL 1 OR NOT final_manifest_count EQUAL 1)
+ message(
+ FATAL_ERROR
+ "cdt must publish exactly one evolved OFF/metadata pair; found ${final_payload_count} payloads and ${final_manifest_count} manifests")
+endif()
+list(GET final_payloads 0 final_payload)
+list(GET final_manifests 0 final_manifest)
+if(NOT final_manifest STREQUAL "${final_payload}.meta")
+ message(FATAL_ERROR "cdt metadata is not paired with its payload")
+endif()
+
+file(READ "${final_manifest}" final_metadata)
+foreach(
+ required
+ "artifact=final-triangulation"
+ "random.seed=93"
+ "input.artifact=initial-triangulation"
+ "input.random.seed=92"
+ "input.random.initialization_stream=0"
+ "input.placement.fnv1a64=${initial_placement_fingerprint}"
+ "input.topology.fnv1a64=${initial_topology_fingerprint}"
+ "transition_trace.fnv1a64=")
+ if(NOT final_metadata MATCHES "${required}")
+ message(FATAL_ERROR "Evolved metadata is missing '${required}':\n${final_metadata}")
+ endif()
+endforeach()
+if(NOT final_metadata MATCHES "transition_trace[.]count=([1-9][0-9]*)")
+ message(FATAL_ERROR "The evolved artifact did not record any CDT transitions")
+endif()
+
+execute_process(
+ COMMAND
+ "${CDT_EXECUTABLE}" --input "${initial_payload}" -a0.6 -k1.1 -l0.1
+ -p1 -c1 --seed 94
+ WORKING_DIRECTORY "${checkpoint_directory}"
+ RESULT_VARIABLE checkpoint_result
+ OUTPUT_VARIABLE checkpoint_output
+ ERROR_VARIABLE checkpoint_error)
+if(NOT checkpoint_result EQUAL 0)
+ message(
+ FATAL_ERROR
+ "cdt checkpoint run failed:\n${checkpoint_output}\n${checkpoint_error}")
+endif()
+file(GLOB checkpoint_payloads LIST_DIRECTORIES false
+ "${checkpoint_directory}/*-pass-1.off")
+list(LENGTH checkpoint_payloads checkpoint_payload_count)
+if(NOT checkpoint_payload_count EQUAL 1)
+ message(
+ FATAL_ERROR
+ "cdt must publish exactly one pass-1 checkpoint; found ${checkpoint_payload_count}")
+endif()
+list(GET checkpoint_payloads 0 checkpoint_payload)
+
+execute_process(
+ COMMAND
+ "${CDT_EXECUTABLE}" --input "${checkpoint_payload}" -a0.6 -k1.1 -l0.1
+ -p1 --seed 95 --no-output
+ WORKING_DIRECTORY "${checkpoint_directory}"
+ RESULT_VARIABLE checkpoint_input_result
+ OUTPUT_VARIABLE checkpoint_input_output
+ ERROR_VARIABLE checkpoint_input_error)
+set(checkpoint_input_log
+ "${checkpoint_input_output}\n${checkpoint_input_error}")
+if(checkpoint_input_result EQUAL 0
+ OR NOT checkpoint_input_log MATCHES "CDT input must be an initial-triangulation artifact")
+ message(
+ FATAL_ERROR
+ "cdt accepted a checkpoint artifact as a new initial state:\n${checkpoint_input_output}\n${checkpoint_input_error}")
+endif()
+
+execute_process(
+ COMMAND
+ "${CDT_EXECUTABLE}" --input "${initial_payload}" --resume
+ "${checkpoint_payload}" -p1 --no-output
+ WORKING_DIRECTORY "${checkpoint_directory}"
+ RESULT_VARIABLE conflicting_mode_result
+ OUTPUT_VARIABLE conflicting_mode_output
+ ERROR_VARIABLE conflicting_mode_error)
+set(conflicting_mode_log
+ "${conflicting_mode_output}\n${conflicting_mode_error}")
+if(conflicting_mode_result EQUAL 0
+ OR NOT conflicting_mode_log MATCHES
+ "--input and --resume are mutually exclusive")
+ message(
+ FATAL_ERROR
+ "cdt accepted both input modes at once:\n${conflicting_mode_log}")
+endif()
+
+execute_process(
+ COMMAND
+ "${CDT_EXECUTABLE}" --input "${final_payload}" -a0.6 -k1.1 -l0.1 -p1
+ --seed 94 --no-output
+ WORKING_DIRECTORY "${cdt_directory}"
+ RESULT_VARIABLE final_input_result
+ OUTPUT_VARIABLE final_input_output
+ ERROR_VARIABLE final_input_error)
+set(final_input_log "${final_input_output}\n${final_input_error}")
+if(final_input_result EQUAL 0
+ OR NOT final_input_log MATCHES "CDT input must be an initial-triangulation artifact")
+ message(
+ FATAL_ERROR
+ "cdt accepted a final artifact as a new initial state:\n${final_input_output}\n${final_input_error}")
+endif()
+
+file(GLOB temporary_files LIST_DIRECTORIES false "${normalized_test_directory}/*/*.tmp")
+if(temporary_files)
+ list(JOIN temporary_files "\n " temporary_file_list)
+ message(FATAL_ERROR "The handoff left temporary files behind:\n ${temporary_file_list}")
+endif()
diff --git a/docs/api-boundary.md b/docs/api-boundary.md
index 360bdb89d..7d06e38af 100644
--- a/docs/api-boundary.md
+++ b/docs/api-boundary.md
@@ -51,6 +51,19 @@ Tests that name `detail` are deliberate white-box tests for mutation
failure-atomicity and malformed-handle rejection. Test access does not promote
those declarations into the supported API.
+The persistence surface distinguishes three read contracts.
+`read_file(path)` remains the general triangulation reader, including
+legacy payloads without manifests. `read_initial_triangulation(path)` is the
+new-run boundary: it requires a complete manifested
+`initial-triangulation`, verifies payload integrity and causal metadata, rejects
+checkpoint, final, and coincident-coordinate artifacts, and returns both the
+owning triangulation and its initialization provenance in
+`Initial_triangulation_artifact`. It does not resume a stochastic stream.
+`read_checkpoint(path)` is the exact-restart boundary: it accepts only a
+manifested checkpoint with complete PCG state, transition accounting, distinct
+vertex coordinates, and a matching producer toolchain, then returns the owning
+triangulation and continuation metadata in `Checkpoint_artifact`.
+
The supported-header compile contract intentionally excludes `experimental`.
The CGAL 6.2 audit confirmed that the periodic and d-dimensional prototypes are
not production dependencies and do not meet the supported compile contract.
diff --git a/docs/ergodic-moves.md b/docs/ergodic-moves.md
index 6c165f48a..292e73401 100644
--- a/docs/ergodic-moves.md
+++ b/docs/ergodic-moves.md
@@ -58,6 +58,11 @@ has different handles; every applicable-move implementation therefore captures
stable point values and re-resolves vertices, cells, or edges in the owning
candidate immediately before mutation.
+Before a checked edge flip, CDT++ also rebinds the CGAL edge descriptor to its
+canonical finite incident cell and orders its endpoints by point value. This
+makes `(3,2)`, `(6,2)`, and `(4,4)` execution independent of the arbitrary
+descriptor owner and orientation recreated by a persistence round trip.
+
None of these moves is required to preserve the Euclidean empty-sphere
(Delaunay) property of the representative coordinates. The scientific state is
a valid causal combinatorial triangulation. `tds().is_valid()` establishes the
@@ -72,9 +77,9 @@ comparison outside hot move paths.
| --- | --- | --- | --- |
| `(2,3)` | Equation (58): `(3,1) 1345 + (2,2) 2345 -> (3,1) 1234 + (2,2) 1235 + (2,2) 1245`, or its time reflection. The shared triangle `345` is timelike and the new edge `12` is timelike. | `(0, 0, +1, +2, 0, +1, 0, +1)` | A checked `Triangulation_3::flip(facet)` is attempted only from correctly labelled `(2,2)` cells whose neighbor is a correctly labelled `(3,1)` or `(1,3)` cell and whose opposite vertices lie on adjacent slices. CGAL additionally rejects infinite, nonflippable, or geometrically inverted cavities. |
| `(3,2)` | The inverse of equation (58): two `(2,2)` cells and exactly one `(3,1)` or `(1,3)` cell meet at the timelike edge removed by the move. | `(0, 0, -1, -2, 0, -1, 0, -1)` | Before `Triangulation_3::flip(edge)`, CDT++ independently requires three finite incident cells spanning adjacent slices with the exact `2 x (2,2) + 1 x ((3,1) or (1,3))` causal composition. CGAL then rejects hull edges and enforces the total degree-three and geometric flippability contracts. Other degree-three timelike cavities can be combinatorially flippable but causally invalid. |
-| `(2,6)` | Equation (56): `(1,3) 1345 + (3,1) 2345` share the spacelike triangle `345`. A new vertex `6` is inserted on the same slice and joined to all five old vertices, producing three tetrahedra above and three below. | `(+1, +3, +2, +8, +2, 0, +2, +4)` | `tds().insert_in_facet()` subdivides the common spacelike facet. The input cells and their metadata must be `(1,3)` and `(3,1)`, the shared vertices must have one time value, and the new star must contain six valid cells. The new point is the facet centroid and receives the facet time. |
+| `(2,6)` | Equation (56): `(1,3) 1345 + (3,1) 2345` share the spacelike triangle `345`. A new vertex `6` is inserted on the same slice and joined to all five old vertices, producing three tetrahedra above and three below. | `(+1, +3, +2, +8, +2, 0, +2, +4)` | `tds().insert_in_facet()` subdivides the common spacelike facet. The input cells and their metadata must be `(1,3)` and `(3,1)`, the shared vertices must have one time value, and the new star must contain six valid cells. The new point is the facet centroid and receives the facet time; the move rejects an already occupied centroid before mutation so coordinate-valued locators remain unique. |
| `(6,2)` | The inverse of equation (56): a degree-five vertex has six incident cells, exactly three `(3,1)` and three `(1,3)`, with no `(2,2)` cell. | `(-1, -3, -2, -8, -2, 0, -2, -4)` | On a private copy, a checked timelike edge flip reduces the candidate to degree four; `tds().remove_from_maximal_dimension_simplex()` then applies its documented degree-`dimension+1` removal. Exact finite incidence, causal types, metadata, output counts, and output cell types are checked before publication. |
-| `(4,4)` | Equation (57): two `(1,3)` and two `(3,1)` tetrahedra form a diamond. The diagonal of the spatial quadrilateral is exchanged; the move is its own inverse. | `(0, 0, 0, 0, 0, 0, 0, 0)` | The pivot must be a spacelike edge with exactly four finite incident cells, two `(3,1)` and two `(1,3)`, all correctly labelled. A checked TDS facet flip creates the new diagonal, then a checked TDS edge flip removes the old one. The composition is performed on a private copy because the transient geometry can fail a `Triangulation_3` geometric flip even when the final abstract diamond is valid. |
+| `(4,4)` | Equation (57): two `(1,3)` and two `(3,1)` tetrahedra form a diamond. The diagonal of the spatial quadrilateral is exchanged; the move is its own inverse. | `(0, 0, 0, 0, 0, 0, 0, 0)` | The pivot must be a spacelike edge with exactly four finite incident cells, two `(3,1)` and two `(1,3)`, all correctly labelled. A checked TDS facet flip creates the new diagonal, then a checked TDS edge flip removes the old one. Both descriptors use canonical incident-cell representatives, and edge endpoints use canonical point order, so equivalent reloaded TDS layouts reach the same checked mutation boundary. The composition is performed on a private copy because the transient geometry can fail a `Triangulation_3` geometric flip even when the final abstract diamond is valid. |
## Independent delta derivations
@@ -115,8 +120,12 @@ helper.
- canonical point/time/simplex representations independent of CGAL handle
identity;
- exact inverse round trips for `(2,3)/(3,2)`, `(2,6)/(6,2)`, and `(4,4)`;
+- equivalent `(4,4)` cavities with different CGAL cell iteration orders and
+ reversed edge descriptors reaching the same canonical transition;
- malformed-handle, stale-metadata, wrong-cavity, non-applicable, and empty-state
- rejection checks with canonical failure-atomicity comparisons; and
+ rejection checks with canonical failure-atomicity comparisons;
+- failure-atomic rejection when a `(2,6)` facet centroid already belongs to
+ another vertex;
- valid construction, forbidden default construction, and stale-locator
behavior for the applicable-move boundary, including structured failure
classification; and
diff --git a/docs/reproducibility.md b/docs/reproducibility.md
index 667feb917..48ea80b8e 100644
--- a/docs/reproducibility.md
+++ b/docs/reproducibility.md
@@ -89,12 +89,17 @@ manifest. The manifest records:
transitions;
- the maximum requested Delaunay thread count;
- completed passes and the transition-trace fingerprint;
+- when a run starts from `--input`, the initial artifact role, source seed,
+ initialization stream, placement fingerprint, and topology fingerprint;
- a canonical placement fingerprint derived from sorted finite vertices and
their timeslices;
- a canonical topology fingerprint derived from sorted vertices, causal
metadata, and finite-cell incidence;
- the CDT++ version, compiler, build configuration, standard library,
- operating system, architecture, C++ standard, and CGAL version;
+ operating system, architecture, C++ standard, source revision, and CGAL
+ version;
+- for a resumable checkpoint, the complete transition PCG state and cumulative
+ proposed, accepted, rejected, attempted, succeeded, and failed move counts;
- the payload byte count and FNV-1a corruption checksum.
The triangulation remains a CGAL-readable payload; provenance is in the sidecar
@@ -132,16 +137,95 @@ manifest/payload mismatches fail with filesystem diagnostics. FNV-1a protects
against accidental truncation or corruption; it does not authenticate files
against deliberate modification.
-Checkpoints are snapshots only. CDT++ does not currently expose a resume CLI,
-and a checkpoint does not serialize mutable PCG engine state or enough runtime
-state to continue the identical stream. The manifest explicitly records
-`resume_supported=false`. The manifest also records
-`fresh_topology_replay_supported=false` and
-`transition_replay_requires_identical_start=true`. Starting a second CLI run
-from the recorded seed and configuration replays the stochastic inputs, but it
-does not override CGAL's non-unique cospherical tetrahedralization. Exact
-transition replay is conditional on supplying an identical starting manifold;
-it is not checkpoint resume.
+### Initial-state handoff
+
+`initialize --output` publishes an `initial-triangulation` payload and
+manifest. `cdt --input PATH` requires both files, verifies the payload checksum,
+parses the complete CGAL stream and CDT++ causal trailer, reconciles the
+manifest with the parsed state, and reconstructs the foliation and manifold
+before attempting a transition. Checkpoint and final artifacts are rejected at
+this boundary. Because prepared move locators are coordinate-valued, this
+evolution boundary also rejects distinct TDS vertices with coincident
+coordinates; generic `read_file` remains available for archival inspection of
+such legacy or manually constructed states.
+
+The input supplies topology, dimension, requested construction counts,
+foliation parameters, and the exact starting state. The new `cdt` invocation
+supplies its own physical parameters, run cadence, output policy, thread limit,
+and root seed. Its transition stream is derived from that new seed; it does not
+continue mutable RNG state from `initialize`. Checkpoint and final metadata
+carry `input.*` fields identifying the initial artifact seed and stream plus
+its placement and topology fingerprints, so the handoff remains auditable
+after evolution changes the state.
+
+The `.off` suffix is historical. The payload is CGAL's version-coupled
+triangulation stream followed by CDT++'s causal-data trailer, not a standalone
+Geomview mesh. The `.off.meta` file is part of the interchange contract because
+it supplies artifact role, foliation, reproducibility, and integrity data that
+plain geometry cannot represent. See the
+[reference fixture package](../reference/README.md) for the downstream import
+boundary.
+
+### Exact checkpoint continuation
+
+Simulation checkpoints written by this release are restart artifacts. At each
+completed checkpoint pass, CDT++ publishes the triangulation and manifest with
+individually atomic replacements. The manifest contains the global completed
+pass, configured total-pass target, checkpoint cadence, physical parameters,
+thread limit, complete mutable PCG state, ordered transition-trace state and
+count, and all cumulative move counters. The manifest records
+`resume_supported=true` only when this complete state is present.
+
+Resume the run with the neighboring pair intact:
+
+```console
+just resume /path/to/checkpoint.off
+```
+
+By default, `cdt` executes the passes remaining before the saved total target.
+`--passes TOTAL` may set a greater global target; it is not an additional-pass
+count and cannot precede the checkpoint's completed pass. Pass numbering and
+checkpoint cadence remain global, so a run interrupted after pass 500 does not
+restart either at pass 1. `--no-output` remains available, but `--resume`
+rejects replacement topology, construction, action, seed, thread, and
+checkpoint-cadence options.
+
+Exact continuation is deliberately version-locked. Before returning any
+resume state, the reader verifies payload integrity and causal metadata, parses
+the complete PCG state, validates counter identities, and requires the current
+CDT++ version, source revision, compiler identity and version, build
+configuration, parallel-triangulation feature, operating system, architecture,
+standard library, and CGAL version to match the producer. A checkpoint is
+therefore suitable for restarting an interrupted Slurm/HPC job with the same
+built program; it is not a portable interchange format or a promise that
+another implementation uses the same random engine representation. Resume also
+rejects coincident vertex coordinates, which cannot identify the unique
+coordinate-valued move locators required by exact continuation. The `(2,6)`
+move prevents current runs from creating that ambiguous state.
+
+The scientific tests exercise the guarantee at two levels. A transition-level
+test restores a checkpoint and compares every subsequent move, outcome,
+triangulation, transition trace, and counter update. The end-to-end
+`checkpoint-resume` CTest compares an uninterrupted `N`-pass run with the same
+run split into `K` passes plus a persisted checkpoint and `N-K` resumed passes.
+It requires equal final canonical placement and topology fingerprints,
+transition trace and count, and all six cumulative counter groups. The test
+also verifies target extension with global checkpoint numbering, rejects a
+target below the completed pass, and rejects corrupt PCG state and conflicting
+resume options.
+
+Older checkpoint manifests that record `resume_supported=false` remain
+readable as validated snapshots through the general persistence API, but
+`cdt --resume` rejects them because they do not contain enough state to prove
+the identical continuation. `cdt --input` continues to accept only an
+`initial-triangulation` artifact and intentionally starts a new transition
+stream from a new seed.
+
+Every manifest also records `fresh_topology_replay_supported=false` and
+`transition_replay_requires_identical_start=true`. Reconstructing a fresh
+manifold from only the recorded seed and configuration still does not override
+CGAL's non-unique cospherical tetrahedralization; checkpoint continuation
+avoids that ambiguity by restoring the persisted manifold itself.
The tracked OFF fixture used by the archival renderer is therefore the canonical rendering input. Its recorded seed
and producer command reproduce stochastic inputs but are provenance, not a promise that fresh CGAL construction will
diff --git a/include/Ergodic_moves_3.hpp b/include/Ergodic_moves_3.hpp
index 02c99bbef..cf611a7c3 100644
--- a/include/Ergodic_moves_3.hpp
+++ b/include/Ergodic_moves_3.hpp
@@ -347,6 +347,30 @@ namespace cdt::ergodic_moves
});
}
+ /// @brief Rebind an edge to its canonical finite incident cell.
+ /// @details CGAL permits any incident cell to own an edge descriptor, but
+ /// checked flips can observe that representative. Choosing by canonical
+ /// cell points makes equivalent in-memory and reloaded triangulations use
+ /// the same mutation boundary.
+ [[nodiscard]] inline auto canonical_edge_descriptor(
+ Delaunay const& triangulation, Edge_handle const& edge)
+ -> std::optional
+ {
+ if (!is_well_formed_edge(edge)) { return std::nullopt; }
+ auto incident_cells = finite_incident_cells(triangulation, edge);
+ if (!incident_cells || incident_cells->empty()) { return std::nullopt; }
+ canonicalize(*incident_cells);
+
+ auto first = edge.first->vertex(edge.second);
+ auto second = edge.first->vertex(edge.third);
+ if (point_less(second->point(), first->point()))
+ {
+ std::swap(first, second);
+ }
+ auto const cell = incident_cells->front();
+ return Edge_handle{cell, cell->index(first), cell->index(second)};
+ }
+
[[nodiscard]] inline auto vertex_precedes(Vertex_handle const& left,
Vertex_handle const& right)
-> bool
@@ -783,7 +807,10 @@ namespace cdt::ergodic_moves
using enum move_tracker::MoveType;
auto const edge = resolve_edge(triangulation, move.m_edge);
if (!edge) { return move_error(MoveFailure::STALE_CANDIDATE, THREE_TWO); }
- if (!triangulation.flip(edge->first, edge->second, edge->third))
+ auto const canonical_edge = canonical_edge_descriptor(triangulation, *edge);
+ if (!canonical_edge ||
+ !triangulation.flip(canonical_edge->first, canonical_edge->second,
+ canonical_edge->third))
{
return move_error(MoveFailure::EXECUTION_FAILURE, THREE_TWO);
}
@@ -997,6 +1024,17 @@ namespace cdt::ergodic_moves
auto const v_1 = (*bottom)->vertex(first);
auto const v_2 = (*bottom)->vertex(second);
auto const v_3 = (*bottom)->vertex(third);
+ std::array face_points{v_1->point(), v_2->point(), v_3->point()};
+ std::ranges::sort(face_points, point_less);
+ auto const center_point =
+ CGAL::centroid(face_points[0], face_points[1], face_points[2]);
+ if (std::ranges::any_of(triangulation.finite_vertex_handles(),
+ [&](auto const& vertex) {
+ return vertex->point() == center_point;
+ }))
+ {
+ return move_error(MoveFailure::INVARIANT_VIOLATION, TWO_SIX);
+ }
auto const center =
triangulation.tds().insert_in_facet(*bottom, common_face_index);
@@ -1012,10 +1050,7 @@ namespace cdt::ergodic_moves
return move_error(MoveFailure::INVARIANT_VIOLATION, TWO_SIX);
}
- std::array face_points{v_1->point(), v_2->point(), v_3->point()};
- std::ranges::sort(face_points, point_less);
- center->set_point(
- CGAL::centroid(face_points[0], face_points[1], face_points[2]));
+ center->set_point(center_point);
center->info() = v_1->info();
if (!post_mutation_validator(static_cast(triangulation)) ||
@@ -1269,7 +1304,9 @@ namespace cdt::ergodic_moves
auto flipped = false;
for (auto const& edge : incident_edges)
{
- if (is_timelike(edge) && tds.flip(edge))
+ auto const canonical_edge =
+ canonical_edge_descriptor(triangulation, edge);
+ if (is_timelike(edge) && canonical_edge && tds.flip(*canonical_edge))
{
flipped = true;
break;
@@ -1607,17 +1644,18 @@ namespace cdt::ergodic_moves
{
using enum move_tracker::MoveType;
Delaunay triangulation{source_triangulation};
- auto const edge = resolve_edge(triangulation, move.edge_points());
+ auto const edge = resolve_edge(triangulation, move.edge_points());
+ auto const pivot_from_1 =
+ resolve_vertex(triangulation, move.edge_points()[0]);
+ auto const pivot_from_2 =
+ resolve_vertex(triangulation, move.edge_points()[1]);
auto const top = resolve_vertex(triangulation, move.top_point());
auto const bottom = resolve_vertex(triangulation, move.bottom_point());
- if (!edge || !top || !bottom)
+ if (!edge || !pivot_from_1 || !pivot_from_2 || !top || !bottom)
{
return move_error(MoveFailure::STALE_CANDIDATE, FOUR_FOUR);
}
- auto const pivot_from_1 = edge->first->vertex(edge->second);
- auto const pivot_from_2 = edge->first->vertex(edge->third);
-
// A 3D 4-to-4 bistellar move is the composition of CGAL's checked TDS
// 2-to-3 facet flip and checked 3-to-2 edge flip. The TDS operations are
// intentional: either geometrically checked Triangulation_3 facet flip
@@ -1629,28 +1667,46 @@ namespace cdt::ergodic_moves
int pivot_from_1_index{};
int pivot_from_2_index{};
int boundary_index{};
- if (!triangulation.is_facet(pivot_from_1, pivot_from_2, *bottom,
+ if (!triangulation.is_facet(*pivot_from_1, *pivot_from_2, *bottom,
boundary_facet_cell, pivot_from_1_index,
pivot_from_2_index, boundary_index))
{
return move_error(MoveFailure::STALE_CANDIDATE, FOUR_FOUR);
}
constexpr auto cell_index_sum = 0 + 1 + 2 + 3;
- auto const boundary_facet_index = cell_index_sum - pivot_from_1_index -
+ auto boundary_facet_index = cell_index_sum - pivot_from_1_index -
pivot_from_2_index - boundary_index;
+ auto const other_boundary_cell =
+ boundary_facet_cell->neighbor(boundary_facet_index);
+ if (other_boundary_cell == nullptr ||
+ triangulation.is_infinite(boundary_facet_cell) ||
+ triangulation.is_infinite(other_boundary_cell))
+ {
+ return move_error(MoveFailure::EXECUTION_FAILURE, FOUR_FOUR);
+ }
+ if (cell_precedes(other_boundary_cell, boundary_facet_cell))
+ {
+ boundary_facet_index = other_boundary_cell->index(boundary_facet_cell);
+ boundary_facet_cell = other_boundary_cell;
+ }
if (!triangulation.tds().flip(
Delaunay::Facet{boundary_facet_cell, boundary_facet_index}))
{
return move_error(MoveFailure::EXECUTION_FAILURE, FOUR_FOUR);
}
- Cell_handle old_edge_cell = nullptr;
- int old_edge_first_index{};
- int old_edge_second_index{};
- if (!triangulation.is_edge(pivot_from_1, pivot_from_2, old_edge_cell,
- old_edge_first_index, old_edge_second_index) ||
- !triangulation.tds().flip(Delaunay::Edge{
- old_edge_cell, old_edge_first_index, old_edge_second_index}))
+ auto const old_edge = resolve_edge(triangulation, move.edge_points());
+ if (!old_edge)
+ {
+ return move_error(MoveFailure::EXECUTION_FAILURE, FOUR_FOUR);
+ }
+ auto const canonical_old_edge =
+ canonical_edge_descriptor(triangulation, *old_edge);
+ if (!canonical_old_edge)
+ {
+ return move_error(MoveFailure::EXECUTION_FAILURE, FOUR_FOUR);
+ }
+ if (!triangulation.tds().flip(*canonical_old_edge))
{
return move_error(MoveFailure::EXECUTION_FAILURE, FOUR_FOUR);
}
diff --git a/include/Metropolis.hpp b/include/Metropolis.hpp
index 07ae24ec1..a41c02f0a 100644
--- a/include/Metropolis.hpp
+++ b/include/Metropolis.hpp
@@ -102,7 +102,54 @@ namespace cdt
/// @brief Checkpoint events from the latest completed invocation
Int_precision m_checkpoint_events{};
- static void record_transition(
+ /// @brief Passes completed before this invocation, including resumed work.
+ Int_precision m_completed_passes{};
+
+ /// @brief Whether the next invocation must begin from restored accounting.
+ bool m_resume_pending{};
+
+ [[nodiscard]] static auto to_counts(Counter const& counter)
+ -> utilities::Move_statistics::Counts
+ {
+ utilities::Move_statistics::Counts counts{};
+ for (std::size_t index = 0; index < counts.size(); ++index)
+ {
+ counts[index] = counter[static_cast(index)];
+ }
+ return counts;
+ }
+
+ [[nodiscard]] static auto from_counts(
+ utilities::Move_statistics::Counts const& counts) -> Counter
+ {
+ Counter counter;
+ for (std::size_t index = 0; index < counts.size(); ++index)
+ {
+ counter[static_cast(index)] = counts[index];
+ }
+ return counter;
+ }
+
+ void restore_statistics(utilities::Reproducibility_metadata const& metadata)
+ {
+ if (!metadata.move_statistics || !metadata.transition_trace ||
+ !metadata.transition_count)
+ {
+ throw std::invalid_argument(
+ "Checkpoint resume requires cumulative transition statistics.");
+ }
+ auto const& saved = *metadata.move_statistics;
+ m_command_results.attempted = from_counts(saved.attempted);
+ m_command_results.succeeded = from_counts(saved.succeeded);
+ m_command_results.failed = from_counts(saved.failed);
+ m_run_statistics.proposed = from_counts(saved.proposed);
+ m_run_statistics.accepted = from_counts(saved.accepted);
+ m_run_statistics.rejected = from_counts(saved.rejected);
+ m_run_statistics.transition_trace = *metadata.transition_trace;
+ m_run_statistics.transition_count = *metadata.transition_count;
+ }
+
+ static void record_transition(
RunStatistics& statistics, move_tracker::MoveType const move,
ergodic_moves::MoveOutcome const outcome) noexcept
{
@@ -157,36 +204,74 @@ namespace cdt
/// @param random Transition random-number stream owned by this strategy.
/// @param reproducibility Optional initialization and requested-state
/// provenance to merge with the effective run configuration.
+ /// @param completed_passes Global passes completed by a restored
+ /// checkpoint.
/// @throws std::invalid_argument If a coupling is non-finite or either
- /// cadence value is nonpositive.
+ /// cadence value is nonpositive, or restored state is incomplete, does not
+ /// match the global pass target, or does not match the supplied generator.
/// @throws std::domain_error If `alpha` is not greater than 1/2.
[[maybe_unused]] MoveStrategy(
long double const alpha, long double const k, long double const lambda,
Int_precision const passes, Int_precision const checkpoint,
bool const write_files, cdt::Random random,
std::optional reproducibility =
- std::nullopt)
+ std::nullopt,
+ Int_precision const completed_passes = 0)
: m_parameters{s3_action::make_physical_parameters(alpha, k, lambda)}
, m_cadence{detail::parse_move_run_cadence(passes, checkpoint,
"Metropolis")}
, m_write_files{write_files}
, m_generator{std::move(random)}
- , m_reproducibility{
- reproducibility.value_or(utilities::Reproducibility_metadata{
+ , m_reproducibility{reproducibility.value_or(
+ utilities::Reproducibility_metadata{
.seed = m_generator.seed(),
.alpha = alpha,
.k = k,
.lambda = lambda,
.configured_passes = passes,
.checkpoint_interval = checkpoint})}
+ , m_completed_passes{completed_passes}
{
- m_reproducibility.seed = m_generator.seed();
- m_reproducibility.transition_stream = m_generator.stream();
- m_reproducibility.alpha = m_parameters.alpha();
- m_reproducibility.k = m_parameters.k();
- m_reproducibility.lambda = m_parameters.lambda();
- m_reproducibility.configured_passes = m_cadence.passes();
+ if (m_completed_passes < 0)
+ {
+ throw std::invalid_argument("Completed passes cannot be negative.");
+ }
+ m_reproducibility.alpha = m_parameters.alpha();
+ m_reproducibility.k = m_parameters.k();
+ m_reproducibility.lambda = m_parameters.lambda();
+ auto const total_passes = static_cast(m_completed_passes) +
+ static_cast(m_cadence.passes());
+ if (!std::in_range(total_passes))
+ {
+ throw std::out_of_range(
+ "Total pass count exceeds the supported range.");
+ }
+ if (m_completed_passes > 0 || m_reproducibility.transition_random_state)
+ {
+ if (!m_reproducibility.configured_passes ||
+ *m_reproducibility.configured_passes != total_passes ||
+ !m_reproducibility.transition_random_state)
+ {
+ throw std::invalid_argument(
+ "Checkpoint resume state does not match its pass range.");
+ }
+ if (m_reproducibility.seed != m_generator.seed() ||
+ m_reproducibility.transition_stream != m_generator.stream() ||
+ *m_reproducibility.transition_random_state !=
+ m_generator.serialized_state())
+ {
+ throw std::invalid_argument(
+ "Checkpoint resume generator does not match its recorded random state.");
+ }
+ restore_statistics(m_reproducibility);
+ m_resume_pending = true;
+ }
+ m_reproducibility.seed = m_generator.seed();
+ m_reproducibility.transition_stream = m_generator.stream();
+ m_reproducibility.configured_passes =
+ static_cast(total_passes);
m_reproducibility.checkpoint_interval = m_cadence.checkpoint();
+ m_reproducibility.transition_random_state.reset();
#ifndef NDEBUG
spdlog::debug("{} called.\n", CDT_PRETTY_FUNCTION);
#endif
@@ -268,7 +353,7 @@ namespace cdt
-> utilities::Reproducibility_metadata
{
return make_reproducibility_metadata(manifold, artifact, completed_passes,
- m_run_statistics);
+ m_command_results, m_run_statistics);
}
/// @returns The container of trial moves
@@ -453,8 +538,9 @@ namespace cdt
[[nodiscard]] auto make_reproducibility_metadata(
ManifoldType const& manifold, utilities::ArtifactKind const artifact,
- Int_precision const completed_passes,
- RunStatistics const& statistics) const
+ Int_precision const completed_passes,
+ CommandResults const& command_results,
+ RunStatistics const& statistics) const
-> utilities::Reproducibility_metadata
{
auto metadata = m_reproducibility;
@@ -462,6 +548,23 @@ namespace cdt
metadata.completed_passes = completed_passes;
metadata.transition_trace = statistics.transition_trace;
metadata.transition_count = statistics.transition_count;
+ metadata.move_statistics = utilities::Move_statistics{
+ .proposed = to_counts(statistics.proposed),
+ .accepted = to_counts(statistics.accepted),
+ .rejected = to_counts(statistics.rejected),
+ .attempted = to_counts(command_results.attempted),
+ .succeeded = to_counts(command_results.succeeded),
+ .failed = to_counts(command_results.failed)};
+ if (artifact == utilities::ArtifactKind::CHECKPOINT &&
+ metadata.max_threads && *metadata.max_threads > 0)
+ {
+ metadata.transition_random_state =
+ std::make_shared(m_generator.serialized_state());
+ }
+ else
+ {
+ metadata.transition_random_state.reset();
+ }
utilities::update_reproducibility_state(metadata, manifold);
if (metadata.desired_simplices == 0)
{
@@ -681,10 +784,16 @@ namespace cdt
spdlog::debug("{} called.\n", CDT_PRETTY_FUNCTION);
#endif
- auto initial_statistics = RunStatistics{};
+ auto initial_statistics =
+ m_resume_pending ? m_run_statistics : RunStatistics{};
initial_statistics.geometry = t_manifold.geometry();
- auto result = detail::execute_move_run(
- t_manifold, std::move(initial_statistics), m_cadence,
+ auto initial_command_results =
+ m_resume_pending ? m_command_results : CommandResults{};
+ auto const initial_completed_passes =
+ m_resume_pending ? m_completed_passes : Int_precision{};
+ auto result = detail::execute_move_run(
+ t_manifold, std::move(initial_command_results),
+ std::move(initial_statistics), initial_completed_passes, m_cadence,
detail::MoveRunIdentity{.algorithm = "Metropolis-Hastings",
.seed = seed(),
.stream = stream()},
@@ -698,18 +807,21 @@ namespace cdt
RunStatistics const& statistics) {
print_results(command_results, statistics);
},
- [this](ManifoldType const& current, CommandResults const&,
- RunStatistics const& statistics,
- Int_precision const pass_number) {
+ [this](ManifoldType const& current,
+ CommandResults const& command_results,
+ RunStatistics const& statistics,
+ Int_precision const pass_number) {
utilities::write_file(
current, make_reproducibility_metadata(
current, utilities::ArtifactKind::CHECKPOINT,
- pass_number, statistics));
+ pass_number, command_results, statistics));
});
m_command_results = std::move(result.command_results);
m_run_statistics = std::move(result.strategy_state);
m_checkpoint_events = result.checkpoint_events;
+ m_completed_passes = initial_completed_passes + m_cadence.passes();
+ m_resume_pending = false;
return std::move(result.manifold);
}
diff --git a/include/Move_always.hpp b/include/Move_always.hpp
index 95ceee1ae..693796d0d 100644
--- a/include/Move_always.hpp
+++ b/include/Move_always.hpp
@@ -184,7 +184,7 @@ namespace cdt
spdlog::debug("{} called.\n", CDT_PRETTY_FUNCTION);
#endif
auto result = detail::execute_move_run(
- t_manifold, std::monostate{}, m_cadence,
+ t_manifold, CommandResults{}, std::monostate{}, 0, m_cadence,
detail::MoveRunIdentity{
.algorithm = "Move Always", .seed = seed(), .stream = stream()},
m_write_files,
diff --git a/include/Move_run.hpp b/include/Move_run.hpp
index d24b4ef69..8c67e158b 100644
--- a/include/Move_run.hpp
+++ b/include/Move_run.hpp
@@ -14,6 +14,7 @@
#include
#include
+#include
#include
#include
#include
@@ -175,17 +176,24 @@ namespace cdt
/// caller, so a reusable strategy never exposes partially reset counters.
template
- [[nodiscard]] auto execute_move_run(ManifoldType initial,
- StrategyState initial_strategy_state,
- MoveRunCadence const cadence,
- MoveRunIdentity const identity,
- bool const writes_files,
- ExecutePass execute_pass, Report report,
- Checkpoint checkpoint)
+ [[nodiscard]] auto execute_move_run(
+ ManifoldType initial,
+ MoveCommandResults initial_command_results,
+ StrategyState initial_strategy_state,
+ Int_precision const completed_passes, MoveRunCadence const cadence,
+ MoveRunIdentity const identity, bool const writes_files,
+ ExecutePass execute_pass, Report report, Checkpoint checkpoint)
-> MoveRunResult
{
+ if (completed_passes < 0 ||
+ completed_passes >
+ std::numeric_limits::max() - cadence.passes())
+ {
+ throw std::invalid_argument(
+ "Completed and configured passes exceed the supported range.");
+ }
auto current = std::move(initial);
- auto command_totals = MoveCommandResults{};
+ auto command_totals = std::move(initial_command_results);
auto strategy_state = std::move(initial_strategy_state);
auto checkpoint_events = Int_precision{};
@@ -198,7 +206,7 @@ namespace cdt
for (auto pass_index = Int_precision{}; pass_index < cadence.passes();
++pass_index)
{
- auto const pass_number = pass_index + 1;
+ auto const pass_number = completed_passes + pass_index + 1;
fmt::print("=== Pass {} ===\n", pass_number);
auto const attempts = current.N3();
auto pass = std::invoke(execute_pass, std::move(current),
diff --git a/include/Random.hpp b/include/Random.hpp
index 041856713..1cb63b07b 100644
--- a/include/Random.hpp
+++ b/include/Random.hpp
@@ -12,9 +12,33 @@
#include
#include
+#include
#include
+#include
#include
#include
+#include
+#include
+#include
+#include
+
+#if !defined(__SIZEOF_INT128__) || (defined(PCG_FORCE_EMULATED_128BIT_MATH) && \
+ PCG_FORCE_EMULATED_128BIT_MATH)
+#include "pcg_uint128.hpp"
+
+/// @cond PCG_COMPATIBILITY
+namespace pcg_extras
+{
+ // PCG's specific_stream::set_stream uses an int literal with its emulated
+ // 128-bit value. Supply the heterogeneous overload that template deduction
+ // cannot obtain through uint_x4's converting constructor.
+ template
+ [[nodiscard]] auto operator|(uint_x4 const& value,
+ int const bits) -> uint_x4
+ { return value | uint_x4{bits}; }
+} // namespace pcg_extras
+/// @endcond
+#endif
#include "pcg_random.hpp"
@@ -169,6 +193,48 @@ namespace cdt
/// @return A new engine at the beginning of the selected sequence.
[[nodiscard]] auto split(RandomStream const stream) const -> Random
{ return Random{m_seed, stream}; }
+
+ /// @brief Serialize the complete mutable PCG state for exact continuation.
+ /// @return Locale-independent PCG engine state.
+ [[nodiscard]] auto serialized_state() const -> std::string
+ {
+ std::ostringstream output;
+ output.imbue(std::locale::classic());
+ output << m_engine;
+ if (!output)
+ {
+ throw std::runtime_error("Could not serialize PCG state.");
+ }
+ return output.str();
+ }
+
+ /// @brief Restore an exact PCG continuation point.
+ /// @param seed Recorded root seed.
+ /// @param stream Recorded stream selector.
+ /// @param state Complete state produced by serialized_state().
+ /// @return A generator whose next draw is the saved generator's next draw.
+ /// @throws std::invalid_argument if the state is malformed, contains
+ /// trailing data, or selects a different PCG stream.
+ [[nodiscard]] static auto from_serialized_state(
+ RandomSeed const seed, RandomStream const stream,
+ std::string_view const state) -> Random
+ {
+ auto restored = Random{seed, stream};
+ auto const expected_stream = restored.m_engine.stream();
+ std::istringstream input{std::string{state}};
+ input.imbue(std::locale::classic());
+ input >> restored.m_engine;
+ if (!input || restored.m_engine.stream() != expected_stream)
+ {
+ throw std::invalid_argument("Malformed or mismatched PCG state.");
+ }
+ input >> std::ws;
+ if (!input.eof())
+ {
+ throw std::invalid_argument("PCG state contains trailing data.");
+ }
+ return restored;
+ }
};
static_assert(std::uniform_random_bit_generator);
diff --git a/include/Utilities.hpp b/include/Utilities.hpp
index ddd6d3aaf..dd132820a 100644
--- a/include/Utilities.hpp
+++ b/include/Utilities.hpp
@@ -26,6 +26,7 @@
#include
#include
#include