From ec53dfaf3307e7a862ebc0994827a2b4974f52a7 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 25 Jul 2026 06:55:30 -0700 Subject: [PATCH 1/9] feat(reference)!: publish deterministic C++ oracle fixtures - Add versioned schemas, canonical raw outputs, provenance manifests, and quantity-specific comparison rules for Rust validation. - Cover causal topology, every 2+1D move, nonzero Regge actions, persistence, bounded execution, and matched thread scaling. - Add clean-commit regeneration and offline validation workflows with a monotone, overflow-safe spherical population contract. - Correct stale LCOV inconsistency counts exposed by the updated utilities. BREAKING CHANGE: The spherical population estimator now uses a monotone, margin-adjusted schedule, changing generated point counts. The s3_bulk_action_alpha_minus_one API is now named s3_bulk_action_alpha_minus_one_imaginary_coefficient. Closes #94 --- .gitignore | 1 + Justfile | 37 +- README.md | 83 +- REFERENCES.md | 16 + docs/Doxyfile | 1 + docs/cgal-integration.md | 37 + docs/metropolis-hastings.md | 34 + docs/reproducibility.md | 6 + include/S3Action.hpp | 16 +- include/Utilities.hpp | 227 ++++- pyproject.toml | 1 + reference/README.md | 119 +++ reference/fixtures/v1/protocol.json | 332 +++++++ reference/manifests/v1/macos-arm64.json | 167 ++++ .../manifests/v1/scaling-macos-arm64.json | 118 +++ reference/raw/v1/cpp-reference.json | 1 + reference/raw/v1/end-to-end.txt | 51 + reference/raw/v1/persistence-v1.off | 59 ++ reference/raw/v1/persistence-v1.off.meta | 35 + reference/raw/v1/scaling-threads-1.txt | 59 ++ reference/raw/v1/scaling-threads-2.txt | 59 ++ reference/raw/v1/scaling-threads-4.txt | 59 ++ reference/schema/fixture-v1.schema.json | 483 +++++++++ reference/schema/result-v1.schema.json | 426 ++++++++ reference/schema/run-manifest-v1.schema.json | 263 +++++ scripts/coverage.sh | 11 +- scripts/generate_reference_fixtures.py | 490 +++++++++ .../tests/test_generate_reference_fixtures.py | 45 + .../tests/test_validate_reference_fixtures.py | 154 +++ scripts/validate_reference_fixtures.py | 926 ++++++++++++++++++ src/CMakeLists.txt | 3 +- tests/CMakeLists.txt | 14 + tests/Metropolis_test.cpp | 102 +- tests/Reference_fixture.cpp | 471 +++++++++ tests/Runtime_config_test.cpp | 5 +- tests/S3Action_test.cpp | 103 +- tests/Utilities_test.cpp | 56 +- uv.lock | 2 + 38 files changed, 4945 insertions(+), 127 deletions(-) create mode 100644 reference/README.md create mode 100644 reference/fixtures/v1/protocol.json create mode 100644 reference/manifests/v1/macos-arm64.json create mode 100644 reference/manifests/v1/scaling-macos-arm64.json create mode 100644 reference/raw/v1/cpp-reference.json create mode 100644 reference/raw/v1/end-to-end.txt create mode 100644 reference/raw/v1/persistence-v1.off create mode 100644 reference/raw/v1/persistence-v1.off.meta create mode 100644 reference/raw/v1/scaling-threads-1.txt create mode 100644 reference/raw/v1/scaling-threads-2.txt create mode 100644 reference/raw/v1/scaling-threads-4.txt create mode 100644 reference/schema/fixture-v1.schema.json create mode 100644 reference/schema/result-v1.schema.json create mode 100644 reference/schema/run-manifest-v1.schema.json create mode 100644 scripts/generate_reference_fixtures.py create mode 100644 scripts/tests/test_generate_reference_fixtures.py create mode 100644 scripts/tests/test_validate_reference_fixtures.py create mode 100644 scripts/validate_reference_fixtures.py create mode 100644 tests/Reference_fixture.cpp diff --git a/.gitignore b/.gitignore index 44622bd68..6c9933f87 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ grid_d output* errors *.off +!reference/raw/**/*.off *.dat slurm-*.out diff --git a/Justfile b/Justfile index 3dc09cfbf..93fae4c2c 100644 --- a/Justfile +++ b/Justfile @@ -22,6 +22,7 @@ primary_binary := if os_family() == "windows" { "out/build/reference/src/cdt.exe rng_benchmark_binary := if os_family() == "windows" { "out/build/reference/tests/CDT_rng_benchmark.exe" } else { "out/build/reference/tests/CDT_rng_benchmark" } cgal_benchmark_binary := if os_family() == "windows" { "out/build/reference/tests/CDT_cgal_benchmark.exe" } else { "out/build/reference/tests/CDT_cgal_benchmark" } parallel_cgal_benchmark_binary := if os_family() == "windows" { "out/build/parallel/tests/CDT_cgal_benchmark.exe" } else { "out/build/parallel/tests/CDT_cgal_benchmark" } +reference_fixture_binary := if os_family() == "windows" { "out/build/reference/tests/CDT_reference_fixture.exe" } else { "out/build/reference/tests/CDT_reference_fixture" } # Build the supported configuration through the repository build script. [group('workflows')] @@ -35,12 +36,12 @@ build-parallel: # Run fast, non-mutating local validation. [group('workflows')] -check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check release-check python-check semgrep semgrep-test +check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check release-check python-check reference-check semgrep semgrep-test @echo "Checks complete." # Run the comprehensive pre-commit/pre-push validation gate. [group('workflows')] -ci: check _pinact-check build +ci: check _pinact-check reference-generated-check @echo "CI validation complete." # Configure dependencies before CodeQL begins tracing the C++ build. @@ -102,6 +103,38 @@ benchmark-cgal-parallel threads='1 2 4' simplices='640' repetitions='5' moves='5 "${thread_count}" {{ quote(warmups) }} done +# Print the raw deterministic C++ oracle for the versioned reference package. +[group('workflows')] +reference-fixtures: build + {{ reference_fixture_binary }} + +# Regenerate every raw reference artifact and manifest from one clean commit. +[group('workflows')] +reference-regenerate: _sync-python-dev + uv run --no-sync python scripts/generate_reference_fixtures.py --check-clean + just build + just build-parallel + uv run --no-sync python scripts/generate_reference_fixtures.py + just reference-check + just reference-archive-check + +# Validate schemas, exact topology, numerical oracles, and provenance. +[group('workflows')] +reference-check: _sync-python-dev + uv run --no-sync python scripts/validate_reference_fixtures.py + +# Require one clean source revision before an archival tag or publication. +[group('workflows')] +reference-archive-check: _sync-python-dev + uv run --no-sync python scripts/validate_reference_fixtures.py \ + --provenance-only + +# Rebuild the C++ fixture producer and compare its canonical scientific payload. +[group('workflows')] +reference-generated-check: build _sync-python-dev + uv run --no-sync python scripts/validate_reference_fixtures.py \ + --generated-only --fixture-binary {{ quote(reference_fixture_binary) }} + # Apply safe automatic formatting to C++/Python source and the Justfile. [group('workflows')] fix: _format-fix python-fix diff --git a/README.md b/README.md index e945e31fb..ce3ea96d3 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ set, and the final release contract tracked by [issue #90](https://github.com/ac - [Maintenance status](#maintenance-status) - [Introduction](#introduction) - [Regression-oracle scope](#regression-oracle-scope) + - [Usage](#usage) - [Roadmap](#roadmap) - [Quickstart](#quickstart) - [Current reference-suite status](#current-reference-suite-status) @@ -35,8 +36,7 @@ set, and the final release contract tracked by [issue #90](https://github.com/ac - [vcpkg maintenance](#vcpkg-maintenance) - [Build](#build) - [Project Layout](#project-layout) - - [Run](#run) - - [Usage](#usage) + - [Command-line reference](#command-line-reference) - [Documentation](#documentation) - [Citing CDT++](#citing-cdt) - [Testing](#testing) @@ -86,6 +86,10 @@ and time labels. Its inputs, detected bad vertex, final initialization state, ce 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 @@ -93,6 +97,44 @@ After building, run that fixture directly with: --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 currently disabled and will be restored as an opt-in v1.0.0 + target by [#98](https://github.com/acgetchell/CDT-plusplus/issues/98). +- `initialize` is used by [CometML] to run + [parameter optimization](#optimizing-parameters). + +See the [command-line reference](#command-line-reference) for every option. +Build and dependency instructions begin at [Quickstart](#quickstart). + ## Roadmap - [x] Cross-platform support on Linux, macOS (x64 & arm64), and Windows @@ -318,39 +360,7 @@ The project is similar to [PitchFork Layout], as follows: - src - Source files - tests - Unit tests -### Run - -The supported build produces `cdt` and `initialize` in `out/build/reference/src`. Run the primary `cdt` -executable 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 --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 currently disabled and will be restored as an opt-in v1.0.0 target by - [#98](https://github.com/acgetchell/CDT-plusplus/issues/98) -- `initialize` is used by [CometML] to run [parameter optimization](#optimizing-parameters) - -## Usage +## Command-line reference CDT-plusplus uses [program_options] to parse options from the help message, and so understands long or short argument formats, provided the short argument given @@ -432,6 +442,9 @@ Online documentation is at . 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). diff --git a/REFERENCES.md b/REFERENCES.md index b42691860..68aab4fc0 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -65,6 +65,22 @@ DOI: [10.1016/S0195-6698(13)80080-7]( +### Random Voronoi and Delaunay expected complexity + +R. A. Dwyer, “Higher-dimensional Voronoi diagrams in linear expected time,” +*Discrete & Computational Geometry* 6 (1991), 343–367. +DOI: [10.1007/BF02574694](https://doi.org/10.1007/BF02574694) + +### Three-dimensional Delaunay complexity + +S.-W. Cheng, T. K. Dey, and J. R. Shewchuk, *Delaunay Mesh Generation*, +Chapter 4, “Three-dimensional Delaunay triangulation.” CRC Press (2013), +ISBN 978-1-58488-730-0. DOI metadata issued 2016: +[10.1201/b12987](https://doi.org/10.1201/b12987). +The authors’ [Chapter 4 +preprint]() +states the finite three-dimensional tetrahedron bound used by CDT++. + ### PCG random-number generators M. E. O’Neill, “PCG: A family of simple fast space-efficient statistically good diff --git a/docs/Doxyfile b/docs/Doxyfile index 4b13b2484..dbfc87712 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -835,6 +835,7 @@ WARN_LOGFILE = INPUT = README.md \ REFERENCES.md \ + reference/README.md \ docs \ include \ src diff --git a/docs/cgal-integration.md b/docs/cgal-integration.md index 4d59434ff..16dcc4fcc 100644 --- a/docs/cgal-integration.md +++ b/docs/cgal-integration.md @@ -98,6 +98,43 @@ triangulation. CDT++ tests validity and point/label preservation but does not promise identical fresh topology across toolchains; see [Reproducible random runs](reproducibility.md). +## Spherical population estimator + +For base population `p`, initial radius `r0`, spacing `dr`, and `T` layers, the +number of vertices supplied to CGAL is evaluated exactly and overflow-safely: + +```text +V_input(p) = sum(i = 0..T-1, floor(p * (r0 + i * dr))). +``` + +`expected_points_per_timeslice()` is a monotone construction heuristic, not an +exact topology oracle. It takes the upper envelope of the historical `0.4`, +`0.2`, `0.15`, and `0.1` schedules. This preserves their established scale +while replacing each downward threshold jump with a plateau, then adds a 25% +population margin to reduce loss of the inner or outer layer during causal +repair. A small construction floor remains for tiny requests. The selected +population and exact input count are deterministic; the post-repair simplex +count is not assigned a numerical tolerance because randomized cospherical +tetrahedralization is explicitly implementation-specific. + +[Dwyer's expected-linear-complexity +result](../REFERENCES.md#random-voronoi-and-delaunay-expected-complexity) +applies to independent uniformly distributed points in the interior of a ball. +CDT++ instead places points on nested cospherical layers and removes causally +invalid vertices, so that result is background rather than a hard bound or a +direct calibration. Cheng, Dey, and Shewchuk +[document the separate three-dimensional worst-case safety +bound](../REFERENCES.md#three-dimensional-delaunay-complexity): + +```text +N3 <= (n^2 - 3n - 2) / 2 +``` + +CDT++ evaluates it without integer overflow and reports it as unrepresentable when it +exceeds `uint64_t`. It is not used as a preallocation target or expected +result. The reference protocol records randomized construction values with a +band; exact topology is reserved for the deterministic minimal fixtures. + ## Mutation and lifetime rules CGAL documents that every triangulation modification invalidates iterators. diff --git a/docs/metropolis-hastings.md b/docs/metropolis-hastings.md index 0ddd201ae..5a015993d 100644 --- a/docs/metropolis-hastings.md +++ b/docs/metropolis-hastings.md @@ -99,6 +99,40 @@ round-to-nearest with ties to an even significand (`MPFR_RNDN`). Conversion to `long double` occurs only for diagnostic output; an acceptance draw is compared directly with the MPFR probability. +### Action conventions + +`s3_bulk_action()` returns the real generalized action for its validated +physical domain `alpha > 1/2`. `s3_bulk_action_alpha_one()` retains the +historical real expression with published three-decimal coefficients. +`s3_bulk_action_alpha_minus_one_imaginary_coefficient()` is deliberately named +differently: the expression at `alpha = -1` is purely imaginary, while this +real-valued API returns the coefficient of `i`. It neither returns a complex +number nor silently labels that coefficient as a Wick-rotated action. + +The reference fixtures use nonzero couplings and independently evaluate + +```text +min(1, q(T | T') / q(T' | T) * exp(S(T) - S(T'))) +``` + +for an interior probability, its inverse and clamp, an explicit rejection, and +a delta whose positive MPFR probability lies below `long double` range. + +### Quantity-specific tolerances + +Combinatorial fields and deterministic decisions are exact. The published +cross-language protocol in +[`reference/fixtures/v1/protocol.json`](../reference/fixtures/v1/protocol.json) +names separate absolute and relative tolerances for coordinates, closed-form +actions, and probabilities. Randomized post-repair simplex counts are +implementation-specific rather than covered by a generic percentage. + +The generalized `alpha = 1` expression is not expected to equal the rounded +historical formula to a generic percentage. Its test derives an absolute bound +from the difference between each exact and three-decimal coefficient, +multiplied by the actual `(3,1)+(1,3)` and `(2,2)` simplex counts, then adds the +separate libm-to-MPFR conversion bound. + ## References Bibliographic metadata for diff --git a/docs/reproducibility.md b/docs/reproducibility.md index 8c0212d2d..6e523be61 100644 --- a/docs/reproducibility.md +++ b/docs/reproducibility.md @@ -69,6 +69,12 @@ actual counts, it makes output-state differences visible; it is not a fingerprint of the pre-CGAL generated point sequence. Exact generation replay is tested directly before insertion. +The archival cross-language record is published in +[`reference/`](../reference/README.md). It retains raw C++ topology, action, +persistence, bounded-run, and matched-scaling outputs before post-processing; +its manifests add fixture version, command line, host, toolchain, thread +limits, seeds, and SHA-256 checksums. + ## Persistence contract Each new stochastic `.off` payload has a neighboring `.off.meta` text diff --git a/include/S3Action.hpp b/include/S3Action.hpp index 43037dde0..02a2ca679 100644 --- a/include/S3Action.hpp +++ b/include/S3Action.hpp @@ -81,10 +81,12 @@ namespace cdt::s3_action return {k, lambda}; } } // namespace detail - /// @brief Calculates S3 bulk action for \f$\alpha\f$=-1. + /// @brief Calculates the coefficient of \f$i\f$ in the + /// \f$\alpha=-1\f$ S3 bulk action. /// - /// This result is i* the action for Euclidean dynamically triangulated - /// gravity in three dimensions. + /// The Lorentzian expression at \f$\alpha=-1\f$ is purely imaginary. This + /// real-valued API returns its coefficient of \f$i\f$; it does not return a + /// complex number or a Wick-rotated Euclidean action. /// The formula is: /// /// \f[S^{(3)}(\alpha=-1)=-2\pi ik N_1^{TL}+N_3^{(3,1)}\left(2.673ik+0.118i @@ -98,8 +100,10 @@ namespace cdt::s3_action /// @param k_value \f$k=\frac{1}{8\pi G_{Newton}}\f$ /// @param lambda_value \f$\lambda=k*\Lambda\f$ where \f$\Lambda\f$ is the /// Cosmological constant - /// @returns \f$S^{(3)}(\alpha=-1)\f$ as a 256-bit MPFR value - [[nodiscard]] inline auto s3_bulk_action_alpha_minus_one( + /// @returns The real coefficient \f$S^{(3)}(\alpha=-1)/i\f$ as a 256-bit + /// MPFR value + [[nodiscard]] inline auto + s3_bulk_action_alpha_minus_one_imaginary_coefficient( Int_precision const n1_tl_count, Int_precision const n3_31_13_count, Int_precision const n3_22_count, long double const k_value, long double const lambda_value) -> mpfr_values::Value @@ -143,7 +147,7 @@ namespace cdt::s3_action auto const total = mpfr_values::add(r11, r12); // total = r11+r12 return total; - } // s3_bulk_action_alpha_minus_one() + } // s3_bulk_action_alpha_minus_one_imaginary_coefficient() /// @brief Calculates S3 bulk action for \f$\alpha\f$=1. /// diff --git a/include/Utilities.hpp b/include/Utilities.hpp index 25d3a2574..fd4696893 100644 --- a/include/Utilities.hpp +++ b/include/Utilities.hpp @@ -1715,22 +1715,117 @@ namespace cdt::utilities return generate_random_real(generator, min, max); } // generate_probability() - /// @brief Calculate expected # of points per simplex - /// - /// Usually, there are less vertices than simplices. - /// Here, we throw away a number of simplices that aren't correctly - /// foliated. - /// The exact formula is given by Dwyer: - /// http://link.springer.com/article/10.1007/BF02574694 + /// @brief Calculate the exact number of vertices generated on spherical + /// layers. + /// @details For base population \f$p\f$, initial radius \f$r_0\f$, spacing + /// \f$\Delta r\f$, and \f$T\f$ layers, this evaluates + /// \f$\sum_{i=0}^{T-1}\lfloor p(r_0+i\Delta r)\rfloor\f$ using the same + /// long-double product and truncation boundary as `make_foliated_ball()`. + /// Every per-layer conversion and the total are checked before narrowing. + /// @param points_per_timeslice Base population \f$p\f$ for the first layer + /// @param timeslices Number of spherical layers \f$T\f$ + /// @param initial_radius Radius \f$r_0\f$ of the first layer + /// @param foliation_spacing Radius increment \f$\Delta r\f$ per layer + /// @return Exact number of vertices supplied to CGAL + /// @throws std::invalid_argument if a count, radius, or spacing is not + /// positive and finite + /// @throws std::out_of_range if a layer or the accumulated vertex count + /// cannot be represented by `uint64_t` + [[nodiscard]] inline auto generated_input_vertex_count( + Int_precision const points_per_timeslice, Int_precision const timeslices, + double const initial_radius, double const foliation_spacing) + -> std::uint64_t + { + if (points_per_timeslice <= 0 || timeslices <= 0) + { + throw std::invalid_argument( + "Population and timeslices must both be positive."); + } + if (!std::isfinite(initial_radius) || initial_radius <= 0.0 || + !std::isfinite(foliation_spacing) || foliation_spacing <= 0.0) + { + throw std::invalid_argument( + "Layer radius and spacing must be finite and positive."); + } + + std::uint64_t total{}; + for (Int_precision layer = 0; layer < timeslices; ++layer) + { + auto const radius = + initial_radius + static_cast(layer) * foliation_spacing; + auto const layer_points = + static_cast(points_per_timeslice) * radius; + if (!std::isfinite(layer_points) || layer_points < 0.0L || + layer_points > static_cast( + std::numeric_limits::max())) + { + throw std::out_of_range( + "A spherical layer exceeds the supported point-count range."); + } + auto const narrowed = + static_cast(static_cast(layer_points)); + if (narrowed > std::numeric_limits::max() - total) + { + throw std::out_of_range( + "The generated input-vertex count exceeds uint64_t."); + } + total += narrowed; + } + return total; + } + + /// @brief Return the rigorous finite 3D Delaunay tetrahedron upper bound. + /// @details Evaluates \f$(n^2-3n-2)/2=n(n-3)/2-1\f$ without overflowing. + /// `std::nullopt` means that the exact bound exceeds `uint64_t`; it never + /// means that the triangulation itself has that many tetrahedra. + /// @param vertices Number \f$n\f$ of finite input vertices + /// @return The exact bound, or `std::nullopt` when it exceeds `uint64_t` + /// @see [Cheng, Dey, and Shewchuk, Chapter + /// 4](../REFERENCES.md#three-dimensional-delaunay-complexity) + [[nodiscard]] inline auto delaunay_tetrahedron_upper_bound( + std::uint64_t const vertices) noexcept -> std::optional + { + if (vertices < 4) { return std::uint64_t{0}; } + auto first = vertices; + auto second = vertices - 3; + if (first % 2 == 0) { first /= 2; } + else + { + second /= 2; + } + if (second != 0 && + first > std::numeric_limits::max() / second) + { + return std::nullopt; + } + return first * second - 1; + } + + /// @brief Estimate the base population for the layered spherical generator. + /// @details The estimator is monotone in the requested simplex count. It + /// takes the upper envelope of the historical piecewise schedules, thereby + /// preserving their established scale without their downward jumps at + /// 1,000, 10,000, and 100,000 requested simplices. This is a construction + /// heuristic for CDT++'s nested cospherical inputs, with a 25% population + /// margin to keep boundary layers populated. It is not Dwyer's + /// distribution-specific expectation or an exact topology oracle. + /// @see [Dwyer's expected-complexity + /// result](../REFERENCES.md#random-voronoi-and-delaunay-expected-complexity) /// - /// @param t_dimension Number of dimensions - /// @param t_number_of_simplices Number of desired simplices - /// @param t_number_of_timeslices Number of desired timeslices - /// @returns The number of points per timeslice to obtain - /// the desired number of simplices + /// @param t_dimension Number of dimensions + /// @param t_number_of_simplices Desired post-repair tetrahedron count + /// @param t_number_of_timeslices Number of spherical layers + /// @param initial_radius Radius of the first layer + /// @param foliation_spacing Distance between successive layers + /// @returns Estimated base population parameter \f$p\f$ + /// @throws std::invalid_argument if the dimension is not three or a count, + /// radius, or spacing is not positive and finite + /// @throws std::out_of_range if the requested estimate or generated vertex + /// population is not representable [[nodiscard]] inline auto expected_points_per_timeslice( Int_precision const t_dimension, Int_precision t_number_of_simplices, - Int_precision t_number_of_timeslices) + Int_precision t_number_of_timeslices, double const initial_radius = 1.0, + double const foliation_spacing = 1.0) { #ifndef NDEBUG spdlog::debug("{} simplices on {} timeslices desired.\n", @@ -1747,42 +1842,77 @@ namespace cdt::utilities throw std::invalid_argument( "Simplices and timeslices must both be positive."); } - - auto const simplices_per_timeslice = - t_number_of_simplices / t_number_of_timeslices; - // Avoid segfaults for small values - if (t_number_of_simplices == t_number_of_timeslices) + if (!std::isfinite(initial_radius) || initial_radius <= 0.0 || + !std::isfinite(foliation_spacing) || foliation_spacing <= 0.0) { - return 2 * simplices_per_timeslice; + throw std::invalid_argument( + "Layer radius and spacing must be finite and positive."); } - if (t_number_of_simplices < 1000) // NOLINT + + constexpr auto minimum_population = Int_precision{8}; + auto const minimum = t_number_of_simplices <= t_number_of_timeslices + ? Int_precision{2} + : minimum_population; + auto const last_radius = + static_cast(initial_radius) + + static_cast(t_number_of_timeslices - 1) * + foliation_spacing; + if (!std::isfinite(last_radius)) { - return static_cast( - 0.4L * // NOLINT - static_cast(simplices_per_timeslice)); + throw std::out_of_range( + "The final foliation radius exceeds the supported range."); } - if (t_number_of_simplices < 10000) // NOLINT + auto const maximum_by_radius = + static_cast(std::numeric_limits::max()) / + last_radius; + auto const maximum = + maximum_by_radius >= static_cast( + std::numeric_limits::max()) + ? std::numeric_limits::max() + : static_cast(maximum_by_radius); + if (maximum < minimum) { - return static_cast( - 0.2L * // NOLINT - static_cast(simplices_per_timeslice)); + throw std::out_of_range( + "Foliation radii leave no supported base population."); } - if (t_number_of_simplices < 100000) // NOLINT + + auto const simplices = static_cast(t_number_of_simplices); + auto const timeslices = static_cast(t_number_of_timeslices); + auto const scaled_floor = [timeslices]( + std::uint64_t const bounded_simplices, + std::uint64_t const numerator, + std::uint64_t const denominator) { + return bounded_simplices * numerator / (timeslices * denominator); + }; + auto const historical_upper_envelope = std::max( + {scaled_floor(std::min(simplices, std::uint64_t{1'000}), 2, 5), + scaled_floor(std::min(simplices, std::uint64_t{10'000}), 1, 5), + scaled_floor(std::min(simplices, std::uint64_t{100'000}), 3, 20), + scaled_floor(simplices, 1, 10)}); + auto const construction_margin = (5 * historical_upper_envelope + 3) / 4; + auto const estimated = + std::max(static_cast(minimum), construction_margin); + if (estimated > static_cast(maximum)) { - return static_cast( - 0.15L * // NOLINT - static_cast(simplices_per_timeslice)); + throw std::out_of_range( + "Requested simplex estimate exceeds the supported population."); } - - return static_cast( - 0.1L * static_cast(simplices_per_timeslice)); // NOLINT - + auto const population = static_cast(estimated); + static_cast(generated_input_vertex_count( + population, t_number_of_timeslices, initial_radius, foliation_spacing)); + return population; } // expected_points_per_timeslice struct Generated_population_bounds { - Int_precision points_per_timeslice; - long double last_layer_points; + /// Base population used to construct the spherical layers. + Int_precision points_per_timeslice; + /// Untruncated point-count expression for the final spherical layer. + long double last_layer_points; + /// Exact total number of vertices supplied to CGAL. + std::uint64_t input_vertices; + /// Rigorous finite 3D Delaunay bound, if representable by `uint64_t`. + std::optional tetrahedra_upper_bound; }; /// @brief Calculate the generated point count and its upper bound @@ -1791,19 +1921,32 @@ namespace cdt::utilities /// @param timeslices Number of desired timeslices /// @param initial_radius Radius of the first timeslice /// @param foliation_spacing Distance between successive timeslices - /// @return Points per timeslice and the last-layer point count + /// @return Base population, last-layer point expression, exact input-vertex + /// count, and representable finite 3D Delaunay tetrahedron bound + /// @throws std::invalid_argument if the dimension is not three or a count, + /// radius, or spacing is not positive and finite + /// @throws std::out_of_range if the requested estimate or generated vertex + /// population is not representable [[nodiscard]] inline auto generated_population_bounds( Int_precision const dimension, Int_precision const simplices, Int_precision const timeslices, double const initial_radius, double const foliation_spacing) -> Generated_population_bounds { - auto const points_per_timeslice = - expected_points_per_timeslice(dimension, simplices, timeslices); + auto const points_per_timeslice = expected_points_per_timeslice( + dimension, simplices, timeslices, initial_radius, foliation_spacing); auto const last_radius = static_cast(initial_radius) + static_cast(timeslices - 1) * foliation_spacing; - return {points_per_timeslice, - static_cast(points_per_timeslice) * last_radius}; + auto const input_vertices = generated_input_vertex_count( + points_per_timeslice, timeslices, initial_radius, foliation_spacing); + return { + .points_per_timeslice = points_per_timeslice, + .last_layer_points = + static_cast(points_per_timeslice) * last_radius, + .input_vertices = input_vertices, + .tetrahedra_upper_bound = + delaunay_tetrahedron_upper_bound(input_vertices), + }; } /// @brief Convert Gmpzf into a double diff --git a/pyproject.toml b/pyproject.toml index 9e3e880e3..1be103d28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ source-exclude = [ [dependency-groups] dev = [ "clang-format==22.1.8", + "jsonschema==4.25.1", "pyyaml==6.0.3", "ruff==0.16.0", "semgrep==1.171.0", diff --git a/reference/README.md b/reference/README.md new file mode 100644 index 000000000..a0daec08a --- /dev/null +++ b/reference/README.md @@ -0,0 +1,119 @@ +# CDT++ reference fixture package + +This directory is the bounded behavior-level oracle published for issue #94 +and for the Rust `causal-triangulations` implementation. CDT++ is an +independent implementation under comparison, not presumed ground truth. A +discrepancy is an investigation target until the protocol or one of the +implementations explains it. + +## Package layout + +- `schema/fixture-v1.schema.json` defines the comparison protocol. +- `schema/result-v1.schema.json` defines canonical C++ topology and action + output. +- `schema/run-manifest-v1.schema.json` defines run and artifact provenance. +- `fixtures/v1/protocol.json` declares ordering, units, tolerances, exact + fields, allowed divergence, move sites, action parameters, proposal + probabilities, acceptance variates, persistence checks, and the bounded run. +- `raw/v1/cpp-reference.json` is direct output from the C++ fixture executable, + before Python or notebook processing. +- `raw/v1/persistence-v1.off` and its `.off.meta` sidecar are the smallest + committed persistence round trip. +- `raw/v1/end-to-end.txt` is the bounded one-pass C++ run. +- `raw/v1/scaling-threads-*.txt` preserves the matched #88 one-, two-, and + four-thread records, including every raw sample. +- `manifests/v1/` records source, toolchain, build, host, thread limits, seeds, + command lines, and SHA-256 artifact checksums. + +Run the complete offline package validator with: + +```console +just reference-check +``` + +It applies the complete JSON Schema Draft 2020-12 contracts, then checks +canonical identifiers and ordering, +incidence, reciprocal adjacency, causal edge and simplex classification, +f-vectors and Euler relations, all five move sites and deltas, raw proposal +domains, independent closed-form actions and Metropolis-Hastings probabilities, +deterministic decisions, persistence size and FNV-1a integrity, bounded-run +command provenance and declared f-vector band, matched scaling parameters, raw +sample counts, and manifest SHA-256 values. + +## Rust consumption + +Read `fixtures/v1/protocol.json` first. Entity arrays are canonical: + +1. vertices sort by `(time, x, y, z)`; +2. edges, facets, and cells sort by their sorted vertex-id tuple; +3. dense `vNN`, `eNN`, `fNN`, and `cNN` identifiers follow array order; and +4. adjacent cell ids sort lexicographically. + +Compare topology, incidence, adjacency, foliation labels, simplex and edge +types, f-vectors, move sites, integer deltas, and accept/reject decisions +exactly. Compare coordinates, actions, deltas, and probabilities with the +named quantity-specific absolute-plus-relative tolerance. Do not apply one +repository-wide percentage. + +Transition fixtures provide the raw proposal site and acceptance variate. +They therefore test proposal preparation, action delta, Hastings factor, and +commit/reject behavior without requiring the C++ and Rust implementations to +share an RNG engine, allocation order, or container iteration order. + +## Regeneration + +`reference-fixtures` is a quick diagnostic that builds and prints only +`cpp-reference.json`: + +```console +just reference-fixtures +``` + +The archival workflow is: + +```console +just reference-regenerate +``` + +It refuses a dirty worktree, builds the sequential and parallel configurations, +and regenerates all four artifact families before publishing them: + +- the canonical topology/action JSON from `CDT_reference_fixture`; +- the bounded run transcript from `cdt`; +- the OFF payload and metadata sidecar from `initialize`; and +- the matched one-, two-, and four-thread records from `CDT_cgal_benchmark`. + +The generator updates both manifests, records the exact producer command for +every file under `raw/v1/`, and refreshes all SHA-256 values. The recipe then +runs the offline validator and `reference-archive-check`, which requires every +raw record and manifest to name the same clean Git commit. +The v1 manifest templates are macOS-arm64-specific, so regeneration refuses a +different host instead of publishing records under a misleading platform name. + +Regeneration is a review operation. Explain every exact-field change. A +randomized CGAL f-vector, wall-clock sample, or benchmark checksum change is +diagnostic data, not automatically a defect; deterministic minimal fixtures +and transition results must remain exact. + +The comprehensive `just ci` gate additionally rebuilds the C++ fixture +executable and compares its canonical states and actions with the committed raw +record. It reuses the offline validation performed by `just check`; the +generated-only step compares exact topology and metadata while applying the +protocol’s named tolerances to coordinates and action values. Host, compiler, +and source-revision provenance are intentionally excluded from that equality +check. + +The committed macOS records contain a `-dirty` revision because they document +the issue #94 implementation while it is under review. Regenerate them from +the resulting clean commit before the archival v1.0.0 tag; the archival check +will fail until that handoff is complete. + +## Scope limits + +The package does not claim exact cross-language Monte Carlo trajectories, +exact fresh topology for nested cospherical CGAL inputs, a general performance +study, or a calibrated phase-distribution equivalence result. The spherical +population value is explicitly a monotone construction heuristic; randomized +post-repair simplex counts are implementation-specific. The rigorous +quadratic tetrahedron bound is retained only for safety and preflight +reasoning. diff --git a/reference/fixtures/v1/protocol.json b/reference/fixtures/v1/protocol.json new file mode 100644 index 000000000..b77dc3b9f --- /dev/null +++ b/reference/fixtures/v1/protocol.json @@ -0,0 +1,332 @@ +{ + "schema_version": 1, + "fixture_set": "cdt-reference-v1", + "canonicalization": { + "vertices": "ascending (time, x, y, z), then vNN", + "edges": "ascending sorted vertex-id pair, then eNN", + "facets": "ascending sorted vertex-id triple, then fNN", + "cells": "ascending sorted vertex-id quadruple, then cNN", + "adjacency": "ascending cell ids sharing exactly one three-vertex facet", + "floating_point": "JSON numbers use max_digits10; compare with the named quantity tolerance" + }, + "units": { + "coordinates": "initial_radius", + "time": "timeslice", + "regge_action": "dimensionless", + "action_delta": "dimensionless", + "probability": "unit_interval", + "benchmark_duration": "nanoseconds" + }, + "tolerances": { + "coordinates": { + "absolute": 2.220446049250313e-15, + "relative": 2.220446049250313e-15, + "rationale": "Ten binary64 epsilon units cover cross-libm construction of the declared algebraic coordinates." + }, + "regge_action_closed_form": { + "absolute": 1.4210854715202004e-14, + "relative": 1.4210854715202004e-14, + "rationale": "Sixty-four binary64 epsilon units bound the independent libm-to-256-bit-MPFR comparison boundary." + }, + "acceptance_probability": { + "absolute": 1e-15, + "relative": 1e-15, + "rationale": "The fixtures use small closed forms; this covers binary64 publication without replacing the MPFR comparison." + } + }, + "comparison_policy": { + "exact": [ + "entity ids and ordering", + "incidence", + "adjacency", + "time labels", + "edge and simplex types", + "f-vectors", + "move sites", + "integer deltas", + "accept or reject decisions", + "persistence topology fingerprint" + ], + "numerical": { + "coordinates": "coordinates", + "actions": "regge_action_closed_form", + "action_deltas": "regge_action_closed_form", + "probabilities": "acceptance_probability" + }, + "implementation_specific": [ + "compiler and standard-library spelling", + "CGAL triangulation handles", + "allocation order", + "wall-clock duration", + "randomized cospherical tetrahedralization" + ], + "unsupported": [ + "exact cross-language Monte Carlo trajectory without a shared RNG and distribution mapping", + "exact simplex counts from randomized CGAL construction", + "treating CDT++ as presumed scientific ground truth" + ] + }, + "state_fixtures": [ + { + "id": "causal-simplex", + "source": "four explicitly labeled causal vertices", + "expected_state": "causal-simplex" + }, + { + "id": "causality-filter", + "source": "six explicitly labeled vertices in causality-filter-before; remove the time-3 vertex at (0,3,0)", + "input_state": "causality-filter-before", + "removed_vertex": { + "position": [ + 0, + 3, + 0 + ], + "time": 3 + }, + "expected_state": "causality-filter-after" + }, + { + "id": "move-23-cavity", + "source": "five explicit vertices", + "expected_state": "move-23-before" + }, + { + "id": "move-26-cavity", + "source": "five explicit vertices", + "expected_state": "move-26-before" + }, + { + "id": "move-44-diamond", + "source": "six explicit vertices", + "expected_state": "move-44-before" + } + ], + "transitions": [ + { + "id": "move-23-accepted", + "move": "2-3", + "before": "move-23-before", + "proposed": "move-23-after", + "site": { + "kind": "2-2-cell", + "entity": "c01", + "vertices": [ + "v01", + "v02", + "v03", + "v04" + ] + }, + "parameters": { + "alpha": 1, + "k": 0, + "lambda": 1 + }, + "proposal_probability": 0.2, + "reverse_probability": 0.03333333333333333, + "action_delta": 0.2041241452319315, + "acceptance_probability": 0.20440840032919283, + "acceptance_variate": 0.1, + "accepted": true, + "committed": "move-23-after" + }, + { + "id": "move-23-rejected", + "move": "2-3", + "before": "move-23-before", + "proposed": "move-23-after", + "site": { + "kind": "2-2-cell", + "entity": "c01", + "vertices": [ + "v01", + "v02", + "v03", + "v04" + ] + }, + "parameters": { + "alpha": 1, + "k": 0, + "lambda": 1 + }, + "proposal_probability": 0.2, + "reverse_probability": 0.03333333333333333, + "action_delta": 0.2041241452319315, + "acceptance_probability": 0.20440840032919283, + "acceptance_variate": 0.9, + "accepted": false, + "committed": "move-23-before" + }, + { + "id": "move-32-accepted", + "move": "3-2", + "before": "move-23-after", + "proposed": "move-23-before", + "site": { + "kind": "timelike-edge", + "entity": "e03", + "vertices": [ + "v00", + "v04" + ] + }, + "parameters": { + "alpha": 1, + "k": 0, + "lambda": 1 + }, + "proposal_probability": 0.03333333333333333, + "reverse_probability": 0.2, + "action_delta": -0.2041241452319315, + "acceptance_probability": 1, + "acceptance_variate": 0.9, + "accepted": true, + "committed": "move-23-before" + }, + { + "id": "move-26-accepted", + "move": "2-6", + "before": "move-26-before", + "proposed": "move-26-after", + "site": { + "kind": "1-3-cell", + "entity": "c00", + "vertices": [ + "v00", + "v01", + "v02", + "v03" + ], + "shared_spacelike_facet": "f03" + }, + "parameters": { + "alpha": 1, + "k": 0, + "lambda": 1 + }, + "proposal_probability": 0.2, + "reverse_probability": 0.03333333333333333, + "action_delta": 0.6666666666666666, + "acceptance_probability": 0.3246223401757793, + "acceptance_variate": 0.3, + "accepted": true, + "committed": "move-26-after" + }, + { + "id": "move-62-accepted", + "move": "6-2", + "before": "move-26-after", + "proposed": "move-26-before", + "site": { + "kind": "degree-five-vertex", + "entity": "v03", + "vertices": [ + "v03" + ] + }, + "parameters": { + "alpha": 1, + "k": 0, + "lambda": 1 + }, + "proposal_probability": 0.03333333333333333, + "reverse_probability": 0.2, + "action_delta": -0.6666666666666666, + "acceptance_probability": 1, + "acceptance_variate": 0.9, + "accepted": true, + "committed": "move-26-before" + }, + { + "id": "move-44-accepted", + "move": "4-4", + "before": "move-44-before", + "proposed": "move-44-after", + "site": { + "kind": "spacelike-edge", + "entity": "e07", + "vertices": [ + "v02", + "v03" + ], + "reverse_vertices": [ + "v01", + "v04" + ] + }, + "parameters": { + "alpha": 1, + "k": 0, + "lambda": 1 + }, + "proposal_probability": 0.04, + "reverse_probability": 0.04, + "action_delta": 0, + "acceptance_probability": 1, + "acceptance_variate": 0.9, + "accepted": true, + "committed": "move-44-after" + } + ], + "persistence": { + "id": "bounded-roundtrip-v1", + "command": [ + "ctest", + "--test-dir", + "out/build/reference", + "-R", + "^(cdt|initialize)$", + "--output-on-failure" + ], + "expected": [ + "payload checksum verified before parse", + "canonical placement and topology fingerprints unchanged", + "causal metadata and f-vector unchanged", + "trailing or malformed data rejected" + ] + }, + "bounded_run": { + "id": "bounded-end-to-end-v1", + "command": [ + "out/build/reference/src/cdt", + "-s", + "-n2", + "-t2", + "-a1", + "-k0", + "-l1", + "-p1", + "-c1", + "--seed", + "92", + "--threads", + "1", + "--no-output" + ], + "exact": [ + "validated command-line parameters", + "root seed and named streams", + "transition accounting identities", + "valid final causal manifold" + ], + "bands": { + "randomized_cgal_f_vector": { + "minimum": [ + 4, + 6, + 4, + 1 + ], + "maximum": [ + 6, + 15, + 20, + 8 + ], + "rationale": "The run generates at most six vertices. The lower endpoint is one tetrahedron; the upper endpoints use complete incidence bounds and the finite three-dimensional Delaunay tetrahedron bound." + } + } + } +} diff --git a/reference/manifests/v1/macos-arm64.json b/reference/manifests/v1/macos-arm64.json new file mode 100644 index 000000000..e4e31cfdf --- /dev/null +++ b/reference/manifests/v1/macos-arm64.json @@ -0,0 +1,167 @@ +{ + "schema_version": 1, + "run_id": "cdt-reference-v1-macos-arm64-20260725", + "recorded_at_utc": "2026-07-25T06:21:48Z", + "implementation": { + "name": "CDT-plusplus", + "version": "1.0.0-rc2", + "source_revision": "49dbce290ee9cc9c0430893e11caa84cbaeaffee-dirty", + "note": "The dirty marker denotes the issue #94 fixture implementation under review; regenerate from the resulting clean commit before archival tagging." + }, + "toolchain": { + "compiler": "AppleClang 21.0.0.21000101", + "cxx_standard": 23, + "standard_library": "libc++-210106", + "cmake": "4.4.0", + "cgal": "6.2-I-900", + "mpfr": "4.2.2" + }, + "build": { + "profile": "RelWithDebInfo", + "preset": "reference", + "parallel_triangulation": false + }, + "host": { + "operating_system": "Darwin", + "architecture": "arm64", + "logical_threads": 16 + }, + "thread_count": 1, + "seeds": { + "causal_construction": null, + "move_23_32": 10623, + "move_26_62": 10626, + "move_44": 10644, + "bounded_end_to_end_root": 92 + }, + "fixture_version": 1, + "commands": [ + { + "id": "canonical-fixture", + "artifacts": [ + "reference/raw/v1/cpp-reference.json" + ], + "command_line": [ + "out/build/reference/tests/CDT_reference_fixture" + ] + }, + { + "id": "bounded-end-to-end", + "artifacts": [ + "reference/raw/v1/end-to-end.txt" + ], + "command_line": [ + "out/build/reference/src/cdt", + "-s", + "-n2", + "-t2", + "-a1", + "-k0", + "-l1", + "-p1", + "-c1", + "--seed", + "92", + "--threads", + "1", + "--no-output" + ] + }, + { + "id": "persistence-roundtrip", + "artifacts": [ + "reference/raw/v1/persistence-v1.off", + "reference/raw/v1/persistence-v1.off.meta" + ], + "command_line": [ + "out/build/reference/src/initialize", + "-s", + "-n2", + "-t2", + "-o", + "--seed", + "92", + "--threads", + "1" + ] + } + ], + "artifacts": [ + { + "path": "reference/raw/v1/cpp-reference.json", + "media_type": "application/json", + "sha256": "96c6890de3acd930956fcd1a97637d25d3bd73a14a2bbfc04f37f10153269cf4" + }, + { + "path": "reference/raw/v1/end-to-end.txt", + "media_type": "text/plain", + "sha256": "d7e24bfe101c20a3a35f53fee189dfda31bae2c029f3d98776fd8ad8cf16b3ea" + }, + { + "path": "reference/raw/v1/persistence-v1.off", + "media_type": "text/plain", + "sha256": "7ed6e0b85f7d31948605bcc9489e036468dc3fafb5e3dc7933ea8fcbbc95b35a" + }, + { + "path": "reference/raw/v1/persistence-v1.off.meta", + "media_type": "text/plain", + "sha256": "da63ba1b0956c5aedbe602a3de625868f433cc23734e29a2ba23f5de50edcbc5" + }, + { + "path": "reference/fixtures/v1/protocol.json", + "media_type": "application/json", + "sha256": "d0d7bc91c35e3a1ee36e7fb18412d6bdb1b0dcd23f9579912e16f5913b5f13aa" + }, + { + "path": "reference/schema/fixture-v1.schema.json", + "media_type": "application/schema+json", + "sha256": "ac574dd4821562d54e6ea0f46087cec0461bd01e091c04d364c9511e901e4215" + }, + { + "path": "reference/schema/result-v1.schema.json", + "media_type": "application/schema+json", + "sha256": "8b7074b44cc98a70bae3612a748ecef24d05f16229343ba4505b0d453bf02977" + }, + { + "path": "reference/schema/run-manifest-v1.schema.json", + "media_type": "application/schema+json", + "sha256": "38c95bc5b0b2cadf726d9cba29eb820dec8ebb584e4adf8a86b2ac2c6151fd38" + } + ], + "verification": { + "targeted_unit_tests": { + "status": "passed", + "command": [ + "out/build/reference/tests/CDT_unit_tests", + "--test-case=*Metropolis-Hastings proposal and acceptance ratios*,*Metropolis proposal domains match the sampled raw sites*,*Calculate the bulk action on S3 triangulations*,*Bulk action rejects invalid physical parameters*,*Expected points per timeslice*" + ] + }, + "reference_smoke": { + "status": "passed", + "command": [ + "ctest", + "--preset", + "reference-smoke", + "--output-on-failure" + ] + }, + "reference_validator": { + "status": "passed", + "command": [ + "just", + "reference-check" + ] + }, + "address_and_undefined_behavior_sanitizers": { + "status": "base revision passed; current issue #94 diff requires PR CI", + "command": [ + "just", + "sanitize", + "asan" + ], + "base_revision": "49dbce290ee9cc9c0430893e11caa84cbaeaffee", + "base_run": "https://github.com/acgetchell/CDT-plusplus/actions/runs/30146661161", + "reason": "The repository's combined ASan/UBSan preset is deliberately supported on Linux only; the linked main-branch run passed, and the unpushed issue #94 diff cannot truthfully be marked covered until its PR workflow runs." + } + } +} diff --git a/reference/manifests/v1/scaling-macos-arm64.json b/reference/manifests/v1/scaling-macos-arm64.json new file mode 100644 index 000000000..bff4c44e0 --- /dev/null +++ b/reference/manifests/v1/scaling-macos-arm64.json @@ -0,0 +1,118 @@ +{ + "schema_version": 1, + "run_id": "cdt-scaling-v1-macos-arm64-20260725", + "recorded_at_utc": "2026-07-25T06:25:37Z", + "implementation": { + "name": "CDT-plusplus", + "version": "1.0.0-rc2", + "source_revision": "49dbce290ee9cc9c0430893e11caa84cbaeaffee-dirty", + "note": "The dirty marker denotes the issue #94 population and manifest changes under review." + }, + "toolchain": { + "compiler": "AppleClang 21.0.0.21000101", + "cxx_standard": 23, + "standard_library": "libc++-210106", + "cmake": "4.4.0", + "cgal": "6.2-I-900", + "tbb": "2023.0.0" + }, + "build": { + "profile": "RelWithDebInfo", + "preset": "parallel", + "parallel_triangulation": true, + "thread_counts": [ + 1, + 2, + 4 + ] + }, + "host": { + "operating_system": "Darwin", + "architecture": "arm64", + "logical_threads": 16, + "comparison_constraint": "All samples were recorded sequentially on the same host, binary, fixture, dependency baseline, warm-up policy, and measurement protocol." + }, + "thread_count": 4, + "seeds": { + "root": 102, + "initialization_stream": 0, + "transition_stream": 1 + }, + "fixture_version": 1, + "commands": [ + { + "id": "scaling-threads-1", + "artifacts": [ + "reference/raw/v1/scaling-threads-1.txt" + ], + "command_line": [ + "out/build/parallel/tests/CDT_cgal_benchmark", + "640", + "5", + "50", + "1", + "1" + ] + }, + { + "id": "scaling-threads-2", + "artifacts": [ + "reference/raw/v1/scaling-threads-2.txt" + ], + "command_line": [ + "out/build/parallel/tests/CDT_cgal_benchmark", + "640", + "5", + "50", + "2", + "1" + ] + }, + { + "id": "scaling-threads-4", + "artifacts": [ + "reference/raw/v1/scaling-threads-4.txt" + ], + "command_line": [ + "out/build/parallel/tests/CDT_cgal_benchmark", + "640", + "5", + "50", + "4", + "1" + ] + } + ], + "artifacts": [ + { + "path": "reference/raw/v1/scaling-threads-1.txt", + "media_type": "text/plain", + "sha256": "5a81902d77313be83cdb08e4c784117914e50dd3637fc7835463433b49cb77b7" + }, + { + "path": "reference/raw/v1/scaling-threads-2.txt", + "media_type": "text/plain", + "sha256": "c2a6fb57f623537b63ae6dbde64c06d4c9509949da96c4ac56c12f3bcbc69aab" + }, + { + "path": "reference/raw/v1/scaling-threads-4.txt", + "media_type": "text/plain", + "sha256": "8cfc83922a562d77c22ba114a7fdc2f7bf47e1a7097fc94b8c6dcb81ad2b645e" + } + ], + "verification": { + "correctness_gate": { + "status": "passed", + "command": [ + "ctest", + "--preset", + "parallel-smoke", + "--output-on-failure" + ] + }, + "interpretation": { + "randomized_fields": "Final CGAL f-vectors and checksums may differ across thread limits and are recorded rather than treated as exact topology oracles.", + "timings": "Raw durations are host-local evidence only; this three-point record is not a general performance claim." + } + } +} diff --git a/reference/raw/v1/cpp-reference.json b/reference/raw/v1/cpp-reference.json new file mode 100644 index 000000000..83ef730c2 --- /dev/null +++ b/reference/raw/v1/cpp-reference.json @@ -0,0 +1 @@ +{"schema":"cdt-reference-raw-v1","implementation":{"name":"CDT-plusplus","version":"1.0.0-rc2","revision":"49dbce290ee9cc9c0430893e11caa84cbaeaffee-dirty","compiler":"AppleClang 21.0.0.21000101","build_profile":"RelWithDebInfo","operating_system":"Darwin","hardware":"arm64","logical_threads":16,"cxx_standard":23,"standard_library":"libc++-210106","cgal_version":"6.2-I-900"},"states":[{"id":"causal-simplex","coordinate_units":"initial_radius","time_units":"timeslice","time_bounds":[1,2],"f_vector":[4,6,4,1],"vertices":[{"id":"v00","position":[0,0,1],"time":1},{"id":"v01","position":[0,1,0],"time":1},{"id":"v02","position":[1,0,0],"time":1},{"id":"v03","position":[1.1547005383792515,1.1547005383792515,1.1547005383792515],"time":2}],"edges":[{"id":"e00","vertices":["v00","v01"],"type":"spacelike"},{"id":"e01","vertices":["v00","v02"],"type":"spacelike"},{"id":"e02","vertices":["v00","v03"],"type":"timelike"},{"id":"e03","vertices":["v01","v02"],"type":"spacelike"},{"id":"e04","vertices":["v01","v03"],"type":"timelike"},{"id":"e05","vertices":["v02","v03"],"type":"timelike"}],"facets":[{"id":"f00","vertices":["v00","v01","v02"],"spacelike":true,"time":1},{"id":"f01","vertices":["v00","v01","v03"],"spacelike":false},{"id":"f02","vertices":["v00","v02","v03"],"spacelike":false},{"id":"f03","vertices":["v01","v02","v03"],"spacelike":false}],"cells":[{"id":"c00","vertices":["v00","v01","v02","v03"],"type":"3-1","adjacent_cells":[]}]},{"id":"causality-filter-before","coordinate_units":"initial_radius","time_units":"timeslice","time_bounds":[1,3],"f_vector":[6,12,10,3],"vertices":[{"id":"v00","position":[0,0,1],"time":1},{"id":"v01","position":[0,1,0],"time":1},{"id":"v02","position":[1,0,0],"time":1},{"id":"v03","position":[0,0,2],"time":2},{"id":"v04","position":[2,0,0],"time":2},{"id":"v05","position":[0,3,0],"time":3}],"edges":[{"id":"e00","vertices":["v00","v01"],"type":"spacelike"},{"id":"e01","vertices":["v00","v02"],"type":"spacelike"},{"id":"e02","vertices":["v00","v03"],"type":"timelike"},{"id":"e03","vertices":["v01","v02"],"type":"spacelike"},{"id":"e04","vertices":["v01","v03"],"type":"timelike"},{"id":"e05","vertices":["v01","v04"],"type":"timelike"},{"id":"e06","vertices":["v01","v05"],"type":"timelike"},{"id":"e07","vertices":["v02","v03"],"type":"timelike"},{"id":"e08","vertices":["v02","v04"],"type":"timelike"},{"id":"e09","vertices":["v03","v04"],"type":"spacelike"},{"id":"e10","vertices":["v03","v05"],"type":"timelike"},{"id":"e11","vertices":["v04","v05"],"type":"timelike"}],"facets":[{"id":"f00","vertices":["v00","v01","v02"],"spacelike":true,"time":1},{"id":"f01","vertices":["v00","v01","v03"],"spacelike":false},{"id":"f02","vertices":["v00","v02","v03"],"spacelike":false},{"id":"f03","vertices":["v01","v02","v03"],"spacelike":false},{"id":"f04","vertices":["v01","v02","v04"],"spacelike":false},{"id":"f05","vertices":["v01","v03","v04"],"spacelike":false},{"id":"f06","vertices":["v01","v03","v05"],"spacelike":false},{"id":"f07","vertices":["v01","v04","v05"],"spacelike":false},{"id":"f08","vertices":["v02","v03","v04"],"spacelike":false},{"id":"f09","vertices":["v03","v04","v05"],"spacelike":false}],"cells":[{"id":"c00","vertices":["v00","v01","v02","v03"],"type":"3-1","adjacent_cells":["c01"]},{"id":"c01","vertices":["v01","v02","v03","v04"],"type":"2-2","adjacent_cells":["c00","c02"]},{"id":"c02","vertices":["v01","v03","v04","v05"],"type":"acausal","adjacent_cells":["c01"]}]},{"id":"causality-filter-after","coordinate_units":"initial_radius","time_units":"timeslice","time_bounds":[1,2],"f_vector":[5,9,7,2],"vertices":[{"id":"v00","position":[0,0,1],"time":1},{"id":"v01","position":[0,1,0],"time":1},{"id":"v02","position":[1,0,0],"time":1},{"id":"v03","position":[0,0,2],"time":2},{"id":"v04","position":[2,0,0],"time":2}],"edges":[{"id":"e00","vertices":["v00","v01"],"type":"spacelike"},{"id":"e01","vertices":["v00","v02"],"type":"spacelike"},{"id":"e02","vertices":["v00","v03"],"type":"timelike"},{"id":"e03","vertices":["v01","v02"],"type":"spacelike"},{"id":"e04","vertices":["v01","v03"],"type":"timelike"},{"id":"e05","vertices":["v01","v04"],"type":"timelike"},{"id":"e06","vertices":["v02","v03"],"type":"timelike"},{"id":"e07","vertices":["v02","v04"],"type":"timelike"},{"id":"e08","vertices":["v03","v04"],"type":"spacelike"}],"facets":[{"id":"f00","vertices":["v00","v01","v02"],"spacelike":true,"time":1},{"id":"f01","vertices":["v00","v01","v03"],"spacelike":false},{"id":"f02","vertices":["v00","v02","v03"],"spacelike":false},{"id":"f03","vertices":["v01","v02","v03"],"spacelike":false},{"id":"f04","vertices":["v01","v02","v04"],"spacelike":false},{"id":"f05","vertices":["v01","v03","v04"],"spacelike":false},{"id":"f06","vertices":["v02","v03","v04"],"spacelike":false}],"cells":[{"id":"c00","vertices":["v00","v01","v02","v03"],"type":"3-1","adjacent_cells":["c01"]},{"id":"c01","vertices":["v01","v02","v03","v04"],"type":"2-2","adjacent_cells":["c00"]}]},{"id":"move-23-before","coordinate_units":"initial_radius","time_units":"timeslice","time_bounds":[1,2],"f_vector":[5,9,7,2],"vertices":[{"id":"v00","position":[0,0,1],"time":1},{"id":"v01","position":[0,1,0],"time":1},{"id":"v02","position":[1,0,0],"time":1},{"id":"v03","position":[1.1547005383792515,1.1547005383792515,1.1547005383792515],"time":2},{"id":"v04","position":[1.4142135623730951,1.4142135623730951,0],"time":2}],"edges":[{"id":"e00","vertices":["v00","v01"],"type":"spacelike"},{"id":"e01","vertices":["v00","v02"],"type":"spacelike"},{"id":"e02","vertices":["v00","v03"],"type":"timelike"},{"id":"e03","vertices":["v01","v02"],"type":"spacelike"},{"id":"e04","vertices":["v01","v03"],"type":"timelike"},{"id":"e05","vertices":["v01","v04"],"type":"timelike"},{"id":"e06","vertices":["v02","v03"],"type":"timelike"},{"id":"e07","vertices":["v02","v04"],"type":"timelike"},{"id":"e08","vertices":["v03","v04"],"type":"spacelike"}],"facets":[{"id":"f00","vertices":["v00","v01","v02"],"spacelike":true,"time":1},{"id":"f01","vertices":["v00","v01","v03"],"spacelike":false},{"id":"f02","vertices":["v00","v02","v03"],"spacelike":false},{"id":"f03","vertices":["v01","v02","v03"],"spacelike":false},{"id":"f04","vertices":["v01","v02","v04"],"spacelike":false},{"id":"f05","vertices":["v01","v03","v04"],"spacelike":false},{"id":"f06","vertices":["v02","v03","v04"],"spacelike":false}],"cells":[{"id":"c00","vertices":["v00","v01","v02","v03"],"type":"3-1","adjacent_cells":["c01"]},{"id":"c01","vertices":["v01","v02","v03","v04"],"type":"2-2","adjacent_cells":["c00"]}]},{"id":"move-23-after","coordinate_units":"initial_radius","time_units":"timeslice","time_bounds":[1,2],"f_vector":[5,10,9,3],"vertices":[{"id":"v00","position":[0,0,1],"time":1},{"id":"v01","position":[0,1,0],"time":1},{"id":"v02","position":[1,0,0],"time":1},{"id":"v03","position":[1.1547005383792515,1.1547005383792515,1.1547005383792515],"time":2},{"id":"v04","position":[1.4142135623730951,1.4142135623730951,0],"time":2}],"edges":[{"id":"e00","vertices":["v00","v01"],"type":"spacelike"},{"id":"e01","vertices":["v00","v02"],"type":"spacelike"},{"id":"e02","vertices":["v00","v03"],"type":"timelike"},{"id":"e03","vertices":["v00","v04"],"type":"timelike"},{"id":"e04","vertices":["v01","v02"],"type":"spacelike"},{"id":"e05","vertices":["v01","v03"],"type":"timelike"},{"id":"e06","vertices":["v01","v04"],"type":"timelike"},{"id":"e07","vertices":["v02","v03"],"type":"timelike"},{"id":"e08","vertices":["v02","v04"],"type":"timelike"},{"id":"e09","vertices":["v03","v04"],"type":"spacelike"}],"facets":[{"id":"f00","vertices":["v00","v01","v02"],"spacelike":true,"time":1},{"id":"f01","vertices":["v00","v01","v03"],"spacelike":false},{"id":"f02","vertices":["v00","v01","v04"],"spacelike":false},{"id":"f03","vertices":["v00","v02","v03"],"spacelike":false},{"id":"f04","vertices":["v00","v02","v04"],"spacelike":false},{"id":"f05","vertices":["v00","v03","v04"],"spacelike":false},{"id":"f06","vertices":["v01","v02","v04"],"spacelike":false},{"id":"f07","vertices":["v01","v03","v04"],"spacelike":false},{"id":"f08","vertices":["v02","v03","v04"],"spacelike":false}],"cells":[{"id":"c00","vertices":["v00","v01","v02","v04"],"type":"3-1","adjacent_cells":["c01","c02"]},{"id":"c01","vertices":["v00","v01","v03","v04"],"type":"2-2","adjacent_cells":["c00","c02"]},{"id":"c02","vertices":["v00","v02","v03","v04"],"type":"2-2","adjacent_cells":["c00","c01"]}]},{"id":"move-26-before","coordinate_units":"initial_radius","time_units":"timeslice","time_bounds":[0,2],"f_vector":[5,9,7,2],"vertices":[{"id":"v00","position":[0,0,0],"time":0},{"id":"v01","position":[0,0,1],"time":1},{"id":"v02","position":[0,1,0],"time":1},{"id":"v03","position":[1,0,0],"time":1},{"id":"v04","position":[1.1547005383792515,1.1547005383792515,1.1547005383792515],"time":2}],"edges":[{"id":"e00","vertices":["v00","v01"],"type":"timelike"},{"id":"e01","vertices":["v00","v02"],"type":"timelike"},{"id":"e02","vertices":["v00","v03"],"type":"timelike"},{"id":"e03","vertices":["v01","v02"],"type":"spacelike"},{"id":"e04","vertices":["v01","v03"],"type":"spacelike"},{"id":"e05","vertices":["v01","v04"],"type":"timelike"},{"id":"e06","vertices":["v02","v03"],"type":"spacelike"},{"id":"e07","vertices":["v02","v04"],"type":"timelike"},{"id":"e08","vertices":["v03","v04"],"type":"timelike"}],"facets":[{"id":"f00","vertices":["v00","v01","v02"],"spacelike":false},{"id":"f01","vertices":["v00","v01","v03"],"spacelike":false},{"id":"f02","vertices":["v00","v02","v03"],"spacelike":false},{"id":"f03","vertices":["v01","v02","v03"],"spacelike":true,"time":1},{"id":"f04","vertices":["v01","v02","v04"],"spacelike":false},{"id":"f05","vertices":["v01","v03","v04"],"spacelike":false},{"id":"f06","vertices":["v02","v03","v04"],"spacelike":false}],"cells":[{"id":"c00","vertices":["v00","v01","v02","v03"],"type":"1-3","adjacent_cells":["c01"]},{"id":"c01","vertices":["v01","v02","v03","v04"],"type":"3-1","adjacent_cells":["c00"]}]},{"id":"move-26-after","coordinate_units":"initial_radius","time_units":"timeslice","time_bounds":[0,2],"f_vector":[6,14,15,6],"vertices":[{"id":"v00","position":[0,0,0],"time":0},{"id":"v01","position":[0,0,1],"time":1},{"id":"v02","position":[0,1,0],"time":1},{"id":"v03","position":[0.33333333333333331,0.33333333333333331,0.33333333333333331],"time":1},{"id":"v04","position":[1,0,0],"time":1},{"id":"v05","position":[1.1547005383792515,1.1547005383792515,1.1547005383792515],"time":2}],"edges":[{"id":"e00","vertices":["v00","v01"],"type":"timelike"},{"id":"e01","vertices":["v00","v02"],"type":"timelike"},{"id":"e02","vertices":["v00","v03"],"type":"timelike"},{"id":"e03","vertices":["v00","v04"],"type":"timelike"},{"id":"e04","vertices":["v01","v02"],"type":"spacelike"},{"id":"e05","vertices":["v01","v03"],"type":"spacelike"},{"id":"e06","vertices":["v01","v04"],"type":"spacelike"},{"id":"e07","vertices":["v01","v05"],"type":"timelike"},{"id":"e08","vertices":["v02","v03"],"type":"spacelike"},{"id":"e09","vertices":["v02","v04"],"type":"spacelike"},{"id":"e10","vertices":["v02","v05"],"type":"timelike"},{"id":"e11","vertices":["v03","v04"],"type":"spacelike"},{"id":"e12","vertices":["v03","v05"],"type":"timelike"},{"id":"e13","vertices":["v04","v05"],"type":"timelike"}],"facets":[{"id":"f00","vertices":["v00","v01","v02"],"spacelike":false},{"id":"f01","vertices":["v00","v01","v03"],"spacelike":false},{"id":"f02","vertices":["v00","v01","v04"],"spacelike":false},{"id":"f03","vertices":["v00","v02","v03"],"spacelike":false},{"id":"f04","vertices":["v00","v02","v04"],"spacelike":false},{"id":"f05","vertices":["v00","v03","v04"],"spacelike":false},{"id":"f06","vertices":["v01","v02","v03"],"spacelike":true,"time":1},{"id":"f07","vertices":["v01","v02","v05"],"spacelike":false},{"id":"f08","vertices":["v01","v03","v04"],"spacelike":true,"time":1},{"id":"f09","vertices":["v01","v03","v05"],"spacelike":false},{"id":"f10","vertices":["v01","v04","v05"],"spacelike":false},{"id":"f11","vertices":["v02","v03","v04"],"spacelike":true,"time":1},{"id":"f12","vertices":["v02","v03","v05"],"spacelike":false},{"id":"f13","vertices":["v02","v04","v05"],"spacelike":false},{"id":"f14","vertices":["v03","v04","v05"],"spacelike":false}],"cells":[{"id":"c00","vertices":["v00","v01","v02","v03"],"type":"1-3","adjacent_cells":["c01","c02","c03"]},{"id":"c01","vertices":["v00","v01","v03","v04"],"type":"1-3","adjacent_cells":["c00","c02","c04"]},{"id":"c02","vertices":["v00","v02","v03","v04"],"type":"1-3","adjacent_cells":["c00","c01","c05"]},{"id":"c03","vertices":["v01","v02","v03","v05"],"type":"3-1","adjacent_cells":["c00","c04","c05"]},{"id":"c04","vertices":["v01","v03","v04","v05"],"type":"3-1","adjacent_cells":["c01","c03","c05"]},{"id":"c05","vertices":["v02","v03","v04","v05"],"type":"3-1","adjacent_cells":["c02","c03","c04"]}]},{"id":"move-44-before","coordinate_units":"initial_radius","time_units":"timeslice","time_bounds":[1,3],"f_vector":[6,13,12,4],"vertices":[{"id":"v00","position":[0,0,0],"time":1},{"id":"v01","position":[-0.70710678118654746,0,0.70710678118654746],"time":2},{"id":"v02","position":[0,-0.70710678118654746,0.70710678118654746],"time":2},{"id":"v03","position":[0,0.70710678118654746,0.70710678118654746],"time":2},{"id":"v04","position":[0.70710678118654746,0,0.70710678118654746],"time":2},{"id":"v05","position":[0,0,2],"time":3}],"edges":[{"id":"e00","vertices":["v00","v01"],"type":"timelike"},{"id":"e01","vertices":["v00","v02"],"type":"timelike"},{"id":"e02","vertices":["v00","v03"],"type":"timelike"},{"id":"e03","vertices":["v00","v04"],"type":"timelike"},{"id":"e04","vertices":["v01","v02"],"type":"spacelike"},{"id":"e05","vertices":["v01","v03"],"type":"spacelike"},{"id":"e06","vertices":["v01","v05"],"type":"timelike"},{"id":"e07","vertices":["v02","v03"],"type":"spacelike"},{"id":"e08","vertices":["v02","v04"],"type":"spacelike"},{"id":"e09","vertices":["v02","v05"],"type":"timelike"},{"id":"e10","vertices":["v03","v04"],"type":"spacelike"},{"id":"e11","vertices":["v03","v05"],"type":"timelike"},{"id":"e12","vertices":["v04","v05"],"type":"timelike"}],"facets":[{"id":"f00","vertices":["v00","v01","v02"],"spacelike":false},{"id":"f01","vertices":["v00","v01","v03"],"spacelike":false},{"id":"f02","vertices":["v00","v02","v03"],"spacelike":false},{"id":"f03","vertices":["v00","v02","v04"],"spacelike":false},{"id":"f04","vertices":["v00","v03","v04"],"spacelike":false},{"id":"f05","vertices":["v01","v02","v03"],"spacelike":true,"time":2},{"id":"f06","vertices":["v01","v02","v05"],"spacelike":false},{"id":"f07","vertices":["v01","v03","v05"],"spacelike":false},{"id":"f08","vertices":["v02","v03","v04"],"spacelike":true,"time":2},{"id":"f09","vertices":["v02","v03","v05"],"spacelike":false},{"id":"f10","vertices":["v02","v04","v05"],"spacelike":false},{"id":"f11","vertices":["v03","v04","v05"],"spacelike":false}],"cells":[{"id":"c00","vertices":["v00","v01","v02","v03"],"type":"1-3","adjacent_cells":["c01","c02"]},{"id":"c01","vertices":["v00","v02","v03","v04"],"type":"1-3","adjacent_cells":["c00","c03"]},{"id":"c02","vertices":["v01","v02","v03","v05"],"type":"3-1","adjacent_cells":["c00","c03"]},{"id":"c03","vertices":["v02","v03","v04","v05"],"type":"3-1","adjacent_cells":["c01","c02"]}]},{"id":"move-44-after","coordinate_units":"initial_radius","time_units":"timeslice","time_bounds":[1,3],"f_vector":[6,13,12,4],"vertices":[{"id":"v00","position":[0,0,0],"time":1},{"id":"v01","position":[-0.70710678118654746,0,0.70710678118654746],"time":2},{"id":"v02","position":[0,-0.70710678118654746,0.70710678118654746],"time":2},{"id":"v03","position":[0,0.70710678118654746,0.70710678118654746],"time":2},{"id":"v04","position":[0.70710678118654746,0,0.70710678118654746],"time":2},{"id":"v05","position":[0,0,2],"time":3}],"edges":[{"id":"e00","vertices":["v00","v01"],"type":"timelike"},{"id":"e01","vertices":["v00","v02"],"type":"timelike"},{"id":"e02","vertices":["v00","v03"],"type":"timelike"},{"id":"e03","vertices":["v00","v04"],"type":"timelike"},{"id":"e04","vertices":["v01","v02"],"type":"spacelike"},{"id":"e05","vertices":["v01","v03"],"type":"spacelike"},{"id":"e06","vertices":["v01","v04"],"type":"spacelike"},{"id":"e07","vertices":["v01","v05"],"type":"timelike"},{"id":"e08","vertices":["v02","v04"],"type":"spacelike"},{"id":"e09","vertices":["v02","v05"],"type":"timelike"},{"id":"e10","vertices":["v03","v04"],"type":"spacelike"},{"id":"e11","vertices":["v03","v05"],"type":"timelike"},{"id":"e12","vertices":["v04","v05"],"type":"timelike"}],"facets":[{"id":"f00","vertices":["v00","v01","v02"],"spacelike":false},{"id":"f01","vertices":["v00","v01","v03"],"spacelike":false},{"id":"f02","vertices":["v00","v01","v04"],"spacelike":false},{"id":"f03","vertices":["v00","v02","v04"],"spacelike":false},{"id":"f04","vertices":["v00","v03","v04"],"spacelike":false},{"id":"f05","vertices":["v01","v02","v04"],"spacelike":true,"time":2},{"id":"f06","vertices":["v01","v02","v05"],"spacelike":false},{"id":"f07","vertices":["v01","v03","v04"],"spacelike":true,"time":2},{"id":"f08","vertices":["v01","v03","v05"],"spacelike":false},{"id":"f09","vertices":["v01","v04","v05"],"spacelike":false},{"id":"f10","vertices":["v02","v04","v05"],"spacelike":false},{"id":"f11","vertices":["v03","v04","v05"],"spacelike":false}],"cells":[{"id":"c00","vertices":["v00","v01","v02","v04"],"type":"1-3","adjacent_cells":["c01","c02"]},{"id":"c01","vertices":["v00","v01","v03","v04"],"type":"1-3","adjacent_cells":["c00","c03"]},{"id":"c02","vertices":["v01","v02","v04","v05"],"type":"3-1","adjacent_cells":["c00","c03"]},{"id":"c03","vertices":["v01","v03","v04","v05"],"type":"3-1","adjacent_cells":["c01","c02"]}]}],"actions":[{"id":"alpha-minus-one-imaginary-coefficient","counts":{"n1_timelike":11,"n3_31_13":7,"n3_22":5},"parameters":{"alpha":-1,"k":1.1,"lambda":0.1},"value":-14.679842216872997},{"id":"alpha-one-rounded","counts":{"n1_timelike":11,"n3_31_13":7,"n3_22":5},"parameters":{"alpha":1,"k":1.1,"lambda":0.1},"value":19.035542216872997},{"id":"alpha-generalized","counts":{"n1_timelike":11,"n3_31_13":7,"n3_22":5},"parameters":{"alpha":0.6,"k":1.1,"lambda":0.1},"value":15.627453388774121}]} diff --git a/reference/raw/v1/end-to-end.txt b/reference/raw/v1/end-to-end.txt new file mode 100644 index 000000000..afcca89fc --- /dev/null +++ b/reference/raw/v1/end-to-end.txt @@ -0,0 +1,51 @@ +Topology is spherical +Dimensionality: 2+1 +Initial radius: 1 +Foliation spacing: 1 +Number of desired simplices: 2 +Number of desired timeslices: 2 +Number of passes: 1 +Checkpoint every 1 passes. +Effective random seed: 92 +Maximum Delaunay threads: 1 +=== Parameters === +Alpha: 1 +K: 0 +Lambda: 1 +cdt started at 2026-07-25.06:42:33UTC + +Generating universe ... +Triangulation has 6 vertices and 14 edges and 16 faces and 7 simplices. +Manifold has 6 vertices and 14 edges and 16 faces and 7 simplices. +There are 0 (3,1) simplices and 3 (2,2) simplices and 4 (1,3) simplices. +There are 7 timelike edges and 7 spacelike edges. +Timeslice 1 has 0 spacelike faces. +Timeslice 2 has 4 spacelike faces. +Starting Metropolis-Hastings algorithm in 2+1 dimensions ... +Effective random seed: 92 (stream 1). +Making random moves ... +=== Pass 1 === +=== Move Results === +There were 7 proposed moves with 0 accepted moves and 7 rejected moves. +There were 7 candidate construction attempts with 0 successful candidates and 7 failed candidates. +(2,3) moves: 0 proposed (0 accepted and 0 rejected); candidate construction: 0 attempted (0 succeeded and 0 failed). +(3,2) moves: 1 proposed (0 accepted and 1 rejected); candidate construction: 1 attempted (0 succeeded and 1 failed). +(2,6) moves: 3 proposed (0 accepted and 3 rejected); candidate construction: 3 attempted (0 succeeded and 3 failed). +(6,2) moves: 2 proposed (0 accepted and 2 rejected); candidate construction: 2 attempted (0 succeeded and 2 failed). +(4,4) moves: 1 proposed (0 accepted and 1 rejected); candidate construction: 1 attempted (0 succeeded and 1 failed). +=== Run results === +=== Move Results === +There were 7 proposed moves with 0 accepted moves and 7 rejected moves. +There were 7 candidate construction attempts with 0 successful candidates and 7 failed candidates. +(2,3) moves: 0 proposed (0 accepted and 0 rejected); candidate construction: 0 attempted (0 succeeded and 0 failed). +(3,2) moves: 1 proposed (0 accepted and 1 rejected); candidate construction: 1 attempted (0 succeeded and 1 failed). +(2,6) moves: 3 proposed (0 accepted and 3 rejected); candidate construction: 3 attempted (0 succeeded and 3 failed). +(6,2) moves: 2 proposed (0 accepted and 2 rejected); candidate construction: 2 attempted (0 succeeded and 2 failed). +(4,4) moves: 1 proposed (0 accepted and 1 rejected); candidate construction: 1 attempted (0 succeeded and 1 failed). +=== Run Results === +Running time is 0.00011801719665527344 seconds. +Manifold has 6 vertices and 14 edges and 16 faces and 7 simplices. +There are 0 (3,1) simplices and 3 (2,2) simplices and 4 (1,3) simplices. +There are 7 timelike edges and 7 spacelike edges. +Timeslice 1 has 0 spacelike faces. +Timeslice 2 has 4 spacelike faces. diff --git a/reference/raw/v1/persistence-v1.off b/reference/raw/v1/persistence-v1.off new file mode 100644 index 000000000..3e52943b1 --- /dev/null +++ b/reference/raw/v1/persistence-v1.off @@ -0,0 +1,59 @@ +3 +6 +0.22264152646727897 -0.09970260436531371 0.96978870965540409 +-0.046594460655508485 -1.2513001713550358 1.5595117304474115 +-0.16533843996794717 0.85558035004408961 -0.49055628105998039 +-0.38023736965271121 1.3469886504577884 -1.4286501035094261 +1.8973363903704143 0.61842201206847314 0.13292417675256729 +-0.41556090947014745 1.7425182318617307 0.88934770599007607 +11 +2 3 4 6 +3 2 1 6 +5 3 1 6 +4 2 0 5 +3 2 4 5 +2 3 1 5 +2 4 0 6 +1 2 5 6 +5 2 0 6 +4 3 5 6 +0 4 5 6 +9 6 1 4 +7 2 0 5 +1 7 9 5 +8 10 4 6 +3 9 5 0 +2 7 4 1 +10 8 0 3 +8 2 1 5 +6 10 7 3 +2 10 0 4 +9 8 6 3 + + + + + + + + + + + + +cdt-plusplus-causal-info-v1 +vertices=6 +v=-0.046594460655508485,-1.2513001713550358,1.5595117304474115|2 +v=-0.16533843996794717,0.85558035004408961,-0.49055628105998039|1 +v=-0.38023736965271121,1.3469886504577884,-1.4286501035094261|2 +v=-0.41556090947014745,1.7425182318617307,0.88934770599007607|2 +v=0.22264152646727897,-0.09970260436531371,0.96978870965540409|1 +v=1.8973363903704143,0.61842201206847314,0.13292417675256729|2 +cells=7 +c=-0.046594460655508485,-1.2513001713550358,1.5595117304474115;-0.16533843996794717,0.85558035004408961,-0.49055628105998039;-0.38023736965271121,1.3469886504577884,-1.4286501035094261;-0.41556090947014745,1.7425182318617307,0.88934770599007607|13 +c=-0.046594460655508485,-1.2513001713550358,1.5595117304474115;-0.16533843996794717,0.85558035004408961,-0.49055628105998039;-0.38023736965271121,1.3469886504577884,-1.4286501035094261;1.8973363903704143,0.61842201206847314,0.13292417675256729|13 +c=-0.046594460655508485,-1.2513001713550358,1.5595117304474115;-0.16533843996794717,0.85558035004408961,-0.49055628105998039;-0.41556090947014745,1.7425182318617307,0.88934770599007607;0.22264152646727897,-0.09970260436531371,0.96978870965540409|22 +c=-0.046594460655508485,-1.2513001713550358,1.5595117304474115;-0.16533843996794717,0.85558035004408961,-0.49055628105998039;0.22264152646727897,-0.09970260436531371,0.96978870965540409;1.8973363903704143,0.61842201206847314,0.13292417675256729|22 +c=-0.046594460655508485,-1.2513001713550358,1.5595117304474115;-0.41556090947014745,1.7425182318617307,0.88934770599007607;0.22264152646727897,-0.09970260436531371,0.96978870965540409;1.8973363903704143,0.61842201206847314,0.13292417675256729|13 +c=-0.16533843996794717,0.85558035004408961,-0.49055628105998039;-0.38023736965271121,1.3469886504577884,-1.4286501035094261;-0.41556090947014745,1.7425182318617307,0.88934770599007607;1.8973363903704143,0.61842201206847314,0.13292417675256729|13 +c=-0.16533843996794717,0.85558035004408961,-0.49055628105998039;-0.41556090947014745,1.7425182318617307,0.88934770599007607;0.22264152646727897,-0.09970260436531371,0.96978870965540409;1.8973363903704143,0.61842201206847314,0.13292417675256729|22 diff --git a/reference/raw/v1/persistence-v1.off.meta b/reference/raw/v1/persistence-v1.off.meta new file mode 100644 index 000000000..5fee6e0f5 --- /dev/null +++ b/reference/raw/v1/persistence-v1.off.meta @@ -0,0 +1,35 @@ +cdt-plusplus-metadata-v1 +payload.size=2727 +payload.fnv1a64=bad0dd210da0af99 +artifact=initial-triangulation +resume_supported=false +fresh_topology_replay_supported=false +transition_replay_requires_identical_start=true +cdt.version=1.0.0-rc2 +build.compiler_id=AppleClang +build.compiler_version=21.0.0.21000101 +build.configuration=RelWithDebInfo +build.system=Darwin +build.processor=arm64 +build.cxx_standard=23 +build.standard_library=libc++-210106 +dependency.cgal_version=6.2-I-900 +random.engine=pcg64 +random.seed=92 +random.initialization_stream=0 +random.transition_stream=1 +topology=spherical +dimension=3 +desired.simplices=2 +desired.timeslices=2 +actual.vertices=6 +actual.edges=14 +actual.faces=16 +actual.simplices=7 +actual.minimum_timeslice=1 +actual.maximum_timeslice=2 +initial_radius=1 +foliation_spacing=1 +parallel.max_threads=1 +placement.fnv1a64=855d4b2867eca820 +topology.fnv1a64=91c07b3f9c757de3 diff --git a/reference/raw/v1/scaling-threads-1.txt b/reference/raw/v1/scaling-threads-1.txt new file mode 100644 index 000000000..b1cc72c72 --- /dev/null +++ b/reference/raw/v1/scaling-threads-1.txt @@ -0,0 +1,59 @@ +record.schema=cdt-cgal-benchmark-v1 +implementation.name=CDT-plusplus +implementation.version=1.0.0-rc2 +implementation.revision=49dbce290ee9cc9c0430893e11caa84cbaeaffee-dirty +build.compiler_id=AppleClang +build.compiler_version=21.0.0.21000101 +build.configuration=RelWithDebInfo +build.system=Darwin +build.processor=arm64 +build.cxx_standard=23 +build.standard_library=libc++-210106 +hardware.logical_threads=16 +dependency.cgal_version=6.2-I-900 +dependency.tbb_version=2023.0.0 +parallel_tds=1 +requested_threads=1 +active_threads=1 +fixture.id=generated-foliated-ball-v1 +random.seed=102 +random.initialization_stream=0 +random.transition_stream=1 +requested_simplices=640 +timeslices=4 +generated_points=800 +warmups=1 +repetitions=5 +moves_per_repetition=50 +final_vertices=710 +final_cells=3378 +sample.unit=nanoseconds +checksum=24218 +bulk_insert_ns_min=2736583 +bulk_insert_ns_median=2895292 +bulk_insert_ns_max=3139250 +bulk_insert_ns_samples=2806000,2895292,2736583,2938375,3139250 +foliation_repair_ns_min=8013375 +foliation_repair_ns_median=8277167 +foliation_repair_ns_max=8905667 +foliation_repair_ns_samples=8695375,8013375,8905667,8084000,8277167 +cache_rebuild_ns_min=2451250 +cache_rebuild_ns_median=2484083 +cache_rebuild_ns_max=2760542 +cache_rebuild_ns_samples=2532750,2760542,2476875,2451250,2484083 +point_lookup_ns_min=611500 +point_lookup_ns_median=655291 +point_lookup_ns_max=660708 +point_lookup_ns_samples=658417,611500,660708,655291,620458 +snapshot_copy_ns_min=171667 +snapshot_copy_ns_median=185417 +snapshot_copy_ns_max=200125 +snapshot_copy_ns_samples=185750,171667,174708,200125,185417 +vertex_removal_ns_min=806375 +vertex_removal_ns_median=825375 +vertex_removal_ns_max=918500 +vertex_removal_ns_samples=814291,825375,875792,806375,918500 +move_workload_ns_min=311093917 +move_workload_ns_median=314707167 +move_workload_ns_max=315700167 +move_workload_ns_samples=314707167,315700167,314565834,311093917,315677542 diff --git a/reference/raw/v1/scaling-threads-2.txt b/reference/raw/v1/scaling-threads-2.txt new file mode 100644 index 000000000..cfbdedcb1 --- /dev/null +++ b/reference/raw/v1/scaling-threads-2.txt @@ -0,0 +1,59 @@ +record.schema=cdt-cgal-benchmark-v1 +implementation.name=CDT-plusplus +implementation.version=1.0.0-rc2 +implementation.revision=49dbce290ee9cc9c0430893e11caa84cbaeaffee-dirty +build.compiler_id=AppleClang +build.compiler_version=21.0.0.21000101 +build.configuration=RelWithDebInfo +build.system=Darwin +build.processor=arm64 +build.cxx_standard=23 +build.standard_library=libc++-210106 +hardware.logical_threads=16 +dependency.cgal_version=6.2-I-900 +dependency.tbb_version=2023.0.0 +parallel_tds=1 +requested_threads=2 +active_threads=2 +fixture.id=generated-foliated-ball-v1 +random.seed=102 +random.initialization_stream=0 +random.transition_stream=1 +requested_simplices=640 +timeslices=4 +generated_points=800 +warmups=1 +repetitions=5 +moves_per_repetition=50 +final_vertices=709 +final_cells=3363 +sample.unit=nanoseconds +checksum=24212 +bulk_insert_ns_min=1942375 +bulk_insert_ns_median=2032334 +bulk_insert_ns_max=3596791 +bulk_insert_ns_samples=2077000,2032334,3596791,1942375,1969000 +foliation_repair_ns_min=7237833 +foliation_repair_ns_median=7731000 +foliation_repair_ns_max=8180875 +foliation_repair_ns_samples=7237833,7363583,8117583,7731000,8180875 +cache_rebuild_ns_min=2422958 +cache_rebuild_ns_median=2446792 +cache_rebuild_ns_max=2481333 +cache_rebuild_ns_samples=2446792,2481333,2466166,2422958,2435709 +point_lookup_ns_min=557417 +point_lookup_ns_median=645583 +point_lookup_ns_max=666834 +point_lookup_ns_samples=645583,666834,557417,665750,632833 +snapshot_copy_ns_min=172042 +snapshot_copy_ns_median=208500 +snapshot_copy_ns_max=218709 +snapshot_copy_ns_samples=208500,172042,208708,218709,200083 +vertex_removal_ns_min=779667 +vertex_removal_ns_median=795958 +vertex_removal_ns_max=1074958 +vertex_removal_ns_samples=799250,786500,779667,1074958,795958 +move_workload_ns_min=308960334 +move_workload_ns_median=313309583 +move_workload_ns_max=314798750 +move_workload_ns_samples=313311959,314798750,313309583,308960334,312040875 diff --git a/reference/raw/v1/scaling-threads-4.txt b/reference/raw/v1/scaling-threads-4.txt new file mode 100644 index 000000000..0401e02c7 --- /dev/null +++ b/reference/raw/v1/scaling-threads-4.txt @@ -0,0 +1,59 @@ +record.schema=cdt-cgal-benchmark-v1 +implementation.name=CDT-plusplus +implementation.version=1.0.0-rc2 +implementation.revision=49dbce290ee9cc9c0430893e11caa84cbaeaffee-dirty +build.compiler_id=AppleClang +build.compiler_version=21.0.0.21000101 +build.configuration=RelWithDebInfo +build.system=Darwin +build.processor=arm64 +build.cxx_standard=23 +build.standard_library=libc++-210106 +hardware.logical_threads=16 +dependency.cgal_version=6.2-I-900 +dependency.tbb_version=2023.0.0 +parallel_tds=1 +requested_threads=4 +active_threads=4 +fixture.id=generated-foliated-ball-v1 +random.seed=102 +random.initialization_stream=0 +random.transition_stream=1 +requested_simplices=640 +timeslices=4 +generated_points=800 +warmups=1 +repetitions=5 +moves_per_repetition=50 +final_vertices=709 +final_cells=3364 +sample.unit=nanoseconds +checksum=24231 +bulk_insert_ns_min=1952500 +bulk_insert_ns_median=4874042 +bulk_insert_ns_max=9547250 +bulk_insert_ns_samples=9547250,4874042,1952500,8105459,4432750 +foliation_repair_ns_min=7241375 +foliation_repair_ns_median=7441792 +foliation_repair_ns_max=7928916 +foliation_repair_ns_samples=7241375,7416417,7441792,7699250,7928916 +cache_rebuild_ns_min=2451625 +cache_rebuild_ns_median=2501625 +cache_rebuild_ns_max=2551500 +cache_rebuild_ns_samples=2551500,2501625,2451625,2524250,2453208 +point_lookup_ns_min=663541 +point_lookup_ns_median=674750 +point_lookup_ns_max=684083 +point_lookup_ns_samples=676458,663541,674750,684083,673458 +snapshot_copy_ns_min=192083 +snapshot_copy_ns_median=229875 +snapshot_copy_ns_max=259375 +snapshot_copy_ns_samples=224333,192083,259375,229875,244583 +vertex_removal_ns_min=795917 +vertex_removal_ns_median=812375 +vertex_removal_ns_max=823375 +vertex_removal_ns_samples=807250,795917,815209,823375,812375 +move_workload_ns_min=306495250 +move_workload_ns_median=308535833 +move_workload_ns_max=309447875 +move_workload_ns_samples=309447875,308722500,308535833,307089917,306495250 diff --git a/reference/schema/fixture-v1.schema.json b/reference/schema/fixture-v1.schema.json new file mode 100644 index 000000000..f6631b1b3 --- /dev/null +++ b/reference/schema/fixture-v1.schema.json @@ -0,0 +1,483 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://adamgetchell.org/CDT-plusplus/reference/schema/fixture-v1.schema.json", + "title": "CDT++ cross-language fixture protocol v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "fixture_set", + "canonicalization", + "units", + "tolerances", + "comparison_policy", + "state_fixtures", + "transitions", + "persistence", + "bounded_run" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "fixture_set": { + "type": "string", + "pattern": "^[a-z0-9-]+-v[0-9]+$" + }, + "canonicalization": { + "$ref": "#/$defs/canonicalization" + }, + "units": { + "$ref": "#/$defs/units" + }, + "tolerances": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/tolerance" + } + }, + "comparison_policy": { + "$ref": "#/$defs/comparisonPolicy" + }, + "state_fixtures": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/stateFixture" + } + }, + "transitions": { + "type": "array", + "minItems": 5, + "items": { + "$ref": "#/$defs/transition" + } + }, + "persistence": { + "$ref": "#/$defs/persistence" + }, + "bounded_run": { + "$ref": "#/$defs/boundedRun" + } + }, + "$defs": { + "nonemptyString": { + "type": "string", + "minLength": 1 + }, + "stringArray": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/nonemptyString" + } + }, + "command": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/nonemptyString" + } + }, + "canonicalization": { + "type": "object", + "additionalProperties": false, + "required": [ + "vertices", + "edges", + "facets", + "cells", + "adjacency", + "floating_point" + ], + "properties": { + "vertices": { + "$ref": "#/$defs/nonemptyString" + }, + "edges": { + "$ref": "#/$defs/nonemptyString" + }, + "facets": { + "$ref": "#/$defs/nonemptyString" + }, + "cells": { + "$ref": "#/$defs/nonemptyString" + }, + "adjacency": { + "$ref": "#/$defs/nonemptyString" + }, + "floating_point": { + "$ref": "#/$defs/nonemptyString" + } + } + }, + "units": { + "type": "object", + "additionalProperties": false, + "required": [ + "coordinates", + "time", + "regge_action", + "action_delta", + "probability", + "benchmark_duration" + ], + "properties": { + "coordinates": { + "const": "initial_radius" + }, + "time": { + "const": "timeslice" + }, + "regge_action": { + "const": "dimensionless" + }, + "action_delta": { + "const": "dimensionless" + }, + "probability": { + "const": "unit_interval" + }, + "benchmark_duration": { + "const": "nanoseconds" + } + } + }, + "tolerance": { + "type": "object", + "additionalProperties": false, + "required": [ + "absolute", + "relative", + "rationale" + ], + "properties": { + "absolute": { + "type": "number", + "minimum": 0 + }, + "relative": { + "type": "number", + "minimum": 0 + }, + "rationale": { + "$ref": "#/$defs/nonemptyString" + } + } + }, + "comparisonPolicy": { + "type": "object", + "additionalProperties": false, + "required": [ + "exact", + "numerical", + "implementation_specific", + "unsupported" + ], + "properties": { + "exact": { + "$ref": "#/$defs/stringArray" + }, + "numerical": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/nonemptyString" + } + }, + "implementation_specific": { + "$ref": "#/$defs/stringArray" + }, + "unsupported": { + "$ref": "#/$defs/stringArray" + } + } + }, + "position": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "type": "number" + } + }, + "stateFixture": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "source", + "expected_state" + ], + "properties": { + "id": { + "$ref": "#/$defs/nonemptyString" + }, + "source": { + "$ref": "#/$defs/nonemptyString" + }, + "expected_state": { + "$ref": "#/$defs/nonemptyString" + }, + "input_state": { + "$ref": "#/$defs/nonemptyString" + }, + "removed_vertex": { + "type": "object", + "additionalProperties": false, + "required": [ + "position", + "time" + ], + "properties": { + "position": { + "$ref": "#/$defs/position" + }, + "time": { + "type": "integer" + } + } + } + } + }, + "site": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "entity", + "vertices" + ], + "properties": { + "kind": { + "enum": [ + "2-2-cell", + "timelike-edge", + "1-3-cell", + "degree-five-vertex", + "spacelike-edge" + ] + }, + "entity": { + "type": "string", + "pattern": "^[vecf][0-9]{2,}$" + }, + "vertices": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^v[0-9]{2,}$" + } + }, + "shared_spacelike_facet": { + "type": "string", + "pattern": "^f[0-9]{2,}$" + }, + "reverse_vertices": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^v[0-9]{2,}$" + } + } + } + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "required": [ + "alpha", + "k", + "lambda" + ], + "properties": { + "alpha": { + "type": "number" + }, + "k": { + "type": "number" + }, + "lambda": { + "type": "number" + } + } + }, + "transition": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "move", + "before", + "proposed", + "site", + "parameters", + "proposal_probability", + "reverse_probability", + "action_delta", + "acceptance_probability", + "acceptance_variate", + "accepted", + "committed" + ], + "properties": { + "id": { + "$ref": "#/$defs/nonemptyString" + }, + "move": { + "enum": [ + "2-3", + "3-2", + "2-6", + "6-2", + "4-4" + ] + }, + "before": { + "$ref": "#/$defs/nonemptyString" + }, + "proposed": { + "$ref": "#/$defs/nonemptyString" + }, + "site": { + "$ref": "#/$defs/site" + }, + "parameters": { + "$ref": "#/$defs/parameters" + }, + "proposal_probability": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1 + }, + "reverse_probability": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 1 + }, + "action_delta": { + "type": "number" + }, + "acceptance_probability": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "acceptance_variate": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "accepted": { + "type": "boolean" + }, + "committed": { + "$ref": "#/$defs/nonemptyString" + } + } + }, + "persistence": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "command", + "expected" + ], + "properties": { + "id": { + "$ref": "#/$defs/nonemptyString" + }, + "command": { + "$ref": "#/$defs/command" + }, + "expected": { + "$ref": "#/$defs/stringArray" + } + } + }, + "fVectorBand": { + "type": "object", + "additionalProperties": false, + "required": [ + "minimum", + "maximum", + "rationale" + ], + "properties": { + "minimum": { + "$ref": "#/$defs/fVector" + }, + "maximum": { + "$ref": "#/$defs/fVector" + }, + "rationale": { + "$ref": "#/$defs/nonemptyString" + } + } + }, + "fVector": { + "type": "array", + "prefixItems": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "integer", + "minimum": 0 + }, + { + "type": "integer", + "minimum": 0 + }, + { + "type": "integer", + "minimum": 0 + } + ], + "items": false + }, + "boundedRun": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "command", + "exact", + "bands" + ], + "properties": { + "id": { + "$ref": "#/$defs/nonemptyString" + }, + "command": { + "$ref": "#/$defs/command" + }, + "exact": { + "$ref": "#/$defs/stringArray" + }, + "bands": { + "type": "object", + "additionalProperties": false, + "required": [ + "randomized_cgal_f_vector" + ], + "properties": { + "randomized_cgal_f_vector": { + "$ref": "#/$defs/fVectorBand" + } + } + } + } + } + } +} diff --git a/reference/schema/result-v1.schema.json b/reference/schema/result-v1.schema.json new file mode 100644 index 000000000..9520b4cad --- /dev/null +++ b/reference/schema/result-v1.schema.json @@ -0,0 +1,426 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://adamgetchell.org/CDT-plusplus/reference/schema/result-v1.schema.json", + "title": "CDT++ canonical raw result v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "implementation", + "states", + "actions" + ], + "properties": { + "schema": { + "const": "cdt-reference-raw-v1" + }, + "implementation": { + "$ref": "#/$defs/implementation" + }, + "states": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/state" + } + }, + "actions": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/action" + } + } + }, + "$defs": { + "nonemptyString": { + "type": "string", + "minLength": 1 + }, + "vertexId": { + "type": "string", + "pattern": "^v[0-9]{2,}$" + }, + "edgeId": { + "type": "string", + "pattern": "^e[0-9]{2,}$" + }, + "facetId": { + "type": "string", + "pattern": "^f[0-9]{2,}$" + }, + "cellId": { + "type": "string", + "pattern": "^c[0-9]{2,}$" + }, + "position": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "type": "number" + } + }, + "vertexIds2": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/vertexId" + } + }, + "vertexIds3": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/vertexId" + } + }, + "vertexIds4": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/vertexId" + } + }, + "implementation": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "version", + "revision", + "compiler", + "build_profile", + "operating_system", + "hardware", + "logical_threads", + "cxx_standard", + "standard_library", + "cgal_version" + ], + "properties": { + "name": { + "$ref": "#/$defs/nonemptyString" + }, + "version": { + "$ref": "#/$defs/nonemptyString" + }, + "revision": { + "$ref": "#/$defs/nonemptyString" + }, + "compiler": { + "$ref": "#/$defs/nonemptyString" + }, + "build_profile": { + "$ref": "#/$defs/nonemptyString" + }, + "operating_system": { + "$ref": "#/$defs/nonemptyString" + }, + "hardware": { + "$ref": "#/$defs/nonemptyString" + }, + "logical_threads": { + "type": "integer", + "minimum": 0 + }, + "cxx_standard": { + "const": 23 + }, + "standard_library": { + "$ref": "#/$defs/nonemptyString" + }, + "cgal_version": { + "$ref": "#/$defs/nonemptyString" + } + } + }, + "fVector": { + "type": "array", + "prefixItems": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "integer", + "minimum": 0 + }, + { + "type": "integer", + "minimum": 0 + }, + { + "type": "integer", + "minimum": 0 + } + ], + "items": false + }, + "vertex": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "position", + "time" + ], + "properties": { + "id": { + "$ref": "#/$defs/vertexId" + }, + "position": { + "$ref": "#/$defs/position" + }, + "time": { + "type": "integer" + } + } + }, + "edge": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "vertices", + "type" + ], + "properties": { + "id": { + "$ref": "#/$defs/edgeId" + }, + "vertices": { + "$ref": "#/$defs/vertexIds2" + }, + "type": { + "enum": [ + "spacelike", + "timelike" + ] + } + } + }, + "facet": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "vertices", + "spacelike" + ], + "properties": { + "id": { + "$ref": "#/$defs/facetId" + }, + "vertices": { + "$ref": "#/$defs/vertexIds3" + }, + "spacelike": { + "type": "boolean" + }, + "time": { + "type": "integer" + } + }, + "allOf": [ + { + "if": { + "properties": { + "spacelike": { + "const": true + } + }, + "required": [ + "spacelike" + ] + }, + "then": { + "required": [ + "time" + ] + }, + "else": { + "not": { + "required": [ + "time" + ] + } + } + } + ] + }, + "cell": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "vertices", + "type", + "adjacent_cells" + ], + "properties": { + "id": { + "$ref": "#/$defs/cellId" + }, + "vertices": { + "$ref": "#/$defs/vertexIds4" + }, + "type": { + "enum": [ + "3-1", + "2-2", + "1-3", + "acausal", + "unclassified" + ] + }, + "adjacent_cells": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/cellId" + } + } + } + }, + "state": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "coordinate_units", + "time_units", + "time_bounds", + "f_vector", + "vertices", + "edges", + "facets", + "cells" + ], + "properties": { + "id": { + "$ref": "#/$defs/nonemptyString" + }, + "coordinate_units": { + "const": "initial_radius" + }, + "time_units": { + "const": "timeslice" + }, + "time_bounds": { + "type": "array", + "prefixItems": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ], + "items": false + }, + "f_vector": { + "$ref": "#/$defs/fVector" + }, + "vertices": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/vertex" + } + }, + "edges": { + "type": "array", + "items": { + "$ref": "#/$defs/edge" + } + }, + "facets": { + "type": "array", + "items": { + "$ref": "#/$defs/facet" + } + }, + "cells": { + "type": "array", + "items": { + "$ref": "#/$defs/cell" + } + } + } + }, + "counts": { + "type": "object", + "additionalProperties": false, + "required": [ + "n1_timelike", + "n3_31_13", + "n3_22" + ], + "properties": { + "n1_timelike": { + "type": "integer", + "minimum": 0 + }, + "n3_31_13": { + "type": "integer", + "minimum": 0 + }, + "n3_22": { + "type": "integer", + "minimum": 0 + } + } + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "required": [ + "alpha", + "k", + "lambda" + ], + "properties": { + "alpha": { + "type": "number" + }, + "k": { + "type": "number" + }, + "lambda": { + "type": "number" + } + } + }, + "action": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "counts", + "parameters", + "value" + ], + "properties": { + "id": { + "$ref": "#/$defs/nonemptyString" + }, + "counts": { + "$ref": "#/$defs/counts" + }, + "parameters": { + "$ref": "#/$defs/parameters" + }, + "value": { + "type": "number" + } + } + } + } +} diff --git a/reference/schema/run-manifest-v1.schema.json b/reference/schema/run-manifest-v1.schema.json new file mode 100644 index 000000000..d0d87374a --- /dev/null +++ b/reference/schema/run-manifest-v1.schema.json @@ -0,0 +1,263 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://adamgetchell.org/CDT-plusplus/reference/schema/run-manifest-v1.schema.json", + "title": "CDT++ reference run manifest v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "run_id", + "recorded_at_utc", + "implementation", + "toolchain", + "build", + "host", + "thread_count", + "seeds", + "fixture_version", + "commands", + "artifacts", + "verification" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "run_id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "recorded_at_utc": { + "type": "string", + "format": "date-time" + }, + "implementation": { + "$ref": "#/$defs/implementation" + }, + "toolchain": { + "$ref": "#/$defs/toolchain" + }, + "build": { + "$ref": "#/$defs/build" + }, + "host": { + "$ref": "#/$defs/host" + }, + "thread_count": { + "type": "integer", + "minimum": 1 + }, + "seeds": { + "$ref": "#/$defs/seeds" + }, + "fixture_version": { + "const": 1 + }, + "commands": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/commandRecord" + } + }, + "artifacts": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/artifact" + } + }, + "verification": { + "type": "object", + "minProperties": 1 + } + }, + "$defs": { + "nonemptyString": { + "type": "string", + "minLength": 1 + }, + "command": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/nonemptyString" + } + }, + "commandRecord": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "artifacts", + "command_line" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "artifacts": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^reference/raw/" + } + }, + "command_line": { + "$ref": "#/$defs/command" + } + } + }, + "implementation": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "version", + "source_revision" + ], + "properties": { + "name": { + "$ref": "#/$defs/nonemptyString" + }, + "version": { + "$ref": "#/$defs/nonemptyString" + }, + "source_revision": { + "type": "string", + "pattern": "^[0-9a-f]{40}(-dirty)?$" + }, + "note": { + "$ref": "#/$defs/nonemptyString" + } + } + }, + "toolchain": { + "type": "object", + "additionalProperties": false, + "required": [ + "compiler", + "cxx_standard", + "standard_library", + "cmake", + "cgal" + ], + "properties": { + "compiler": { + "$ref": "#/$defs/nonemptyString" + }, + "cxx_standard": { + "const": 23 + }, + "standard_library": { + "$ref": "#/$defs/nonemptyString" + }, + "cmake": { + "$ref": "#/$defs/nonemptyString" + }, + "cgal": { + "$ref": "#/$defs/nonemptyString" + }, + "mpfr": { + "$ref": "#/$defs/nonemptyString" + }, + "tbb": { + "$ref": "#/$defs/nonemptyString" + } + } + }, + "build": { + "type": "object", + "additionalProperties": false, + "required": [ + "profile", + "preset", + "parallel_triangulation" + ], + "properties": { + "profile": { + "$ref": "#/$defs/nonemptyString" + }, + "preset": { + "$ref": "#/$defs/nonemptyString" + }, + "parallel_triangulation": { + "type": "boolean" + }, + "thread_counts": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "integer", + "minimum": 1 + } + } + } + }, + "host": { + "type": "object", + "additionalProperties": false, + "required": [ + "operating_system", + "architecture", + "logical_threads" + ], + "properties": { + "operating_system": { + "$ref": "#/$defs/nonemptyString" + }, + "architecture": { + "$ref": "#/$defs/nonemptyString" + }, + "logical_threads": { + "type": "integer", + "minimum": 1 + }, + "comparison_constraint": { + "$ref": "#/$defs/nonemptyString" + } + } + }, + "seeds": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + } + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "media_type", + "sha256" + ], + "properties": { + "path": { + "type": "string", + "pattern": "^reference/" + }, + "media_type": { + "$ref": "#/$defs/nonemptyString" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + } + } + } + } +} diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 03faa280d..b7003f9bb 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -87,24 +87,23 @@ lcov \ "${repo_root}/src/*" \ --no-function-coverage \ --ignore-errors inconsistent \ - --expect-message-count inconsistent:3 \ + --expect-message-count inconsistent:0 \ --rc branch_coverage=1 \ --output-file "${coverage_file}" -# GCC 16 emits three known line-hit/branch-unhit records in Utilities.hpp. -# LCOV checks each record twice while rendering and listing the filtered report, -# so keep the warnings visible and fail if either expected count changes. +# Project-only coverage should contain no inconsistent line/branch records. +# Keep warnings visible and fail if LCOV reports any. genhtml \ --branch-coverage \ --no-function-coverage \ --ignore-errors inconsistent \ - --expect-message-count inconsistent:6 \ + --expect-message-count inconsistent:0 \ --output-directory "${html_dir}" \ "${coverage_file}" lcov \ --list "${coverage_file}" \ --no-function-coverage \ --ignore-errors inconsistent \ - --expect-message-count inconsistent:6 \ + --expect-message-count inconsistent:0 \ --rc branch_coverage=1 rm -f -- "${raw_coverage_file}" diff --git a/scripts/generate_reference_fixtures.py b/scripts/generate_reference_fixtures.py new file mode 100644 index 000000000..06647c42f --- /dev/null +++ b/scripts/generate_reference_fixtures.py @@ -0,0 +1,490 @@ +"""Regenerate every CDT++ reference artifact from one clean source commit.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import subprocess +import sys +import tempfile +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from scripts.validate_reference_fixtures import ( + load_json, + parse_json_object, + validate_document, +) + +ROOT = Path(__file__).resolve().parents[1] +MANIFESTS = ROOT / "reference" / "manifests" / "v1" + +REFERENCE_COMMANDS = [ + { + "id": "canonical-fixture", + "artifacts": ["reference/raw/v1/cpp-reference.json"], + }, + { + "id": "bounded-end-to-end", + "artifacts": ["reference/raw/v1/end-to-end.txt"], + }, + { + "id": "persistence-roundtrip", + "artifacts": [ + "reference/raw/v1/persistence-v1.off", + "reference/raw/v1/persistence-v1.off.meta", + ], + }, +] + + +def run_command( + command: list[str], + *, + cwd: Path = ROOT, + timeout: int = 900, +) -> str: + """Run one producer without a shell and return its exact standard output.""" + completed = subprocess.run( # noqa: S603 + command, + cwd=cwd, + check=False, + capture_output=True, + text=True, + timeout=timeout, + ) + if completed.returncode != 0: + rendered = " ".join(command) + message = f"reference producer failed with exit {completed.returncode}: {rendered}\n{completed.stderr.strip()}" + raise RuntimeError(message) + return completed.stdout + + +def clean_source_revision() -> str: + """Return HEAD only when every tracked and untracked source path is clean.""" + status = run_command( + ["git", "status", "--porcelain", "--untracked-files=all"], + timeout=30, + ) + if status: + message = "reference regeneration requires a clean worktree; commit the implementation and regeneration workflow first" + raise RuntimeError(message) + revision = run_command(["git", "rev-parse", "HEAD"], timeout=30).strip() + if len(revision) != 40 or any(character not in "0123456789abcdef" for character in revision): + message = f"git returned an invalid source revision: {revision!r}" + raise RuntimeError(message) + return revision + + +def executable(path: Path) -> Path: + """Resolve and validate one repository-owned producer executable.""" + resolved = path if path.is_absolute() else ROOT / path + if not resolved.is_file() or not os.access(resolved, os.X_OK): + message = f"reference producer is missing or not executable: {resolved}" + raise ValueError(message) + return resolved + + +def display_path(path: Path) -> str: + """Render a producer path relative to the repository when possible.""" + resolved = path if path.is_absolute() else ROOT / path + try: + return str(resolved.resolve().relative_to(ROOT.resolve())) + except ValueError: + return str(resolved) + + +def parse_key_values(payload: str) -> dict[str, str]: + """Parse one benchmark or persistence key-value record.""" + return dict(line.split("=", maxsplit=1) for line in payload.splitlines() if "=" in line) + + +def sha256(payload: bytes) -> str: + """Return a lowercase SHA-256 digest for generated bytes.""" + return hashlib.sha256(payload).hexdigest() + + +def artifact_digest(path: str, generated: dict[str, bytes]) -> str: + """Hash generated bytes or an authored package input.""" + if path in generated: + return sha256(generated[path]) + return sha256((ROOT / path).read_bytes()) + + +def refresh_artifacts( + manifest: dict[str, Any], + generated: dict[str, bytes], +) -> None: + """Refresh every artifact checksum without changing its media type.""" + for artifact in manifest["artifacts"]: + artifact["sha256"] = artifact_digest(str(artifact["path"]), generated) + + +def record_commands( + records: list[dict[str, Any]], + command_lines: list[list[str]], +) -> list[dict[str, Any]]: + """Attach exact argv arrays to the artifacts each command produced.""" + if len(records) != len(command_lines): + message = "producer command metadata does not match the generated artifacts" + raise ValueError(message) + return [ + { + **record, + "command_line": command_line, + } + for record, command_line in zip(records, command_lines, strict=True) + ] + + +def canonical_timestamp(value: str | None) -> str: + """Return a canonical UTC timestamp, rejecting ambiguous overrides.""" + if value is None: + instant = datetime.now(UTC).replace(microsecond=0) + else: + try: + instant = datetime.fromisoformat(value) + except ValueError as error: + message = f"invalid --recorded-at-utc value: {value!r}" + raise ValueError(message) from error + if instant.tzinfo is None or instant.utcoffset() is None: + message = "--recorded-at-utc must include a UTC offset" + raise ValueError(message) + instant = instant.astimezone(UTC) + return instant.isoformat().replace("+00:00", "Z") + + +def producer_commands( + fixture: Path, + cdt: Path, + initialize: Path, + benchmark: Path, +) -> tuple[list[str], list[str], list[str], list[list[str]]]: + """Return the four producer command families with resolved executables.""" + return ( + [str(fixture)], + [ + str(cdt), + "-s", + "-n2", + "-t2", + "-a1", + "-k0", + "-l1", + "-p1", + "-c1", + "--seed", + "92", + "--threads", + "1", + "--no-output", + ], + [ + str(initialize), + "-s", + "-n2", + "-t2", + "-o", + "--seed", + "92", + "--threads", + "1", + ], + [[str(benchmark), "640", "5", "50", str(threads), "1"] for threads in (1, 2, 4)], + ) + + +def produce_raw_artifacts( + revision: str, + fixture_command: list[str], + bounded_command: list[str], + persistence_command: list[str], + benchmark_commands: list[list[str]], +) -> tuple[dict[str, bytes], dict[str, Any], list[dict[str, str]]]: + """Run every producer and retain its direct, unnormalized output.""" + generated: dict[str, bytes] = {} + fixture_output = run_command(fixture_command) + fixture_json = parse_json_object(fixture_output, fixture_command[0]) + if fixture_json["implementation"]["revision"] != revision: + message = "fixture binary does not embed the clean source revision; reconfigure and rebuild it" + raise RuntimeError(message) + generated["reference/raw/v1/cpp-reference.json"] = fixture_output.encode() + generated["reference/raw/v1/end-to-end.txt"] = run_command(bounded_command).encode() + + with tempfile.TemporaryDirectory(prefix="cdt-reference-persistence-") as temp: + temp_path = Path(temp) + run_command(persistence_command, cwd=temp_path) + payloads = list(temp_path.glob("*.off")) + if len(payloads) != 1: + message = f"persistence producer did not create exactly one OFF payload: {payloads}" + raise RuntimeError(message) + payload = payloads[0] + metadata = payload.with_suffix(".off.meta") + if not metadata.is_file(): + message = f"persistence producer did not create {metadata.name}" + raise RuntimeError(message) + generated["reference/raw/v1/persistence-v1.off"] = payload.read_bytes() + generated["reference/raw/v1/persistence-v1.off.meta"] = metadata.read_bytes() + + scaling_records: list[dict[str, str]] = [] + for threads, command in zip((1, 2, 4), benchmark_commands, strict=True): + output = run_command(command) + record = parse_key_values(output) + if record.get("implementation.revision") != revision: + message = f"{threads}-thread benchmark does not embed the clean source revision; reconfigure and rebuild it" + raise RuntimeError(message) + scaling_records.append(record) + generated[f"reference/raw/v1/scaling-threads-{threads}.txt"] = output.encode() + return generated, fixture_json, scaling_records + + +def validate_platform_identity( + fixture_json: dict[str, Any], + scaling_records: list[dict[str, str]], +) -> None: + """Protect the current macOS-arm64 manifest names from cross-host misuse.""" + implementation = fixture_json["implementation"] + first_scaling = scaling_records[0] + identities = { + ( + str(implementation["operating_system"]), + str(implementation["hardware"]), + ), + ( + first_scaling["build.system"], + first_scaling["build.processor"], + ), + } + if identities != {("Darwin", "arm64")}: + message = "the v1 manifest templates are macOS-arm64-specific; add explicit templates before regenerating on another platform" + raise RuntimeError(message) + + +def make_reference_manifest( + generated: dict[str, bytes], + fixture_json: dict[str, Any], + revision: str, + recorded: str, + command_lines: list[list[str]], +) -> dict[str, Any]: + """Build the canonical/persistence manifest from producer metadata.""" + manifest = json.loads((MANIFESTS / "macos-arm64.json").read_text(encoding="utf-8")) + implementation = fixture_json["implementation"] + recorded_date = recorded[:10].replace("-", "") + manifest["run_id"] = f"cdt-reference-v1-{implementation['operating_system'].lower()}-{implementation['hardware'].lower()}-{recorded_date}" + manifest["recorded_at_utc"] = recorded + manifest["implementation"].update( + { + "version": implementation["version"], + "source_revision": revision, + } + ) + manifest["implementation"].pop("note", None) + manifest["toolchain"].update( + { + "compiler": implementation["compiler"], + "cxx_standard": implementation["cxx_standard"], + "standard_library": implementation["standard_library"], + "cgal": implementation["cgal_version"], + } + ) + manifest["build"]["profile"] = implementation["build_profile"] + manifest["host"].update( + { + "operating_system": implementation["operating_system"], + "architecture": implementation["hardware"], + "logical_threads": implementation["logical_threads"], + } + ) + manifest["commands"] = record_commands(REFERENCE_COMMANDS, command_lines) + refresh_artifacts(manifest, generated) + return manifest + + +def make_scaling_manifest( + generated: dict[str, bytes], + records: list[dict[str, str]], + revision: str, + recorded: str, + command_lines: list[list[str]], +) -> dict[str, Any]: + """Build the matched scaling manifest from benchmark metadata.""" + first = records[0] + recorded_date = recorded[:10].replace("-", "") + manifest = json.loads((MANIFESTS / "scaling-macos-arm64.json").read_text(encoding="utf-8")) + manifest["run_id"] = f"cdt-scaling-v1-{first['build.system'].lower()}-{first['build.processor'].lower()}-{recorded_date}" + manifest["recorded_at_utc"] = recorded + manifest["implementation"].update( + { + "version": first["implementation.version"], + "source_revision": revision, + } + ) + manifest["implementation"].pop("note", None) + manifest["toolchain"].update( + { + "compiler": f"{first['build.compiler_id']} {first['build.compiler_version']}", + "cxx_standard": int(first["build.cxx_standard"]), + "standard_library": first["build.standard_library"], + "cgal": first["dependency.cgal_version"], + "tbb": first["dependency.tbb_version"], + } + ) + manifest["build"]["profile"] = first["build.configuration"] + manifest["host"].update( + { + "operating_system": first["build.system"], + "architecture": first["build.processor"], + "logical_threads": int(first["hardware.logical_threads"]), + } + ) + command_records = [ + { + "id": f"scaling-threads-{threads}", + "artifacts": [f"reference/raw/v1/scaling-threads-{threads}.txt"], + } + for threads in (1, 2, 4) + ] + manifest["commands"] = record_commands(command_records, command_lines) + refresh_artifacts(manifest, generated) + return manifest + + +def validate_generated_json( + fixture_json: dict[str, Any], + reference_manifest: dict[str, Any], + scaling_manifest: dict[str, Any], +) -> None: + """Apply the package schemas before any generated bytes are published.""" + result_schema = load_json(ROOT / "reference" / "schema" / "result-v1.schema.json") + manifest_schema = load_json(ROOT / "reference" / "schema" / "run-manifest-v1.schema.json") + validate_document(fixture_json, result_schema, Path("generated fixture")) + validate_document( + reference_manifest, + manifest_schema, + Path("generated reference manifest"), + ) + validate_document( + scaling_manifest, + manifest_schema, + Path("generated scaling manifest"), + ) + + +def publish_artifacts(generated: dict[str, bytes]) -> None: + """Write all bytes to sibling temporaries before replacing any artifact.""" + pending: list[tuple[Path, Path]] = [] + try: + for relative_path, payload in generated.items(): + destination = ROOT / relative_path + destination.parent.mkdir(parents=True, exist_ok=True) + descriptor, temporary_name = tempfile.mkstemp(prefix=f".{destination.name}.", dir=destination.parent) + temporary = Path(temporary_name) + with os.fdopen(descriptor, "wb") as stream: + stream.write(payload) + stream.flush() + os.fsync(stream.fileno()) + pending.append((temporary, destination)) + for temporary, destination in pending: + temporary.replace(destination) + finally: + for temporary, _destination in pending: + temporary.unlink(missing_ok=True) + + +def regenerate( + fixture_binary: Path, + cdt_binary: Path, + initialize_binary: Path, + benchmark_binary: Path, + recorded_at_utc: str | None, +) -> None: + """Generate all raw outputs, then publish them with refreshed manifests.""" + revision = clean_source_revision() + paths = ( + executable(fixture_binary), + executable(cdt_binary), + executable(initialize_binary), + executable(benchmark_binary), + ) + fixture_command, bounded_command, persistence_command, benchmark_commands = producer_commands(*paths) + generated, fixture_json, scaling_records = produce_raw_artifacts( + revision, + fixture_command, + bounded_command, + persistence_command, + benchmark_commands, + ) + validate_platform_identity(fixture_json, scaling_records) + recorded = canonical_timestamp(recorded_at_utc) + reference_commands = [ + [display_path(fixture_binary)], + [display_path(cdt_binary), *bounded_command[1:]], + [display_path(initialize_binary), *persistence_command[1:]], + ] + scaling_commands = [[display_path(benchmark_binary), *command[1:]] for command in benchmark_commands] + reference_manifest = make_reference_manifest(generated, fixture_json, revision, recorded, reference_commands) + scaling_manifest = make_scaling_manifest(generated, scaling_records, revision, recorded, scaling_commands) + validate_generated_json(fixture_json, reference_manifest, scaling_manifest) + generated["reference/manifests/v1/macos-arm64.json"] = (json.dumps(reference_manifest, indent=2, ensure_ascii=False) + "\n").encode() + generated["reference/manifests/v1/scaling-macos-arm64.json"] = (json.dumps(scaling_manifest, indent=2, ensure_ascii=False) + "\n").encode() + publish_artifacts(generated) + print(f"Regenerated reference/raw/v1 and reference/manifests/v1 from {revision}.") + + +def main() -> int: + """Parse regeneration paths and publish the complete reference package.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--fixture-binary", + type=Path, + default=Path("out/build/reference/tests/CDT_reference_fixture"), + ) + parser.add_argument( + "--cdt-binary", + type=Path, + default=Path("out/build/reference/src/cdt"), + ) + parser.add_argument( + "--initialize-binary", + type=Path, + default=Path("out/build/reference/src/initialize"), + ) + parser.add_argument( + "--benchmark-binary", + type=Path, + default=Path("out/build/parallel/tests/CDT_cgal_benchmark"), + ) + parser.add_argument( + "--recorded-at-utc", + help="override the ISO-8601 manifest timestamp for controlled regeneration", + ) + parser.add_argument( + "--check-clean", + action="store_true", + help="verify the clean source precondition without running producers", + ) + arguments = parser.parse_args() + if arguments.check_clean: + revision = clean_source_revision() + print(f"Reference regeneration source is clean at {revision}.") + return 0 + regenerate( + arguments.fixture_binary, + arguments.cdt_binary, + arguments.initialize_binary, + arguments.benchmark_binary, + arguments.recorded_at_utc, + ) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (OSError, RuntimeError, TypeError, ValueError, subprocess.SubprocessError) as error: + print(f"error: {error}", file=sys.stderr) + raise SystemExit(1) from None diff --git a/scripts/tests/test_generate_reference_fixtures.py b/scripts/tests/test_generate_reference_fixtures.py new file mode 100644 index 000000000..19d540ff4 --- /dev/null +++ b/scripts/tests/test_generate_reference_fixtures.py @@ -0,0 +1,45 @@ +"""Regression tests for complete reference-package regeneration.""" + +from __future__ import annotations + +import unittest + +from scripts import generate_reference_fixtures as generator + + +class ReferenceFixtureGenerationTests(unittest.TestCase): + """Exercise regeneration metadata before any artifacts are published.""" + + def test_recorded_timestamp_is_canonical_utc(self) -> None: + """Equivalent explicit offsets produce one stable manifest spelling.""" + self.assertEqual( + generator.canonical_timestamp("2026-07-25T01:02:03-07:00"), + "2026-07-25T08:02:03Z", + ) + + def test_recorded_timestamp_requires_an_offset(self) -> None: + """An ambiguous local timestamp cannot enter archival provenance.""" + with self.assertRaisesRegex(ValueError, "UTC offset"): + generator.canonical_timestamp("2026-07-25T08:02:03") + + def test_platform_identity_must_match_manifest_names(self) -> None: + """A Linux run cannot silently overwrite macOS-arm64 manifests.""" + fixture = { + "implementation": { + "operating_system": "Linux", + "hardware": "x86_64", + } + } + scaling = [ + { + "build.system": "Linux", + "build.processor": "x86_64", + } + ] + + with self.assertRaisesRegex(RuntimeError, "macOS-arm64-specific"): + generator.validate_platform_identity(fixture, scaling) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_validate_reference_fixtures.py b/scripts/tests/test_validate_reference_fixtures.py new file mode 100644 index 000000000..3660cc6eb --- /dev/null +++ b/scripts/tests/test_validate_reference_fixtures.py @@ -0,0 +1,154 @@ +"""Regression tests for the cross-language reference-package validator.""" + +from __future__ import annotations + +import copy +import unittest +from pathlib import Path +from typing import override +from unittest import mock + +from scripts import validate_reference_fixtures as validator + + +class ReferenceFixtureValidationTests(unittest.TestCase): + """Exercise the validation gaps that previously admitted stale fixtures.""" + + @classmethod + @override + def setUpClass(cls) -> None: + """Load the committed package once; individual tests mutate copies.""" + cls.fixture_path = validator.REFERENCE / "fixtures" / "v1" / "protocol.json" + cls.result_path = validator.REFERENCE / "raw" / "v1" / "cpp-reference.json" + cls.fixture_schema_path = validator.REFERENCE / "schema" / "fixture-v1.schema.json" + cls.result_schema_path = validator.REFERENCE / "schema" / "result-v1.schema.json" + cls.end_to_end_path = validator.REFERENCE / "raw" / "v1" / "end-to-end.txt" + cls.manifest_path = validator.REFERENCE / "manifests" / "v1" / "macos-arm64.json" + cls.protocol = validator.load_json(cls.fixture_path) + cls.raw = validator.load_json(cls.result_path) + cls.manifest = validator.load_json(cls.manifest_path) + cls.fixture_schema = validator.load_json(cls.fixture_schema_path) + cls.result_schema = validator.load_json(cls.result_schema_path) + + def test_complete_schema_rejects_nested_unknown_fields(self) -> None: + """Nested entity shapes are enforced, not only top-level keys.""" + raw = copy.deepcopy(self.raw) + raw["states"][0]["vertices"][0]["unexpected"] = True + + with self.assertRaisesRegex(ValueError, "schema violation.*unexpected"): + validator.validate_document(raw, self.result_schema, Path("result.json")) + + def test_transition_site_must_resolve_in_raw_topology(self) -> None: + """A stale canonical entity id cannot survive protocol validation.""" + protocol = copy.deepcopy(self.protocol) + protocol["transitions"][0]["site"]["entity"] = "c99" + + with self.assertRaisesRegex(ValueError, "unknown cell id 'c99'"): + validator.validate_protocol(protocol, self.raw) + + def test_state_edges_must_be_derived_from_cells(self) -> None: + """A canonical-looking edge outside every cell is rejected.""" + state = copy.deepcopy(next(item for item in self.raw["states"] if item["id"] == "move-23-before")) + state["edges"][2]["vertices"] = ["v00", "v04"] + state["edges"].sort(key=lambda edge: edge["vertices"]) + for index, edge in enumerate(state["edges"]): + edge["id"] = f"e{index:02d}" + + with self.assertRaisesRegex(ValueError, "edge set does not equal cell incidence"): + validator.validate_state(state) + + def test_generated_payload_uses_declared_numerical_tolerances(self) -> None: + """Within-tolerance coordinate and action drift remains interoperable.""" + generated = copy.deepcopy(self.raw) + generated["states"][0]["vertices"][0]["position"][0] += 1e-15 + generated["actions"][0]["value"] += 1e-14 + + validator.compare_generated_scientific_payload(generated, self.raw, self.protocol) + + def test_generated_payload_rejects_out_of_tolerance_coordinates(self) -> None: + """Coordinates outside the named tolerance fail generated comparison.""" + generated = copy.deepcopy(self.raw) + generated["states"][0]["vertices"][0]["position"][0] += 1e-10 + + with self.assertRaisesRegex(ValueError, "coordinate tolerance"): + validator.compare_generated_scientific_payload(generated, self.raw, self.protocol) + + def test_generated_payload_keeps_topology_exact(self) -> None: + """Numerical tolerance never weakens exact incidence comparison.""" + generated = copy.deepcopy(self.raw) + generated["states"][0]["edges"][0]["type"] = "timelike" + + with self.assertRaisesRegex(ValueError, "exact topology or metadata"): + validator.compare_generated_scientific_payload(generated, self.raw, self.protocol) + + def test_manifest_records_one_producer_for_every_raw_artifact(self) -> None: + """A checksum alone cannot conceal missing regeneration provenance.""" + manifest = copy.deepcopy(self.manifest) + manifest["commands"][0]["artifacts"] = [] + + with self.assertRaisesRegex(ValueError, "exactly one producer"): + validator.validate_manifest(manifest) + + def test_manifest_commands_must_match_the_protocol(self) -> None: + """Recorded producer argv cannot drift from the retained experiment.""" + manifests = [ + copy.deepcopy(self.manifest), + validator.load_json(validator.REFERENCE / "manifests" / "v1" / "scaling-macos-arm64.json"), + ] + manifests[0]["commands"][1]["command_line"][2] = "-n3" + + with self.assertRaisesRegex(ValueError, "do not match"): + validator.validate_command_provenance(manifests, self.protocol) + + def test_archival_provenance_rejects_dirty_revisions(self) -> None: + """Archival validation requires one clean commit across all records.""" + raw = copy.deepcopy(self.raw) + manifests = [copy.deepcopy(self.manifest)] + dirty_revision = "a" * 40 + "-dirty" + raw["implementation"]["revision"] = dirty_revision + manifests[0]["implementation"]["source_revision"] = dirty_revision + + with ( + mock.patch.object( + validator, + "parse_key_value_record", + return_value={"implementation.revision": dirty_revision}, + ), + self.assertRaisesRegex(ValueError, "clean Git commit"), + ): + validator.validate_clean_provenance(raw, manifests) + + def test_proposal_probabilities_are_derived_from_raw_domains(self) -> None: + """Preserving a wrong Hastings ratio does not conceal scaled inputs.""" + protocol = copy.deepcopy(self.protocol) + transition = protocol["transitions"][0] + transition["proposal_probability"] *= 0.5 + transition["reverse_probability"] *= 0.5 + + with self.assertRaisesRegex(ValueError, "forward proposal probability"): + validator.validate_protocol(protocol, self.raw) + + def test_bounded_run_command_must_match_retained_output(self) -> None: + """A stale timeslice option is rejected against raw provenance.""" + protocol = copy.deepcopy(self.protocol) + command = protocol["bounded_run"]["command"] + command[command.index("-t2")] = "-t3" + + with self.assertRaisesRegex(ValueError, "desired timeslices"): + validator.validate_end_to_end(protocol, self.end_to_end_path) + + def test_bounded_run_f_vector_must_lie_inside_declared_band(self) -> None: + """The declared randomized band is executable acceptance data.""" + protocol = copy.deepcopy(self.protocol) + protocol["bounded_run"]["bands"]["randomized_cgal_f_vector"]["maximum"][0] = 5 + + with self.assertRaisesRegex(ValueError, "outside its declared band"): + validator.validate_end_to_end(protocol, self.end_to_end_path) + + def test_current_protocol_satisfies_its_complete_schema(self) -> None: + """The versioned protocol itself exercises the complete schema path.""" + validator.validate_document(self.protocol, self.fixture_schema, self.fixture_path) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/validate_reference_fixtures.py b/scripts/validate_reference_fixtures.py new file mode 100644 index 000000000..adf3362d2 --- /dev/null +++ b/scripts/validate_reference_fixtures.py @@ -0,0 +1,926 @@ +"""Validate the versioned CDT++ cross-language reference package.""" + +from __future__ import annotations + +import argparse +import hashlib +import itertools +import json +import math +import re +import subprocess +import sys +from pathlib import Path +from typing import Any + +from jsonschema import Draft202012Validator, FormatChecker +from jsonschema.exceptions import SchemaError, ValidationError + +ROOT = Path(__file__).resolve().parents[1] +REFERENCE = ROOT / "reference" + + +def reject_nonstandard_number(value: str) -> None: + """Reject NaN and infinities, which are not JSON numbers.""" + message = f"nonstandard JSON number {value!r}" + raise ValueError(message) + + +def load_json(path: Path) -> dict[str, Any]: + """Load one JSON object or raise a path-specific validation error.""" + try: + value = json.loads(path.read_text(encoding="utf-8"), parse_constant=reject_nonstandard_number) + except (OSError, json.JSONDecodeError, ValueError) as error: + message = f"{path}: invalid JSON: {error}" + raise ValueError(message) from error + if not isinstance(value, dict): + message = f"{path}: top-level JSON value must be an object" + raise TypeError(message) + return value + + +def validate_document(document: dict[str, Any], schema: dict[str, Any], path: Path) -> None: + """Apply one complete JSON Schema Draft 2020-12 contract.""" + try: + Draft202012Validator.check_schema(schema) + Draft202012Validator(schema, format_checker=FormatChecker()).validate(document) + except SchemaError as error: + location = ".".join(str(part) for part in error.absolute_schema_path) + message = f"{path}: invalid JSON schema at {location or ''}: {error.message}" + raise ValueError(message) from error + except ValidationError as error: + location = ".".join(str(part) for part in error.absolute_path) + message = f"{path}: schema violation at {location or ''}: {error.message}" + raise ValueError(message) from error + + +def is_close(actual: float, expected: float, tolerance: dict[str, Any]) -> bool: + """Compare one numerical quantity with named absolute and relative parts.""" + absolute = float(tolerance["absolute"]) + relative = float(tolerance["relative"]) + return abs(actual - expected) <= absolute + relative * abs(expected) + + +def expected_cell_type(times: list[int]) -> str: + """Classify one tetrahedron independently from production cell metadata.""" + minimum = min(times) + maximum = max(times) + if maximum - minimum != 1: + return "acausal" + return {3: "3-1", 2: "2-2", 1: "1-3"}.get(times.count(minimum), "unclassified") + + +def validate_entity_ids(entities: list[dict[str, Any]], prefix: str, state_id: str) -> None: + """Require dense canonical entity identifiers in array order.""" + actual = [entity["id"] for entity in entities] + expected = [f"{prefix}{index:02d}" for index in range(len(entities))] + if actual != expected: + message = f"{state_id}: noncanonical {prefix!r} identifiers" + raise ValueError(message) + + +def validate_state(state: dict[str, Any]) -> None: # noqa: C901, PLR0912, PLR0915 + """Validate ordering, incidence, adjacency, classification, and f-vector.""" + state_id = str(state["id"]) + vertices = state["vertices"] + edges = state["edges"] + facets = state["facets"] + cells = state["cells"] + validate_entity_ids(vertices, "v", state_id) + validate_entity_ids(edges, "e", state_id) + validate_entity_ids(facets, "f", state_id) + validate_entity_ids(cells, "c", state_id) + + f_vector = [len(vertices), len(edges), len(facets), len(cells)] + if state["f_vector"] != f_vector: + message = f"{state_id}: f-vector does not match entity arrays" + raise ValueError(message) + if f_vector[0] - f_vector[1] + f_vector[2] - f_vector[3] != 1: + message = f"{state_id}: finite complex does not have Euler characteristic 1" + raise ValueError(message) + + vertex_order = [(vertex["time"], *vertex["position"]) for vertex in vertices] + if vertex_order != sorted(vertex_order): + message = f"{state_id}: vertices are not in canonical order" + raise ValueError(message) + vertex_by_id = {vertex["id"]: vertex for vertex in vertices} + observed_times = [int(vertex["time"]) for vertex in vertices] + if state["time_bounds"] != [min(observed_times), max(observed_times)]: + message = f"{state_id}: time bounds do not match vertices" + raise ValueError(message) + + edge_keys: list[list[str]] = [] + for edge in edges: + endpoints = edge["vertices"] + if endpoints != sorted(endpoints) or any(endpoint not in vertex_by_id for endpoint in endpoints): + message = f"{state_id}: invalid edge incidence" + raise ValueError(message) + times = [vertex_by_id[endpoint]["time"] for endpoint in endpoints] + expected_type = "spacelike" if times[0] == times[1] else "timelike" + if edge["type"] != expected_type: + message = f"{state_id}: edge {edge['id']} has incorrect causal type" + raise ValueError(message) + edge_keys.append(endpoints) + if edge_keys != sorted(edge_keys) or len(edge_keys) != len({tuple(key) for key in edge_keys}): + message = f"{state_id}: edges are not unique and canonical" + raise ValueError(message) + + facet_keys: list[list[str]] = [] + for facet in facets: + incident = facet["vertices"] + if incident != sorted(incident) or any(vertex not in vertex_by_id for vertex in incident): + message = f"{state_id}: invalid facet incidence" + raise ValueError(message) + times = [vertex_by_id[vertex]["time"] for vertex in incident] + spacelike = len(set(times)) == 1 + if facet["spacelike"] is not spacelike: + message = f"{state_id}: facet {facet['id']} has incorrect causal type" + raise ValueError(message) + if spacelike and facet.get("time") != times[0]: + message = f"{state_id}: facet {facet['id']} has incorrect time" + raise ValueError(message) + facet_keys.append(incident) + if facet_keys != sorted(facet_keys) or len(facet_keys) != len({tuple(key) for key in facet_keys}): + message = f"{state_id}: facets are not unique and canonical" + raise ValueError(message) + + cell_keys: list[list[str]] = [] + for cell in cells: + incident = cell["vertices"] + if incident != sorted(incident) or len(incident) != len(set(incident)) or any(vertex not in vertex_by_id for vertex in incident): + message = f"{state_id}: invalid cell incidence" + raise ValueError(message) + cell_keys.append(incident) + if cell_keys != sorted(cell_keys) or len(cell_keys) != len({tuple(key) for key in cell_keys}): + message = f"{state_id}: cells are not unique and canonical" + raise ValueError(message) + + expected_edges = sorted({pair for cell in cell_keys for pair in itertools.combinations(cell, 2)}) + if [tuple(key) for key in edge_keys] != expected_edges: + message = f"{state_id}: edge set does not equal cell incidence" + raise ValueError(message) + expected_facets = sorted({triple for cell in cell_keys for triple in itertools.combinations(cell, 3)}) + if [tuple(key) for key in facet_keys] != expected_facets: + message = f"{state_id}: facet set does not equal cell incidence" + raise ValueError(message) + + for index, cell in enumerate(cells): + incident = cell["vertices"] + times = [int(vertex_by_id[vertex]["time"]) for vertex in incident] + if cell["type"] != expected_cell_type(times): + message = f"{state_id}: cell {cell['id']} has incorrect simplex type" + raise ValueError(message) + expected_adjacency = [cells[other]["id"] for other in range(len(cells)) if other != index and len(set(incident) & set(cells[other]["vertices"])) == 3] + if cell["adjacent_cells"] != expected_adjacency: + message = f"{state_id}: cell {cell['id']} has incorrect adjacency" + raise ValueError(message) + + +def generalized_action(counts: dict[str, Any], parameters: dict[str, Any]) -> float: + """Evaluate the published generalized action independently with libm.""" + n1 = float(counts["n1_timelike"]) + n31 = float(counts["n3_31_13"]) + n22 = float(counts["n3_22"]) + alpha = float(parameters["alpha"]) + coupling = float(parameters["k"]) + cosmological = float(parameters["lambda"]) + root_alpha = math.sqrt(alpha) + denominator = 4.0 * alpha + 1.0 + three_one = ( + -3.0 * coupling * math.asinh(1.0 / (math.sqrt(3.0) * math.sqrt(denominator))) + - 3.0 * coupling * root_alpha * math.acos((2.0 * alpha + 1.0) / denominator) + - cosmological * math.sqrt(3.0 * alpha + 1.0) / 12.0 + ) + two_two = ( + 2.0 * coupling * math.asinh(2.0 * math.sqrt(2.0) * math.sqrt(2.0 * alpha + 1.0) / denominator) + - 4.0 * coupling * root_alpha * math.acos(-1.0 / denominator) + - cosmological * math.sqrt(4.0 * alpha + 2.0) / 12.0 + ) + return 2.0 * math.pi * coupling * root_alpha * n1 + n31 * three_one + n22 * two_two + + +def validate_actions(raw: dict[str, Any], protocol: dict[str, Any]) -> None: + """Compare raw C++ actions with independent closed-form calculations.""" + tolerance = protocol["tolerances"]["regge_action_closed_form"] + for action in raw["actions"]: + counts = action["counts"] + parameters = action["parameters"] + n1 = float(counts["n1_timelike"]) + n31 = float(counts["n3_31_13"]) + n22 = float(counts["n3_22"]) + coupling = float(parameters["k"]) + cosmological = float(parameters["lambda"]) + if action["id"] == "alpha-minus-one-imaginary-coefficient": + expected = -2.0 * math.pi * coupling * n1 + n31 * (2.673 * coupling + 0.118 * cosmological) + n22 * (7.386 * coupling + 0.118 * cosmological) + elif action["id"] == "alpha-one-rounded": + expected = 2.0 * math.pi * coupling * n1 + n31 * (-3.548 * coupling - 0.167 * cosmological) + n22 * (-5.355 * coupling - 0.204 * cosmological) + elif action["id"] == "alpha-generalized": + expected = generalized_action(counts, parameters) + else: + message = f"unknown action fixture {action['id']!r}" + raise ValueError(message) + if not is_close(float(action["value"]), expected, tolerance): + message = f"{action['id']}: raw action does not match independent oracle" + raise ValueError(message) + + +def validate_causality_filter(raw: dict[str, Any], protocol: dict[str, Any]) -> None: + """Check the explicit acausal input and its failure-atomic repair result.""" + states = {state["id"]: state for state in raw["states"]} + fixture = next(item for item in protocol["state_fixtures"] if item["id"] == "causality-filter") + before = states[fixture["input_state"]] + after = states[fixture["expected_state"]] + removed = fixture["removed_vertex"] + + def vertex_value(vertex: dict[str, Any]) -> tuple[tuple[float, ...], int]: + return tuple(float(value) for value in vertex["position"]), int(vertex["time"]) + + removed_value = ( + tuple(float(value) for value in removed["position"]), + int(removed["time"]), + ) + before_values = [vertex_value(vertex) for vertex in before["vertices"]] + after_values = [vertex_value(vertex) for vertex in after["vertices"]] + if before_values.count(removed_value) != 1: + message = "causality-filter input does not contain the unique declared bad vertex" + raise ValueError(message) + if after_values != [value for value in before_values if value != removed_value]: + message = "causality-filter output does not remove exactly the declared bad vertex" + raise ValueError(message) + if not any(cell["type"] == "acausal" for cell in before["cells"]): + message = "causality-filter input does not expose an acausal cell" + raise ValueError(message) + if any(cell["type"] == "acausal" for cell in after["cells"]): + message = "causality-filter output still contains an acausal cell" + raise ValueError(message) + actual_delta = [right - left for left, right in zip(before["f_vector"], after["f_vector"], strict=True)] + if actual_delta != [-1, -3, -3, -1]: + message = "causality-filter output has an unexpected exact f-vector delta" + raise ValueError(message) + + +REVERSE_MOVE = { + "2-3": "3-2", + "3-2": "2-3", + "2-6": "6-2", + "6-2": "2-6", + "4-4": "4-4", +} + + +def entity_by_id(state: dict[str, Any], collection: str, entity_id: str) -> dict[str, Any]: + """Resolve one canonical entity or reject a stale protocol identifier.""" + entity = next((item for item in state[collection] if item["id"] == entity_id), None) + if entity is None: + message = f"{state['id']}: unknown {collection[:-1]} id {entity_id!r}" + raise ValueError(message) + return entity + + +def proposal_site_count(state: dict[str, Any], move: str) -> int: + """Derive the raw proposal-domain size independently from canonical state.""" + if move == "2-3": + return sum(cell["type"] == "2-2" for cell in state["cells"]) + if move == "3-2": + return sum(edge["type"] == "timelike" for edge in state["edges"]) + if move == "2-6": + return sum(cell["type"] == "1-3" for cell in state["cells"]) + if move == "6-2": + return len(state["vertices"]) + if move == "4-4": + return sum(edge["type"] == "spacelike" for edge in state["edges"]) + message = f"unknown move type {move!r}" + raise ValueError(message) + + +def proposal_probability(state: dict[str, Any], move: str) -> float: + """Return the declared uniform-move, uniform-raw-site probability.""" + site_count = proposal_site_count(state, move) + if site_count <= 0: + message = f"{state['id']}: {move} has an empty raw proposal domain" + raise ValueError(message) + return 1.0 / (5.0 * site_count) + + +def validate_cell_site(transition: dict[str, Any], before: dict[str, Any]) -> None: + """Validate a cell-based proposal site.""" + move = transition["move"] + site = transition["site"] + cell = entity_by_id(before, "cells", str(site["entity"])) + expected_type = "2-2" if move == "2-3" else "1-3" + if cell["type"] != expected_type or site["vertices"] != cell["vertices"]: + message = f"{transition['id']}: declared cell site does not match raw topology" + raise ValueError(message) + if move == "2-6": + facet = entity_by_id(before, "facets", str(site["shared_spacelike_facet"])) + if not facet["spacelike"] or not set(facet["vertices"]).issubset(cell["vertices"]): + message = f"{transition['id']}: shared facet is not spacelike incidence of the proposed cell" + raise ValueError(message) + + +def validate_edge_site(transition: dict[str, Any], before: dict[str, Any], proposed: dict[str, Any]) -> None: + """Validate an edge-based proposal site.""" + move = transition["move"] + site = transition["site"] + edge = entity_by_id(before, "edges", str(site["entity"])) + expected_type = "timelike" if move == "3-2" else "spacelike" + if edge["type"] != expected_type or site["vertices"] != edge["vertices"]: + message = f"{transition['id']}: declared edge site does not match raw topology" + raise ValueError(message) + if move == "4-4": + reverse_vertices = site["reverse_vertices"] + if not any(candidate["vertices"] == reverse_vertices and candidate["type"] == "spacelike" for candidate in proposed["edges"]): + message = f"{transition['id']}: proposed state does not contain the declared reverse edge" + raise ValueError(message) + + +def validate_vertex_site(transition: dict[str, Any], before: dict[str, Any]) -> None: + """Validate a degree-five vertex proposal site.""" + site = transition["site"] + vertex_id = str(site["entity"]) + vertex = entity_by_id(before, "vertices", vertex_id) + if site["vertices"] != [vertex["id"]]: + message = f"{transition['id']}: declared vertex site does not match raw topology" + raise ValueError(message) + degree = sum(vertex_id in edge["vertices"] for edge in before["edges"]) + if degree != 5: + message = f"{transition['id']}: removal site has degree {degree}, expected 5" + raise ValueError(message) + + +def validate_transition_site(transition: dict[str, Any], before: dict[str, Any], proposed: dict[str, Any]) -> None: + """Bind a declared move site to the exact raw topology it names.""" + move = str(transition["move"]) + expected_kind = { + "2-3": "2-2-cell", + "3-2": "timelike-edge", + "2-6": "1-3-cell", + "6-2": "degree-five-vertex", + "4-4": "spacelike-edge", + }[move] + if transition["site"]["kind"] != expected_kind: + message = f"{transition['id']}: site kind does not match move {move}" + raise ValueError(message) + if move in {"2-3", "2-6"}: + validate_cell_site(transition, before) + elif move in {"3-2", "4-4"}: + validate_edge_site(transition, before, proposed) + else: + validate_vertex_site(transition, before) + + +def state_action(state: dict[str, Any], parameters: dict[str, Any]) -> float: + """Derive the action from raw causal counts rather than protocol deltas.""" + counts = { + "n1_timelike": sum(edge["type"] == "timelike" for edge in state["edges"]), + "n3_31_13": sum(cell["type"] in {"3-1", "1-3"} for cell in state["cells"]), + "n3_22": sum(cell["type"] == "2-2" for cell in state["cells"]), + } + return generalized_action(counts, parameters) + + +def validate_protocol(protocol: dict[str, Any], raw: dict[str, Any]) -> None: + """Validate exact move deltas and independent Metropolis decisions.""" + states = {state["id"]: state for state in raw["states"]} + expected_delta = { + "2-3": [0, 1, 2, 1], + "3-2": [0, -1, -2, -1], + "2-6": [1, 5, 8, 4], + "6-2": [-1, -5, -8, -4], + "4-4": [0, 0, 0, 0], + } + probability_tolerance = protocol["tolerances"]["acceptance_probability"] + observed_moves: set[str] = set() + for transition in protocol["transitions"]: + move = transition["move"] + observed_moves.add(move) + before = states[transition["before"]] + proposed = states[transition["proposed"]] + validate_transition_site(transition, before, proposed) + actual_delta = [right - left for left, right in zip(before["f_vector"], proposed["f_vector"], strict=True)] + if actual_delta != expected_delta[move]: + message = f"{transition['id']}: incorrect exact f-vector delta" + raise ValueError(message) + expected_forward = proposal_probability(before, move) + expected_reverse = proposal_probability(proposed, REVERSE_MOVE[move]) + if not is_close(float(transition["proposal_probability"]), expected_forward, probability_tolerance): + message = f"{transition['id']}: forward proposal probability does not match the raw proposal domain" + raise ValueError(message) + if not is_close(float(transition["reverse_probability"]), expected_reverse, probability_tolerance): + message = f"{transition['id']}: reverse proposal probability does not match the raw proposal domain" + raise ValueError(message) + expected_action_delta = state_action(before, transition["parameters"]) - state_action(proposed, transition["parameters"]) + action_tolerance = protocol["tolerances"]["regge_action_closed_form"] + if not is_close(float(transition["action_delta"]), expected_action_delta, action_tolerance): + message = f"{transition['id']}: action delta does not match independently derived raw-state actions" + raise ValueError(message) + expected_probability = min( + 1.0, + expected_reverse / expected_forward * math.exp(expected_action_delta), + ) + if not is_close(float(transition["acceptance_probability"]), expected_probability, probability_tolerance): + message = f"{transition['id']}: incorrect Metropolis-Hastings probability" + raise ValueError(message) + accepted = float(transition["acceptance_variate"]) <= expected_probability + if transition["accepted"] is not accepted: + message = f"{transition['id']}: accept/reject decision is inconsistent" + raise ValueError(message) + committed = transition["proposed"] if accepted else transition["before"] + if transition["committed"] != committed: + message = f"{transition['id']}: committed state is inconsistent" + raise ValueError(message) + if observed_moves != set(expected_delta): + message = "transition protocol does not cover every supported 3D move" + raise ValueError(message) + + +def file_digest(path: Path) -> str: + """Return a lowercase SHA-256 digest for one artifact.""" + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def validate_manifest(manifest: dict[str, Any]) -> None: + """Check that every manifested local artifact exists and matches SHA-256.""" + artifact_paths = {artifact["path"] for artifact in manifest["artifacts"]} + command_ids = [command["id"] for command in manifest["commands"]] + if len(command_ids) != len(set(command_ids)): + message = "manifest producer command ids must be unique" + raise ValueError(message) + command_artifacts = [artifact for command in manifest["commands"] for artifact in command["artifacts"]] + if len(command_artifacts) != len(set(command_artifacts)): + message = "manifest maps one generated artifact to multiple producer commands" + raise ValueError(message) + if not set(command_artifacts).issubset(artifact_paths): + message = "manifest producer command names an undeclared artifact" + raise ValueError(message) + generated_artifacts = {path for path in artifact_paths if path.startswith("reference/raw/")} + if set(command_artifacts) != generated_artifacts: + message = "manifest does not record exactly one producer for every raw artifact" + raise ValueError(message) + + for artifact in manifest["artifacts"]: + path = (ROOT / artifact["path"]).resolve() + if not path.is_relative_to(REFERENCE.resolve()): + message = f"manifest artifact escapes the reference directory: {path}" + raise ValueError(message) + if not path.is_file(): + message = f"manifest artifact does not exist: {path}" + raise ValueError(message) + if file_digest(path) != artifact["sha256"]: + message = f"manifest artifact checksum mismatch: {path}" + raise ValueError(message) + + +def validate_command_provenance( + manifests: list[dict[str, Any]], + protocol: dict[str, Any], +) -> None: + """Require exact producer argv for every raw artifact family.""" + command_entries = [command for manifest in manifests for command in manifest["commands"]] + commands = {command["id"]: command["command_line"] for command in command_entries} + if len(commands) != len(command_entries): + message = "producer command ids must be unique across manifests" + raise ValueError(message) + expected = { + "canonical-fixture": [ + "out/build/reference/tests/CDT_reference_fixture", + ], + "bounded-end-to-end": protocol["bounded_run"]["command"], + "persistence-roundtrip": [ + "out/build/reference/src/initialize", + "-s", + "-n2", + "-t2", + "-o", + "--seed", + "92", + "--threads", + "1", + ], + **{ + f"scaling-threads-{threads}": [ + "out/build/parallel/tests/CDT_cgal_benchmark", + "640", + "5", + "50", + str(threads), + "1", + ] + for threads in (1, 2, 4) + }, + } + if commands != expected: + message = "manifest producer commands do not match the reference protocol" + raise ValueError(message) + + +def fnv1a64(payload: bytes) -> str: + """Return the lowercase 64-bit FNV-1a spelling used by persistence.""" + value = 14695981039346656037 + for byte in payload: + value ^= byte + value = value * 1099511628211 & 0xFFFFFFFFFFFFFFFF + return f"{value:016x}" + + +def validate_persistence() -> None: + """Verify the committed payload against its raw C++ sidecar.""" + payload_path = REFERENCE / "raw" / "v1" / "persistence-v1.off" + metadata_path = REFERENCE / "raw" / "v1" / "persistence-v1.off.meta" + payload = payload_path.read_bytes() + metadata = dict(line.split("=", maxsplit=1) for line in metadata_path.read_text(encoding="utf-8").splitlines() if "=" in line) + if int(metadata["payload.size"]) != len(payload): + message = "persistence payload size does not match its sidecar" + raise ValueError(message) + if metadata["payload.fnv1a64"] != fnv1a64(payload): + message = "persistence payload FNV-1a digest does not match its sidecar" + raise ValueError(message) + if metadata["placement.fnv1a64"] == metadata["topology.fnv1a64"]: + message = "placement and topology fingerprints must be separately derived" + raise ValueError(message) + + +def command_option(command: list[str], short_name: str, long_name: str | None = None) -> str: + """Read one required option from either joined or separated CLI spelling.""" + names = (short_name,) if long_name is None else (short_name, long_name) + for index, token in enumerate(command): + for name in names: + if token == name: + if index + 1 >= len(command): + message = f"command option {name!r} has no value" + raise ValueError(message) + return command[index + 1] + if token.startswith(name) and token != name: + return token[len(name) :] + message = f"command does not declare required option {short_name!r}" + raise ValueError(message) + + +def output_number(output: str, label: str) -> float: + """Parse one scalar from the bounded-run output.""" + match = re.search(rf"^{re.escape(label)}:\s+([^\s]+)$", output, flags=re.MULTILINE) + if match is None: + message = f"bounded run does not report {label!r}" + raise ValueError(message) + return float(match.group(1)) + + +def validate_bounded_run_command(bounded_run: dict[str, Any], output: str, path: Path) -> None: + """Match every scientific CLI value to its retained textual record.""" + command = bounded_run["command"] + expected_text = ( + f"Number of desired simplices: {command_option(command, '-n', '--simplices')}", + f"Number of desired timeslices: {command_option(command, '-t', '--timeslices')}", + f"Number of passes: {command_option(command, '-p', '--passes')}", + f"Checkpoint every {command_option(command, '-c', '--checkpoint')} passes.", + f"Effective random seed: {command_option(command, '--seed')}", + f"Maximum Delaunay threads: {command_option(command, '--threads')}", + ) + for text in expected_text: + if text not in output: + message = f"{path}: output contradicts declared command field {text!r}" + raise ValueError(message) + + expected_parameters = { + "Alpha": float(command_option(command, "-a", "--alpha")), + "K": float(command_option(command, "-k")), + "Lambda": float(command_option(command, "-l", "--lambda")), + } + for label, expected in expected_parameters.items(): + if output_number(output, label) != expected: + message = f"{path}: output {label!r} contradicts the declared command" + raise ValueError(message) + + +def bounded_run_final_f_vector(output: str) -> list[int]: + """Return the final, rather than initial, reported manifold counts.""" + matches = re.findall(r"Manifold has (\d+) vertices and (\d+) edges and (\d+) faces and (\d+) simplices\.", output) + if not matches: + message = "bounded run does not report a final manifold f-vector" + raise ValueError(message) + return [int(value) for value in matches[-1]] + + +def validate_end_to_end(protocol: dict[str, Any], path: Path | None = None) -> None: + """Check bounded-run command provenance, bands, and accounting identities.""" + if path is None: + path = REFERENCE / "raw" / "v1" / "end-to-end.txt" + output = path.read_text(encoding="utf-8") + bounded_run = protocol["bounded_run"] + command = bounded_run["command"] + validate_bounded_run_command(bounded_run, output, path) + required = ( + f"Effective random seed: {command_option(command, '--seed')} (stream 1).", + "There were 7 proposed moves with 0 accepted moves and 7 rejected moves.", + "There were 7 candidate construction attempts with 0 successful candidates and 7 failed candidates.", + ) + for text in required: + if text not in output: + message = f"{path}: missing bounded-run evidence {text!r}" + raise ValueError(message) + f_vector = bounded_run_final_f_vector(output) + n0, n1, n2, n3 = f_vector + if n0 - n1 + n2 - n3 != 1: + message = "bounded run final manifold violates the Euler relation" + raise ValueError(message) + band = bounded_run["bands"]["randomized_cgal_f_vector"] + if any(value < minimum or value > maximum for value, minimum, maximum in zip(f_vector, band["minimum"], band["maximum"], strict=True)): + message = f"bounded run final f-vector {f_vector} lies outside its declared band" + raise ValueError(message) + + +def parse_key_value_record(path: Path) -> dict[str, str]: + """Read one raw benchmark record without changing or normalizing it.""" + return dict(line.split("=", maxsplit=1) for line in path.read_text(encoding="utf-8").splitlines() if "=" in line) + + +def validate_scaling_records() -> None: + """Verify #88 records share one matched protocol and retain raw samples.""" + records = [parse_key_value_record(REFERENCE / "raw" / "v1" / f"scaling-threads-{threads}.txt") for threads in (1, 2, 4)] + matched_keys = ( + "record.schema", + "implementation.revision", + "build.compiler_id", + "build.compiler_version", + "build.configuration", + "build.system", + "build.processor", + "dependency.cgal_version", + "dependency.tbb_version", + "fixture.id", + "random.seed", + "requested_simplices", + "timeslices", + "generated_points", + "warmups", + "repetitions", + "moves_per_repetition", + "sample.unit", + ) + for key in matched_keys: + if len({record[key] for record in records}) != 1: + message = f"scaling records do not match on {key!r}" + raise ValueError(message) + for threads, record in zip((1, 2, 4), records, strict=True): + if int(record["requested_threads"]) != threads or int(record["active_threads"]) != threads: + message = f"scaling record does not enforce the requested {threads}-thread limit" + raise ValueError(message) + repetitions = int(record["repetitions"]) + for key, value in record.items(): + if key.endswith("_ns_samples") and len(value.split(",")) != repetitions: + message = f"{key}: raw sample count does not match repetitions" + raise ValueError(message) + + +def reference_revisions( + raw: dict[str, Any], + manifests: list[dict[str, Any]], +) -> set[str]: + """Collect every source revision recorded by the reference package.""" + return { + str(raw["implementation"]["revision"]), + *(str(manifest["implementation"]["source_revision"]) for manifest in manifests), + *(parse_key_value_record(REFERENCE / "raw" / "v1" / f"scaling-threads-{threads}.txt")["implementation.revision"] for threads in (1, 2, 4)), + } + + +def validate_provenance_consistency( + raw: dict[str, Any], + manifests: list[dict[str, Any]], +) -> str: + """Require every artifact family to name the same source revision.""" + revisions = reference_revisions(raw, manifests) + if len(revisions) != 1: + message = "reference artifacts do not share one source revision" + raise ValueError(message) + return revisions.pop() + + +def validate_clean_provenance( + raw: dict[str, Any], + manifests: list[dict[str, Any]], +) -> None: + """Require archival artifacts to identify one clean source commit.""" + revision = validate_provenance_consistency(raw, manifests) + if revision.endswith("-dirty") or re.fullmatch(r"[0-9a-f]{40}", revision) is None: + message = "archival reference artifacts must name one clean Git commit" + raise ValueError(message) + + +def parse_json_object(payload: str, source: str) -> dict[str, Any]: + """Parse a generated JSON object without publishing an intermediate file.""" + try: + value = json.loads(payload, parse_constant=reject_nonstandard_number) + except (json.JSONDecodeError, ValueError) as error: + message = f"{source}: invalid JSON: {error}" + raise ValueError(message) from error + if not isinstance(value, dict): + message = f"{source}: top-level JSON value must be an object" + raise TypeError(message) + return value + + +def compare_generated_vertex( + generated: dict[str, Any], + committed: dict[str, Any], + tolerance: dict[str, Any], +) -> None: + """Compare one vertex's exact metadata and tolerant coordinates.""" + generated_exact = {key: value for key, value in generated.items() if key != "position"} + committed_exact = {key: value for key, value in committed.items() if key != "position"} + if generated_exact != committed_exact: + message = f"generated reference vertex {committed['id']!r} differs in exact metadata" + raise ValueError(message) + for coordinate, expected in zip(generated["position"], committed["position"], strict=True): + if not is_close(float(coordinate), float(expected), tolerance): + message = f"generated reference vertex {committed['id']!r} differs outside the coordinate tolerance" + raise ValueError(message) + + +def compare_generated_state( + generated: dict[str, Any], + committed: dict[str, Any], + tolerance: dict[str, Any], +) -> None: + """Compare one state's exact topology and tolerant coordinates.""" + generated_exact = {key: value for key, value in generated.items() if key != "vertices"} + committed_exact = {key: value for key, value in committed.items() if key != "vertices"} + if generated_exact != committed_exact: + message = f"generated reference state {committed['id']!r} differs in exact topology or metadata" + raise ValueError(message) + generated_vertices = generated["vertices"] + committed_vertices = committed["vertices"] + if len(generated_vertices) != len(committed_vertices): + message = f"generated reference state {committed['id']!r} differs in vertex count" + raise ValueError(message) + for generated_vertex, committed_vertex in zip(generated_vertices, committed_vertices, strict=True): + compare_generated_vertex(generated_vertex, committed_vertex, tolerance) + + +def compare_generated_action( + generated: dict[str, Any], + committed: dict[str, Any], + tolerance: dict[str, Any], +) -> None: + """Compare one action's exact inputs and tolerant numerical result.""" + generated_exact = {key: value for key, value in generated.items() if key != "value"} + committed_exact = {key: value for key, value in committed.items() if key != "value"} + if generated_exact != committed_exact: + message = f"generated reference action {committed['id']!r} differs in exact inputs" + raise ValueError(message) + if not is_close(float(generated["value"]), float(committed["value"]), tolerance): + message = f"generated reference action {committed['id']!r} differs outside the action tolerance" + raise ValueError(message) + + +def compare_generated_scientific_payload( + generated: dict[str, Any], + committed: dict[str, Any], + protocol: dict[str, Any], +) -> None: + """Compare exact topology and protocol-designated numerical quantities.""" + generated_states = generated["states"] + committed_states = committed["states"] + if len(generated_states) != len(committed_states): + message = "generated reference states differ in count" + raise ValueError(message) + coordinate_tolerance = protocol["tolerances"]["coordinates"] + for generated_state, committed_state in zip(generated_states, committed_states, strict=True): + compare_generated_state(generated_state, committed_state, coordinate_tolerance) + + generated_actions = generated["actions"] + committed_actions = committed["actions"] + if len(generated_actions) != len(committed_actions): + message = "generated reference actions differ in count" + raise ValueError(message) + action_tolerance = protocol["tolerances"]["regge_action_closed_form"] + for generated_action, committed_action in zip(generated_actions, committed_actions, strict=True): + compare_generated_action(generated_action, committed_action, action_tolerance) + + +def validate_generated_fixture( + fixture_binary: Path, + committed: dict[str, Any], + result_schema: dict[str, Any], + protocol: dict[str, Any], +) -> None: + """Compare generated scientific fields while ignoring host provenance.""" + binary = fixture_binary if fixture_binary.is_absolute() else ROOT / fixture_binary + if not binary.is_file(): + message = f"reference fixture binary does not exist: {binary}" + raise ValueError(message) + completed = subprocess.run( # noqa: S603 + [str(binary)], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + timeout=60, + ) + if completed.returncode != 0: + message = f"reference fixture generator failed with exit {completed.returncode}: {completed.stderr.strip()}" + raise RuntimeError(message) + generated = parse_json_object(completed.stdout, str(binary)) + validate_document(generated, result_schema, binary) + compare_generated_scientific_payload(generated, committed, protocol) + + +def main( # noqa: C901 + fixture_binary: Path | None = None, + *, + generated_only: bool = False, + provenance_only: bool = False, + require_clean_provenance: bool = False, +) -> int: + """Validate schemas, protocol data, raw results, and provenance.""" + fixture_path = REFERENCE / "fixtures" / "v1" / "protocol.json" + result_path = REFERENCE / "raw" / "v1" / "cpp-reference.json" + result_schema = load_json(REFERENCE / "schema" / "result-v1.schema.json") + protocol = load_json(fixture_path) + raw = load_json(result_path) + if generated_only: + if fixture_binary is None: + message = "--generated-only requires --fixture-binary" + raise ValueError(message) + validate_generated_fixture(fixture_binary, raw, result_schema, protocol) + print("Generated reference fixture matches the committed scientific payload.") + return 0 + + manifest_paths = sorted((REFERENCE / "manifests" / "v1").glob("*.json")) + manifests = [load_json(path) for path in manifest_paths] + if provenance_only: + validate_clean_provenance(raw, manifests) + print("Reference artifacts identify one clean source commit.") + return 0 + + fixture_schema = load_json(REFERENCE / "schema" / "fixture-v1.schema.json") + manifest_schema = load_json(REFERENCE / "schema" / "run-manifest-v1.schema.json") + + validate_document(protocol, fixture_schema, fixture_path) + validate_document(raw, result_schema, result_path) + for path, manifest in zip(manifest_paths, manifests, strict=True): + validate_document(manifest, manifest_schema, path) + for schema in (fixture_schema, result_schema, manifest_schema): + if schema.get("$schema") != "https://json-schema.org/draft/2020-12/schema": + message = "reference schemas must declare JSON Schema Draft 2020-12" + raise ValueError(message) + + state_ids = [state["id"] for state in raw["states"]] + if len(state_ids) != len(set(state_ids)): + message = "raw result contains duplicate state ids" + raise ValueError(message) + for state in raw["states"]: + validate_state(state) + validate_causality_filter(raw, protocol) + validate_actions(raw, protocol) + validate_protocol(protocol, raw) + for manifest in manifests: + validate_manifest(manifest) + validate_command_provenance(manifests, protocol) + validate_provenance_consistency(raw, manifests) + validate_persistence() + validate_end_to_end(protocol) + validate_scaling_records() + if require_clean_provenance: + validate_clean_provenance(raw, manifests) + if fixture_binary is not None: + validate_generated_fixture(fixture_binary, raw, result_schema, protocol) + print("Reference fixture package is valid.") + return 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--fixture-binary", + type=Path, + help="run this C++ generator and compare its canonical states and actions", + ) + parser.add_argument( + "--generated-only", + action="store_true", + help="skip offline package checks and compare only generated scientific data", + ) + parser.add_argument( + "--provenance-only", + action="store_true", + help="skip offline package checks and require one clean source revision", + ) + parser.add_argument( + "--require-clean-provenance", + action="store_true", + help="reject dirty or inconsistent source revisions for archival use", + ) + arguments = parser.parse_args() + try: + raise SystemExit( + main( + arguments.fixture_binary, + generated_only=arguments.generated_only, + provenance_only=arguments.provenance_only, + require_clean_provenance=arguments.require_clean_provenance, + ) + ) + except (OSError, RuntimeError, TypeError, ValueError, subprocess.SubprocessError) as error: + print(f"error: {error}", file=sys.stderr) + raise SystemExit(1) from None diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b99b1ac51..b170c4954 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -80,7 +80,8 @@ add_cli_failure_test(cdt-checkpoint-zero cdt "Checkpoint interval must be positi -l0.1 -c0 --seed 92) add_cli_failure_test(cdt-radius-zero cdt "Initial radius must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 -i0 --seed 92) add_cli_failure_test(cdt-spacing-zero cdt "Foliation spacing must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 -f0 --seed 92) -add_cli_failure_test(cdt-empty-population cdt "would create an empty triangulation" -s -n3 -t2 -a0.6 -k1.1 -l0.1 --seed 92) +add_cli_failure_test(cdt-empty-population cdt "Initial radius is too small to populate the first timeslice." -s -n64 + -t3 -i0.1 -a0.6 -k1.1 -l0.1 --seed 92) add_cli_failure_test(cdt-integer-narrowing cdt "exceeds the supported integer range" -s -n2147483648 -t3 -a0.6 -k1.1 -l0.1 --seed 92) add_cli_failure_test(cdt-threads-zero cdt "Thread count must be positive." -s -n64 -t3 -a0.6 -k1.1 -l0.1 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f5c83ea14..a9d585ec8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -143,6 +143,20 @@ target_link_libraries( spdlog::spdlog_header_only CGAL::CGAL) +# Emit the versioned, language-neutral raw C++ oracle consumed by the +# repository-owned issue #94 fixture package. This is intentionally a manual +# artifact generator rather than a performance or correctness test. +add_executable(CDT_reference_fixture Reference_fixture.cpp) +target_compile_features(CDT_reference_fixture PRIVATE cxx_std_23) +target_link_libraries( + CDT_reference_fixture + PRIVATE project_options + project_warnings + date::date-tz + fmt::fmt-header-only + spdlog::spdlog_header_only + CGAL::CGAL) + if(ENABLE_PARALLEL_TRIANGULATION) add_test(NAME cdt-parallel-triangulation COMMAND $) diff --git a/tests/Metropolis_test.cpp b/tests/Metropolis_test.cpp index ec0119adb..14f0d718c 100644 --- a/tests/Metropolis_test.cpp +++ b/tests/Metropolis_test.cpp @@ -313,6 +313,10 @@ SCENARIO("Metropolis member functions" * doctest::test_suite("metropolis")) Metropolis_3(0.5L, K, Lambda, passes, output_every_n_passes, true, cdt::RandomSeed{92}), std::domain_error); + CHECK_THROWS_AS( + Metropolis_3(std::nextafter(0.5L, 0.0L), K, Lambda, passes, + output_every_n_passes, true, cdt::RandomSeed{92}), + std::domain_error); CHECK_THROWS_AS( Metropolis_3(std::numeric_limits::infinity(), K, Lambda, passes, output_every_n_passes, true, @@ -360,6 +364,69 @@ SCENARIO("Metropolis-Hastings proposal and acceptance ratios" * } } + GIVEN("A nonzero-action (2,3) fixture with alpha=1, k=0, and lambda=1.") + { + Metropolis_3 nonzero_action_strategy(1.0L, 0.0L, 1.0L, 1, 1, false, + cdt::RandomSeed{94}); + Geometry_3 current; + current.N3_22 = 4; + Geometry_3 proposed; + proposed.N1_TL = 10; + proposed.N3_22 = 5; + + auto const volume_delta = std::sqrt(6.0L) / 12.0L; + auto const expected_forward = 0.4L * std::exp(volume_delta); + + WHEN("The forward probability is evaluated independently.") + { + auto const actual = nonzero_action_strategy.acceptance_probability( + current, proposed, move_tracker::MoveType::TWO_THREE); + + THEN("The Hastings factor and action exponential both contribute.") + { + CHECK_LT(expected_forward, 1.0L); + CHECK(mpfr_values::to_long_double(actual) == + doctest::Approx(expected_forward).epsilon(1.0e-15L)); + } + } + + WHEN("The inverse probability is evaluated.") + { + auto const actual = nonzero_action_strategy.acceptance_probability( + proposed, current, move_tracker::MoveType::THREE_TWO); + auto const unclamped = 2.5L * std::exp(-volume_delta); + + THEN("The reverse proposal and action factors invert and clamp at one.") + { + CHECK_GT(unclamped, 1.0L); + CHECK_EQ(mpfr_values::to_long_double(actual), 1.0L); + } + } + } + + GIVEN("A numerically extreme nonzero-action delta.") + { + Metropolis_3 extreme_action_strategy(1.0L, 0.0L, 1.0e6L, 1, 1, false, + cdt::RandomSeed{94}); + Geometry_3 current; + current.N1_SL = 1; + current.N3_22 = 1'000; + Geometry_3 proposed; + proposed.N1_SL = 1; + + WHEN("A symmetric (4,4) proposal removes the negative volume action.") + { + auto const actual = extreme_action_strategy.acceptance_probability( + current, proposed, move_tracker::MoveType::FOUR_FOUR); + + THEN("MPFR preserves a positive probability below long-double range.") + { + CHECK_GT(mpfr_sgn(actual.fr()), 0); + CHECK_EQ(mpfr_values::to_long_double(actual), 0.0L); + } + } + } + GIVEN("A (2,6) move and its inverse.") { Geometry_3 current; @@ -460,15 +527,40 @@ SCENARIO("Metropolis proposal domains match the sampled raw sites" * ++selections.at(static_cast(*selected)); } - THEN("All sites remain within a conservative uniformity envelope.") + THEN("A predeclared Pearson statistic does not reject uniformity.") { - constexpr auto expected = draws / sites.size(); - constexpr auto tolerance = expected / 10; + constexpr auto expected = static_cast(draws) / + static_cast(sites.size()); + long double chi_squared{}; for (auto const selected : selections) { - CHECK_GE(selected, expected - tolerance); - CHECK_LE(selected, expected + tolerance); + auto const residual = static_cast(selected) - expected; + chi_squared += residual * residual / expected; } + // chi-square(4) 0.999 quantile; the false-positive rate is 0.1%. + constexpr auto critical_value = 18.46682695290317L; + CAPTURE(chi_squared); + CHECK_LT(chi_squared, critical_value); + } + } + } + + GIVEN("Empty and singleton raw-site domains.") + { + std::mt19937_64 generator{94}; + std::array const empty{}; + std::array const singleton{7}; + + THEN("Boundary behavior is deterministic and separate from the GOF test.") + { + CHECK_FALSE( + ergodic_moves::detail::random_element(empty, generator).has_value()); + for (auto draw = 0; draw < 32; ++draw) + { + auto const selected = + ergodic_moves::detail::random_element(singleton, generator); + REQUIRE(selected.has_value()); + CHECK_EQ(*selected, 7); } } } diff --git a/tests/Reference_fixture.cpp b/tests/Reference_fixture.cpp new file mode 100644 index 000000000..b75ecb65b --- /dev/null +++ b/tests/Reference_fixture.cpp @@ -0,0 +1,471 @@ +/******************************************************************************* + Causal Dynamical Triangulations in C++ using CGAL + + Copyright © 2026 Adam Getchell + ******************************************************************************/ + +/// @file Reference_fixture.cpp +/// @brief Emit the bounded, canonical C++ reference record for issue #94 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Ergodic_moves_3.hpp" +#include "Foliated_triangulation.hpp" +#include "Metropolis.hpp" +#include "S3Action.hpp" +#include "Utilities.hpp" +#include "Version.hpp" + +namespace +{ + using cdt::Int_precision; + using cdt::Point_t; + using Manifold = cdt::manifolds::Manifold_3; + + struct Vertex_record + { + cdt::Vertex_handle_t<3> handle; + std::array position; + Int_precision time; + }; + + struct Edge_record + { + std::array vertices; + std::string_view type; + }; + + struct Facet_record + { + std::array vertices; + bool spacelike; + Int_precision time; + }; + + struct Cell_record + { + std::array vertices; + std::string_view type; + std::vector adjacent_cells; + }; + + struct State_record + { + std::vector vertices; + std::vector edges; + std::vector facets; + std::vector cells; + Int_precision minimum_time; + Int_precision maximum_time; + }; + + [[nodiscard]] auto normalized_coordinate(double const value) noexcept + -> double + { return value == 0.0 ? 0.0 : value; } + + [[nodiscard]] auto cell_type(std::array const& times) + -> std::string_view + { + auto const [minimum, maximum] = std::ranges::minmax(times); + if (maximum - minimum != 1) { return "acausal"; } + switch (std::ranges::count(times, minimum)) + { + case 3: return "3-1"; + case 2: return "2-2"; + case 1: return "1-3"; + default: return "unclassified"; + } + } + + [[nodiscard]] auto canonical_state(cdt::Delaunay_t<3> const& triangulation) + -> State_record + { + State_record result{}; + + for (auto vertex = triangulation.finite_vertices_begin(); + vertex != triangulation.finite_vertices_end(); ++vertex) + { + result.vertices.push_back({ + .handle = vertex, + .position = + {normalized_coordinate(CGAL::to_double(vertex->point().x())), + normalized_coordinate(CGAL::to_double(vertex->point().y())), + normalized_coordinate(CGAL::to_double(vertex->point().z()))}, + .time = vertex->info() + }); + } + std::ranges::sort(result.vertices, {}, [](Vertex_record const& record) { + return std::tuple{record.time, record.position[0], record.position[1], + record.position[2]}; + }); + + auto const vertex_id = [&](cdt::Vertex_handle_t<3> const handle) { + auto const found = + std::ranges::find(result.vertices, handle, &Vertex_record::handle); + if (found == result.vertices.end()) + { + throw std::logic_error{"Canonical vertex handle was not found."}; + } + return static_cast( + std::ranges::distance(result.vertices.begin(), found)); + }; + + if (!result.vertices.empty()) + { + result.minimum_time = result.vertices.front().time; + result.maximum_time = result.vertices.back().time; + } + + for (auto edge = triangulation.finite_edges_begin(); + edge != triangulation.finite_edges_end(); ++edge) + { + std::array vertices{ + vertex_id(edge->first->vertex(edge->second)), + vertex_id(edge->first->vertex(edge->third)), + }; + std::ranges::sort(vertices); + auto const first_time = result.vertices[vertices[0]].time; + auto const second_time = result.vertices[vertices[1]].time; + result.edges.push_back( + {vertices, first_time == second_time ? "spacelike" : "timelike"}); + } + std::ranges::sort(result.edges, {}, &Edge_record::vertices); + + for (auto facet = triangulation.finite_facets_begin(); + facet != triangulation.finite_facets_end(); ++facet) + { + std::array vertices{}; + auto output = vertices.begin(); + for (auto index = 0; index < 4; ++index) + { + if (index != facet->second) + { + *output++ = vertex_id(facet->first->vertex(index)); + } + } + std::ranges::sort(vertices); + auto const time = result.vertices[vertices[0]].time; + auto const spacelike = std::ranges::all_of(vertices, [&](auto const id) { + return result.vertices[id].time == time; + }); + result.facets.push_back({vertices, spacelike, time}); + } + std::ranges::sort(result.facets, {}, &Facet_record::vertices); + + for (auto cell = triangulation.finite_cells_begin(); + cell != triangulation.finite_cells_end(); ++cell) + { + std::array vertices{}; + std::array times{}; + for (auto index = std::size_t{}; index < vertices.size(); ++index) + { + vertices[index] = vertex_id(cell->vertex(static_cast(index))); + times[index] = cell->vertex(static_cast(index))->info(); + } + std::ranges::sort(vertices); + result.cells.push_back({vertices, cell_type(times), {}}); + } + std::ranges::sort(result.cells, {}, &Cell_record::vertices); + + for (auto first = std::size_t{}; first < result.cells.size(); ++first) + { + for (auto second = first + 1; second < result.cells.size(); ++second) + { + std::array intersection{}; + auto const end = + std::ranges::set_intersection(result.cells[first].vertices, + result.cells[second].vertices, + intersection.begin()) + .out; + if (std::ranges::distance(intersection.begin(), end) == 3) + { + result.cells[first].adjacent_cells.push_back(second); + result.cells[second].adjacent_cells.push_back(first); + } + } + } + return result; + } + + [[nodiscard]] auto canonical_state(Manifold const& manifold) -> State_record + { return canonical_state(manifold.delaunay_snapshot()); } + + [[nodiscard]] auto make_causal_simplex() -> Manifold + { + constexpr auto radius = 2.0 * std::numbers::inv_sqrt3_v; + std::vector points{ + Point_t<3>{ 1, 0, 0}, + Point_t<3>{ 0, 1, 0}, + Point_t<3>{ 0, 0, 1}, + Point_t<3>{radius, radius, radius} + }; + std::vector const times{1, 1, 1, 2}; + return Manifold{cdt::manifolds::make_causal_vertices<3>(points, times)}; + } + + [[nodiscard]] auto make_causality_filter_fixture() + -> std::pair, cdt::Delaunay_t<3>> + { + std::vector points{ + Point_t<3>{1, 0, 0}, + Point_t<3>{0, 1, 0}, + Point_t<3>{0, 0, 1}, + Point_t<3>{0, 0, 2}, + Point_t<3>{2, 0, 0}, + Point_t<3>{0, 3, 0} + }; + std::vector const times{1, 1, 1, 2, 2, 3}; + auto const causal_vertices = + cdt::manifolds::make_causal_vertices<3>(points, times); + cdt::Delaunay_t<3> before{causal_vertices.begin(), causal_vertices.end()}; + auto after = before; + if (!cdt::foliated_triangulations::fix_timevalues<3>(after)) + { + throw std::runtime_error{ + "The deterministic causality-filter fixture was not repaired."}; + } + return {std::move(before), std::move(after)}; + } + + [[nodiscard]] auto make_23_fixture() -> Manifold + { + constexpr auto radius = 2.0 * std::numbers::inv_sqrt3_v; + constexpr auto root_2 = std::numbers::sqrt2_v; + std::vector points{ + Point_t<3>{ 1, 0, 0}, + Point_t<3>{ 0, 1, 0}, + Point_t<3>{ 0, 0, 1}, + Point_t<3>{radius, radius, radius}, + Point_t<3>{root_2, root_2, 0} + }; + std::vector const times{1, 1, 1, 2, 2}; + return Manifold{cdt::manifolds::make_causal_vertices<3>(points, times)}; + } + + [[nodiscard]] auto make_26_fixture() -> Manifold + { + constexpr auto radius = 2.0 * std::numbers::inv_sqrt3_v; + std::vector points{ + Point_t<3>{ 0, 0, 0}, + Point_t<3>{ 1, 0, 0}, + Point_t<3>{ 0, 1, 0}, + Point_t<3>{ 0, 0, 1}, + Point_t<3>{radius, radius, radius} + }; + std::vector const times{0, 1, 1, 1, 2}; + return Manifold{cdt::manifolds::make_causal_vertices<3>(points, times)}; + } + + [[nodiscard]] auto make_44_fixture() -> Manifold + { + constexpr auto inverse_root_2 = 1.0 / std::numbers::sqrt2_v; + std::vector points{ + Point_t<3>{ 0, 0, 0}, + Point_t<3>{ inverse_root_2, 0, inverse_root_2}, + Point_t<3>{ 0, inverse_root_2, inverse_root_2}, + Point_t<3>{-inverse_root_2, 0, inverse_root_2}, + Point_t<3>{ 0, -inverse_root_2, inverse_root_2}, + Point_t<3>{ 0, 0, 2} + }; + std::vector const times{0, 1, 1, 1, 1, 2}; + return Manifold{cdt::manifolds::make_causal_vertices<3>(points, times), 0, + 1}; + } + + void write_vertex_id(std::ostream& output, char const prefix, + std::size_t const id) + { output << '"' << prefix << std::setw(2) << std::setfill('0') << id << '"'; } + + template + void write_vertex_ids(std::ostream& output, + std::array const& vertices) + { + output << '['; + for (auto index = std::size_t{}; index < vertices.size(); ++index) + { + if (index != 0) { output << ','; } + write_vertex_id(output, 'v', vertices[index]); + } + output << ']'; + } + + template + void write_state(std::ostream& output, std::string_view const id, + StateSource const& source) + { + auto const state = canonical_state(source); + output << "{\"id\":\"" << id + << "\",\"coordinate_units\":\"initial_radius\",\"time_units\":" + "\"timeslice\",\"time_bounds\":[" + << state.minimum_time << ',' << state.maximum_time + << "],\"f_vector\":[" << state.vertices.size() << ',' + << state.edges.size() << ',' << state.facets.size() << ',' + << state.cells.size() << "],\"vertices\":["; + for (auto index = std::size_t{}; index < state.vertices.size(); ++index) + { + if (index != 0) { output << ','; } + auto const& vertex = state.vertices[index]; + output << "{\"id\":"; + write_vertex_id(output, 'v', index); + output << ",\"position\":[" << vertex.position[0] << ',' + << vertex.position[1] << ',' << vertex.position[2] + << "],\"time\":" << vertex.time << '}'; + } + output << "],\"edges\":["; + for (auto index = std::size_t{}; index < state.edges.size(); ++index) + { + if (index != 0) { output << ','; } + output << "{\"id\":"; + write_vertex_id(output, 'e', index); + output << ",\"vertices\":"; + write_vertex_ids(output, state.edges[index].vertices); + output << ",\"type\":\"" << state.edges[index].type << "\"}"; + } + output << "],\"facets\":["; + for (auto index = std::size_t{}; index < state.facets.size(); ++index) + { + if (index != 0) { output << ','; } + output << "{\"id\":"; + write_vertex_id(output, 'f', index); + output << ",\"vertices\":"; + write_vertex_ids(output, state.facets[index].vertices); + output << ",\"spacelike\":" + << (state.facets[index].spacelike ? "true" : "false"); + if (state.facets[index].spacelike) + { + output << ",\"time\":" << state.facets[index].time; + } + output << '}'; + } + output << "],\"cells\":["; + for (auto index = std::size_t{}; index < state.cells.size(); ++index) + { + if (index != 0) { output << ','; } + output << "{\"id\":"; + write_vertex_id(output, 'c', index); + output << ",\"vertices\":"; + write_vertex_ids(output, state.cells[index].vertices); + output << ",\"type\":\"" << state.cells[index].type + << "\",\"adjacent_cells\":["; + for (auto adjacency = std::size_t{}; + adjacency < state.cells[index].adjacent_cells.size(); ++adjacency) + { + if (adjacency != 0) { output << ','; } + write_vertex_id(output, 'c', + state.cells[index].adjacent_cells[adjacency]); + } + output << "]}"; + } + output << "]}"; + } + + void write_action_records(std::ostream& output) + { + constexpr auto n1 = Int_precision{11}; + constexpr auto n31 = Int_precision{7}; + constexpr auto n22 = Int_precision{5}; + constexpr auto alpha = 0.6L; + constexpr auto k = 1.1L; + constexpr auto lambda = 0.1L; + auto const parameters = + cdt::s3_action::make_physical_parameters(alpha, k, lambda); + auto const imaginary = + cdt::s3_action::s3_bulk_action_alpha_minus_one_imaginary_coefficient( + n1, n31, n22, k, lambda); + auto const rounded = + cdt::s3_action::s3_bulk_action_alpha_one(n1, n31, n22, k, lambda); + auto const generalized = + cdt::s3_action::s3_bulk_action(n1, n31, n22, parameters); + + output << "[{\"id\":\"alpha-minus-one-imaginary-coefficient\",\"counts\":" + "{\"n1_timelike\":11,\"n3_31_13\":7,\"n3_22\":5},\"parameters\":" + "{\"alpha\":-1,\"k\":1.1,\"lambda\":0.1},\"value\":" + << cdt::mpfr_values::to_long_double(imaginary) + << "},{\"id\":\"alpha-one-rounded\",\"counts\":{\"n1_timelike\":11," + "\"n3_31_13\":7,\"n3_22\":5},\"parameters\":{\"alpha\":1,\"k\":" + "1.1,\"lambda\":0.1},\"value\":" + << cdt::mpfr_values::to_long_double(rounded) + << "},{\"id\":\"alpha-generalized\",\"counts\":{\"n1_timelike\":11," + "\"n3_31_13\":7,\"n3_22\":5},\"parameters\":{\"alpha\":0.6,\"k\":" + "1.1,\"lambda\":0.1},\"value\":" + << cdt::mpfr_values::to_long_double(generalized) << "}]"; + } +} // namespace + +auto main() -> int +try +{ + auto const causal = make_causal_simplex(); + auto [filter_before, filter_after] = make_causality_filter_fixture(); + auto const before_23 = make_23_fixture(); + cdt::Random random_23{10623}; + auto const after_23 = cdt::ergodic_moves::do_23_move(before_23, random_23); + auto const before_26 = make_26_fixture(); + cdt::Random random_26{10626}; + auto const after_26 = cdt::ergodic_moves::do_26_move(before_26, random_26); + auto const before_44 = make_44_fixture(); + cdt::Random random_44{10644}; + auto const after_44 = cdt::ergodic_moves::do_44_move(before_44, random_44); + if (!after_23 || !after_26 || !after_44) + { + throw std::runtime_error{ + "A deterministic move fixture was not applicable."}; + } + + std::cout << std::setprecision(std::numeric_limits::max_digits10) + << "{\"schema\":\"cdt-reference-raw-v1\",\"implementation\":" + "{\"name\":\"CDT-plusplus\",\"version\":\"" + << cdt::VERSION << "\",\"revision\":\"" << cdt::SOURCE_REVISION + << "\",\"compiler\":\"" << cdt::BUILD_COMPILER_ID << ' ' + << cdt::BUILD_COMPILER_VERSION << "\",\"build_profile\":\"" + << cdt::BUILD_CONFIGURATION << "\",\"operating_system\":\"" + << cdt::BUILD_SYSTEM_NAME << "\",\"hardware\":\"" + << cdt::BUILD_SYSTEM_PROCESSOR + << "\",\"logical_threads\":" << std::thread::hardware_concurrency() + << ",\"cxx_standard\":23,\"standard_library\":\"" + << cdt::utilities::detail::standard_library_name() + << "\",\"cgal_version\":\"" << CGAL_VERSION_STR + << "\"},\"states\":["; + write_state(std::cout, "causal-simplex", causal); + std::cout << ','; + write_state(std::cout, "causality-filter-before", filter_before); + std::cout << ','; + write_state(std::cout, "causality-filter-after", filter_after); + std::cout << ','; + write_state(std::cout, "move-23-before", before_23); + std::cout << ','; + write_state(std::cout, "move-23-after", *after_23); + std::cout << ','; + write_state(std::cout, "move-26-before", before_26); + std::cout << ','; + write_state(std::cout, "move-26-after", *after_26); + std::cout << ','; + write_state(std::cout, "move-44-before", before_44); + std::cout << ','; + write_state(std::cout, "move-44-after", *after_44); + std::cout << "],\"actions\":"; + write_action_records(std::cout); + std::cout << "}\n"; + return 0; +} +catch (std::exception const& error) +{ + std::cerr << "Reference fixture: " << error.what() << '\n'; + return 2; +} diff --git a/tests/Runtime_config_test.cpp b/tests/Runtime_config_test.cpp index fc5137532..6890a31ff 100644 --- a/tests/Runtime_config_test.cpp +++ b/tests/Runtime_config_test.cpp @@ -135,8 +135,9 @@ SCENARIO("Runtime triangulation options parse into a validated value" * { CHECK_THROWS_AS(test_make_triangulation(true, false, 1, 3, 3, 1.0, 1.0), std::invalid_argument); - CHECK_THROWS_AS(test_make_triangulation(true, false, 3, 2, 3, 1.0, 1.0), - std::invalid_argument); + CHECK_THROWS_AS( + test_make_triangulation(true, false, 64, 3, 3, 0.1, 1.0), + std::invalid_argument); } } WHEN("The radius or foliation spacing is nonpositive.") diff --git a/tests/S3Action_test.cpp b/tests/S3Action_test.cpp index 378ef3346..be7c97842 100644 --- a/tests/S3Action_test.cpp +++ b/tests/S3Action_test.cpp @@ -14,6 +14,7 @@ #include +#include #include #include #include @@ -31,6 +32,25 @@ static_assert(std::is_nothrow_destructible_v); // test translation unit. namespace { + // The independent oracle uses the platform libm before comparison with + // 256-bit MPFR. Bound that conversion/transcendental boundary in units of + // binary64 epsilon; the rounded-coefficient comparison adds its separate + // analytic coefficient bound below. + inline constexpr long double ACTION_ABSOLUTE_TOLERANCE = + 64.0L * std::numeric_limits::epsilon(); + inline constexpr long double ACTION_RELATIVE_TOLERANCE = + 64.0L * std::numeric_limits::epsilon(); + + void check_action_close(long double const actual, long double const expected, + long double const absolute_tolerance, + long double const relative_tolerance) + { + auto const error = std::abs(actual - expected); + auto const bound = + absolute_tolerance + relative_tolerance * std::abs(expected); + CHECK_LE(error, bound); + } + [[nodiscard]] auto alpha_minus_one_reference(Int_precision const n1_tl, Int_precision const n3_31_13, Int_precision const n3_22, @@ -89,6 +109,27 @@ namespace return 2.0L * std::numbers::pi_v * k * sqrt_alpha * n1 + n31 * three_one + n22 * two_two; } + + [[nodiscard]] auto rounded_alpha_one_error_bound(Int_precision const n3_31_13, + Int_precision const n3_22, + long double const k, + long double const lambda) + -> long double + { + auto const exact_three_one = + -3.0L * k * std::asinh(1.0L / std::sqrt(15.0L)) - + 3.0L * k * std::acos(3.0L / 5.0L) - lambda / 6.0L; + auto const rounded_three_one = -3.548L * k - 0.167L * lambda; + auto const exact_two_two = + 2.0L * k * std::asinh(2.0L * std::sqrt(6.0L) / 5.0L) - + 4.0L * k * std::acos(-1.0L / 5.0L) - lambda * std::sqrt(6.0L) / 12.0L; + auto const rounded_two_two = -5.355L * k - 0.204L * lambda; + + return static_cast(n3_31_13) * + std::abs(exact_three_one - rounded_three_one) + + static_cast(n3_22) * + std::abs(exact_two_two - rounded_two_two); + } } // namespace SCENARIO("MPFR calculations use scope-owned values" * @@ -157,16 +198,20 @@ SCENARIO("Calculate the bulk action on S3 triangulations" * CHECK_EQ(universe.min_time(), 1); WHEN("The alpha=-1 Bulk Action is calculated.") { - auto Bulk_action = s3_bulk_action_alpha_minus_one( + auto Bulk_action = s3_bulk_action_alpha_minus_one_imaginary_coefficient( universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), K, Lambda); - THEN("The action matches the independent closed-form calculation.") + THEN( + "The imaginary coefficient matches the independent closed-form " + "calculation.") { - spdlog::debug("s3_bulk_action_alpha_minus_one() = {}\n", - Bulk_action.to_double()); + spdlog::debug( + "s3_bulk_action_alpha_minus_one_imaginary_coefficient() = {}\n", + Bulk_action.to_double()); auto const expected = alpha_minus_one_reference( universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), K, Lambda); - CHECK(mpfr_values::to_long_double(Bulk_action) == - doctest::Approx(expected).epsilon(1e-12)); + check_action_close(mpfr_values::to_long_double(Bulk_action), expected, + ACTION_ABSOLUTE_TOLERANCE, + ACTION_RELATIVE_TOLERANCE); } } WHEN("The alpha=1 Bulk Action is calculated.") @@ -179,8 +224,9 @@ SCENARIO("Calculate the bulk action on S3 triangulations" * Bulk_action.to_double()); auto const expected = alpha_one_reference( universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), K, Lambda); - CHECK(mpfr_values::to_long_double(Bulk_action) == - doctest::Approx(expected).epsilon(1e-12)); + check_action_close(mpfr_values::to_long_double(Bulk_action), expected, + ACTION_ABSOLUTE_TOLERANCE, + ACTION_RELATIVE_TOLERANCE); } } WHEN("The generalized Bulk Action is calculated.") @@ -196,8 +242,9 @@ SCENARIO("Calculate the bulk action on S3 triangulations" * auto const expected = generalized_reference(universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), Alpha, K, Lambda); - CHECK(mpfr_values::to_long_double(Bulk_action) == - doctest::Approx(expected).epsilon(1e-12)); + check_action_close(mpfr_values::to_long_double(Bulk_action), expected, + ACTION_ABSOLUTE_TOLERANCE, + ACTION_RELATIVE_TOLERANCE); } } WHEN( @@ -210,16 +257,22 @@ SCENARIO("Calculate the bulk action on S3 triangulations" * universe.N3_22(), parameters); auto Bulk_action_one = s3_bulk_action_alpha_one( universe.N1_TL(), universe.N3_31_13(), universe.N3_22(), K, Lambda); - THEN( - "s3_bulk_action(alpha=1) == s3_bulk_action_alpha_one() within " - "tolerances.") + THEN("Their difference is bounded by the published-coefficient rounding.") { spdlog::debug("s3_bulk_action() = {}\n", Bulk_action.to_double()); spdlog::debug("s3_bulk_action_alpha_one() = {}\n", Bulk_action_one.to_double()); - REQUIRE(mpfr_values::to_double(Bulk_action_one) == - doctest::Approx(mpfr_values::to_double(Bulk_action)) - .epsilon(TOLERANCE)); + auto const actual_difference = + std::abs(mpfr_values::to_long_double(Bulk_action_one) - + mpfr_values::to_long_double(Bulk_action)); + auto const coefficient_bound = rounded_alpha_one_error_bound( + universe.N3_31_13(), universe.N3_22(), K, Lambda); + auto const conversion_bound = + ACTION_ABSOLUTE_TOLERANCE + + ACTION_RELATIVE_TOLERANCE * + std::max(std::abs(mpfr_values::to_long_double(Bulk_action_one)), + std::abs(mpfr_values::to_long_double(Bulk_action))); + CHECK_LE(actual_difference, coefficient_bound + conversion_bound); } } } @@ -268,6 +321,16 @@ SCENARIO("Bulk action rejects invalid physical parameters" * "Alpha in 3D must be greater than 1/2.", std::domain_error); } } + WHEN("Alpha is immediately below the lower domain boundary.") + { + THEN("Physical-parameter construction rejects it with a domain error.") + { + CHECK_THROWS_WITH_AS(static_cast(make_physical_parameters( + std::nextafter(0.5L, 0.0L), 1.1L, 0.1L)), + "Alpha in 3D must be greater than 1/2.", + std::domain_error); + } + } WHEN("Alpha is not finite.") { THEN("Physical-parameter construction rejects it as invalid input.") @@ -276,6 +339,14 @@ SCENARIO("Bulk action rejects invalid physical parameters" * static_cast(make_physical_parameters( std::numeric_limits::quiet_NaN(), 1.1L, 0.1L)), "Physical parameters must be finite.", std::invalid_argument); + CHECK_THROWS_WITH_AS( + static_cast(make_physical_parameters( + std::numeric_limits::infinity(), 1.1L, 0.1L)), + "Physical parameters must be finite.", std::invalid_argument); + CHECK_THROWS_WITH_AS( + static_cast(make_physical_parameters( + -std::numeric_limits::infinity(), 1.1L, 0.1L)), + "Physical parameters must be finite.", std::invalid_argument); } } WHEN("A specialized action receives an infinite coupling.") diff --git a/tests/Utilities_test.cpp b/tests/Utilities_test.cpp index c58e98f79..6e6770f8b 100644 --- a/tests/Utilities_test.cpp +++ b/tests/Utilities_test.cpp @@ -813,25 +813,57 @@ SCENARIO("Expected points per timeslice" * doctest::test_suite("utilities")) THEN("The results are correct.") { REQUIRE_EQ(expected_points_per_timeslice(3, 2, 2), 2); } } - WHEN("We request 500 simplices on 4 timeslices.") + WHEN("We request increasing populations across the former thresholds.") { - THEN("The results are correct.") - { REQUIRE_EQ(expected_points_per_timeslice(3, 500, 4), 50); } + THEN("The estimator is monotone and has no downward threshold jumps.") + { + for (auto const threshold : {999, 9'999, 99'999}) + { + auto const before = expected_points_per_timeslice(3, threshold, 4); + auto const after = expected_points_per_timeslice(3, threshold + 1, 4); + CHECK_LE(before, after); + } + } } - WHEN("We request 5000 simplices on 8 timeslices.") + WHEN("We request established benchmark and integration populations.") { - THEN("The results are correct.") - { REQUIRE_EQ(expected_points_per_timeslice(3, 5000, 8), 125); } + THEN("The monotone envelope preserves their historical scale.") + { + CHECK_EQ(expected_points_per_timeslice(3, 640, 4), 80); + CHECK_EQ(expected_points_per_timeslice(3, 6'400, 7), 228); + } } - WHEN("We request 64,000 simplices on 16 timeslices.") + WHEN("The exact layered input population is calculated.") { - THEN("The results are correct.") - { REQUIRE_EQ(expected_points_per_timeslice(3, 64000, 16), 600); } + THEN("Every truncated layer contributes to the checked total.") + { + CHECK_EQ(generated_input_vertex_count(10, 4, 1.0, 1.0), 100); + CHECK_EQ(generated_input_vertex_count(10, 3, 0.5, 0.25), 22); + } } - WHEN("We request 640,000 simplices on 64 timeslices.") + WHEN("The rigorous 3D Delaunay upper bound is evaluated.") { - THEN("The results are correct.") - { REQUIRE_EQ(expected_points_per_timeslice(3, 640000, 64), 1000); } + THEN("Small exact values and unrepresentable large values are explicit.") + { + REQUIRE(delaunay_tetrahedron_upper_bound(4).has_value()); + CHECK_EQ(*delaunay_tetrahedron_upper_bound(4), 1); + REQUIRE(delaunay_tetrahedron_upper_bound(5).has_value()); + CHECK_EQ(*delaunay_tetrahedron_upper_bound(5), 4); + CHECK_FALSE(delaunay_tetrahedron_upper_bound( + std::numeric_limits::max()) + .has_value()); + } + } + WHEN("Population provenance is requested.") + { + auto const bounds = generated_population_bounds(3, 640, 4, 1.0, 1.0); + THEN("The exact input and rigorous safety bound are recorded.") + { + CHECK_EQ(bounds.input_vertices, + generated_input_vertex_count(bounds.points_per_timeslice, 4, + 1.0, 1.0)); + CHECK(bounds.tetrahedra_upper_bound.has_value()); + } } WHEN("We specify 4 dimensions") { diff --git a/uv.lock b/uv.lock index 55859806d..e431efb34 100644 --- a/uv.lock +++ b/uv.lock @@ -81,6 +81,7 @@ source = { editable = "." } [package.dev-dependencies] dev = [ { name = "clang-format" }, + { name = "jsonschema" }, { name = "pyyaml" }, { name = "ruff" }, { name = "semgrep" }, @@ -99,6 +100,7 @@ experiments = [ [package.metadata.requires-dev] dev = [ { name = "clang-format", specifier = "==22.1.8" }, + { name = "jsonschema", specifier = "==4.25.1" }, { name = "pyyaml", specifier = "==6.0.3" }, { name = "ruff", specifier = "==0.16.0" }, { name = "semgrep", specifier = "==1.171.0" }, From 6972970c77248c3700661b8293f602f58a1deed1 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 25 Jul 2026 08:12:02 -0700 Subject: [PATCH 2/9] fix(reference): require fresh fixture provenance - Rebuild manifests from canonical producers and current toolchain evidence. - Reject noncanonical binaries, malformed records, and stale verification state. - Standardize console and fixture output on fmt::print and enforce it with Semgrep. Closes #94 --- include/Periodic_3_triangulations.hpp | 7 +- include/Utilities.hpp | 6 +- pyproject.toml | 1 + scripts/generate_reference_fixtures.py | 157 ++++++++++--- .../tests/test_generate_reference_fixtures.py | 89 ++++++++ .../tests/test_validate_reference_fixtures.py | 37 +++ scripts/validate_reference_fixtures.py | 117 +++++++--- semgrep.yaml | 27 +++ src/cdt-viewer.cpp | 3 +- src/cdt.cpp | 3 +- src/initialize.cpp | 4 +- tests/CGAL_benchmark.cpp | 100 +++++---- tests/Metropolis_test.cpp | 2 + tests/Random_benchmark.cpp | 17 +- tests/Reference_fixture.cpp | 211 +++++++++--------- tests/semgrep/formatted_output.cpp | 23 ++ 16 files changed, 572 insertions(+), 232 deletions(-) create mode 100644 tests/semgrep/formatted_output.cpp diff --git a/include/Periodic_3_triangulations.hpp b/include/Periodic_3_triangulations.hpp index 874585c9a..53f727562 100644 --- a/include/Periodic_3_triangulations.hpp +++ b/include/Periodic_3_triangulations.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -47,7 +48,7 @@ namespace cdt::experimental::periodic_triangulations template void make_random_T3_triangulation(T* T3, int simplices, int timeslices) { - std::cout << "make_random_T3_triangulation() called" << std::endl; + fmt::print("make_random_T3_triangulation() called\n"); int simplices_per_timeslice = simplices / timeslices; /// We can't directly pick number of simplices as we can in S3 @@ -71,7 +72,7 @@ namespace cdt::experimental::periodic_triangulations ++timeslice_index) { /// Debugging - std::cout << "Timeslice " << timeslice_index << std::endl; + fmt::print("Timeslice {}\n", timeslice_index); for (int point_index = 0; point_index < points_per_timeslice; ++point_index) { @@ -82,7 +83,7 @@ namespace cdt::experimental::periodic_triangulations { auto const offset = timeslice_index * points_per_timeslice + point_index; - std::cout << " " << v[static_cast(offset)] << std::endl; + fmt::print(" {}\n", fmt::streamed(v[static_cast(offset)])); } } } diff --git a/include/Utilities.hpp b/include/Utilities.hpp index fd4696893..0a66cd6d8 100644 --- a/include/Utilities.hpp +++ b/include/Utilities.hpp @@ -1906,11 +1906,11 @@ namespace cdt::utilities struct Generated_population_bounds { /// Base population used to construct the spherical layers. - Int_precision points_per_timeslice; + Int_precision points_per_timeslice; /// Untruncated point-count expression for the final spherical layer. - long double last_layer_points; + long double last_layer_points; /// Exact total number of vertices supplied to CGAL. - std::uint64_t input_vertices; + std::uint64_t input_vertices; /// Rigorous finite 3D Delaunay bound, if representable by `uint64_t`. std::optional tetrahedra_upper_bound; }; diff --git a/pyproject.toml b/pyproject.toml index 1be103d28..ac27ef572 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ default-groups = ["dev"] no-build-package = [ "clang-format", "comet-ml", + "jsonschema", "matplotlib", "numpy", "pyyaml", diff --git a/scripts/generate_reference_fixtures.py b/scripts/generate_reference_fixtures.py index 06647c42f..aa50e3015 100644 --- a/scripts/generate_reference_fixtures.py +++ b/scripts/generate_reference_fixtures.py @@ -9,6 +9,7 @@ import subprocess import sys import tempfile +from dataclasses import dataclass from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -16,6 +17,7 @@ from scripts.validate_reference_fixtures import ( load_json, parse_json_object, + parse_key_value_payload, validate_document, ) @@ -40,6 +42,27 @@ }, ] +CANONICAL_PRODUCER_PATHS = { + "--fixture-binary": "out/build/reference/tests/CDT_reference_fixture", + "--cdt-binary": "out/build/reference/src/cdt", + "--initialize-binary": "out/build/reference/src/initialize", + "--benchmark-binary": "out/build/parallel/tests/CDT_cgal_benchmark", +} + +CMAKE_CACHE_PATHS = ( + ROOT / "out" / "build" / "reference" / "CMakeCache.txt", + ROOT / "out" / "build" / "parallel" / "CMakeCache.txt", +) + + +@dataclass(frozen=True) +class RegenerationMetadata: + """Run-scoped provenance that must not come from a manifest template.""" + + revision: str + recorded_at_utc: str + cmake_version: str + def run_command( command: list[str], @@ -97,11 +120,6 @@ def display_path(path: Path) -> str: return str(resolved) -def parse_key_values(payload: str) -> dict[str, str]: - """Parse one benchmark or persistence key-value record.""" - return dict(line.split("=", maxsplit=1) for line in payload.splitlines() if "=" in line) - - def sha256(payload: bytes) -> str: """Return a lowercase SHA-256 digest for generated bytes.""" return hashlib.sha256(payload).hexdigest() @@ -140,6 +158,24 @@ def record_commands( ] +def validate_producer_paths( + reference_commands: list[list[str]], + scaling_commands: list[list[str]], +) -> None: + """Require every recorded argv[0] to use the canonical build layout.""" + recorded = ( + ("--fixture-binary", reference_commands[0][0]), + ("--cdt-binary", reference_commands[1][0]), + ("--initialize-binary", reference_commands[2][0]), + *(("--benchmark-binary", command[0]) for command in scaling_commands), + ) + for option, path in recorded: + expected = CANONICAL_PRODUCER_PATHS[option] + if path != expected: + message = f"{option} must use the canonical producer path {expected!r}; got {path!r}" + raise ValueError(message) + + def canonical_timestamp(value: str | None) -> str: """Return a canonical UTC timestamp, rejecting ambiguous overrides.""" if value is None: @@ -157,6 +193,41 @@ def canonical_timestamp(value: str | None) -> str: return instant.isoformat().replace("+00:00", "Z") +def cmake_command_from_cache(cache_path: Path) -> Path: + """Return the exact CMake executable recorded by one configured build.""" + prefix = "CMAKE_COMMAND:INTERNAL=" + commands = [line.removeprefix(prefix) for line in cache_path.read_text(encoding="utf-8").splitlines() if line.startswith(prefix)] + if len(commands) != 1: + message = f"{cache_path}: expected exactly one configured CMake command" + raise RuntimeError(message) + command = Path(commands[0]) + if not command.is_file(): + message = f"{cache_path}: configured CMake executable does not exist: {command}" + raise RuntimeError(message) + return command + + +def configured_cmake_version( + cache_paths: tuple[Path, ...] = CMAKE_CACHE_PATHS, +) -> str: + """Return the common CMake version that configured every producer build.""" + prefix = "cmake version " + versions: dict[Path, str] = {} + for cache_path in cache_paths: + command = cmake_command_from_cache(cache_path) + lines = run_command([str(command), "--version"], timeout=30).splitlines() + if not lines or not lines[0].startswith(prefix): + observed = lines[0] if lines else "" + message = f"{cache_path}: cannot parse CMake version from {observed!r}" + raise RuntimeError(message) + versions[cache_path] = lines[0].removeprefix(prefix) + if len(set(versions.values())) != 1: + rendered = ", ".join(f"{path}: {version}" for path, version in versions.items()) + message = f"producer builds were configured by different CMake versions: {rendered}" + raise RuntimeError(message) + return next(iter(versions.values())) + + def producer_commands( fixture: Path, cdt: Path, @@ -232,7 +303,7 @@ def produce_raw_artifacts( scaling_records: list[dict[str, str]] = [] for threads, command in zip((1, 2, 4), benchmark_commands, strict=True): output = run_command(command) - record = parse_key_values(output) + record = parse_key_value_payload(output, command[0]) if record.get("implementation.revision") != revision: message = f"{threads}-thread benchmark does not embed the clean source revision; reconfigure and rebuild it" raise RuntimeError(message) @@ -266,31 +337,30 @@ def validate_platform_identity( def make_reference_manifest( generated: dict[str, bytes], fixture_json: dict[str, Any], - revision: str, - recorded: str, + metadata: RegenerationMetadata, command_lines: list[list[str]], ) -> dict[str, Any]: """Build the canonical/persistence manifest from producer metadata.""" manifest = json.loads((MANIFESTS / "macos-arm64.json").read_text(encoding="utf-8")) implementation = fixture_json["implementation"] - recorded_date = recorded[:10].replace("-", "") + recorded_date = metadata.recorded_at_utc[:10].replace("-", "") manifest["run_id"] = f"cdt-reference-v1-{implementation['operating_system'].lower()}-{implementation['hardware'].lower()}-{recorded_date}" - manifest["recorded_at_utc"] = recorded + manifest["recorded_at_utc"] = metadata.recorded_at_utc manifest["implementation"].update( { "version": implementation["version"], - "source_revision": revision, + "source_revision": metadata.revision, } ) manifest["implementation"].pop("note", None) - manifest["toolchain"].update( - { - "compiler": implementation["compiler"], - "cxx_standard": implementation["cxx_standard"], - "standard_library": implementation["standard_library"], - "cgal": implementation["cgal_version"], - } - ) + manifest["toolchain"] = { + "compiler": implementation["compiler"], + "cxx_standard": implementation["cxx_standard"], + "standard_library": implementation["standard_library"], + "cmake": metadata.cmake_version, + "cgal": implementation["cgal_version"], + } + manifest.pop("verification", None) manifest["build"]["profile"] = implementation["build_profile"] manifest["host"].update( { @@ -307,32 +377,31 @@ def make_reference_manifest( def make_scaling_manifest( generated: dict[str, bytes], records: list[dict[str, str]], - revision: str, - recorded: str, + metadata: RegenerationMetadata, command_lines: list[list[str]], ) -> dict[str, Any]: """Build the matched scaling manifest from benchmark metadata.""" first = records[0] - recorded_date = recorded[:10].replace("-", "") + recorded_date = metadata.recorded_at_utc[:10].replace("-", "") manifest = json.loads((MANIFESTS / "scaling-macos-arm64.json").read_text(encoding="utf-8")) manifest["run_id"] = f"cdt-scaling-v1-{first['build.system'].lower()}-{first['build.processor'].lower()}-{recorded_date}" - manifest["recorded_at_utc"] = recorded + manifest["recorded_at_utc"] = metadata.recorded_at_utc manifest["implementation"].update( { "version": first["implementation.version"], - "source_revision": revision, + "source_revision": metadata.revision, } ) manifest["implementation"].pop("note", None) - manifest["toolchain"].update( - { - "compiler": f"{first['build.compiler_id']} {first['build.compiler_version']}", - "cxx_standard": int(first["build.cxx_standard"]), - "standard_library": first["build.standard_library"], - "cgal": first["dependency.cgal_version"], - "tbb": first["dependency.tbb_version"], - } - ) + manifest["toolchain"] = { + "compiler": f"{first['build.compiler_id']} {first['build.compiler_version']}", + "cxx_standard": int(first["build.cxx_standard"]), + "standard_library": first["build.standard_library"], + "cmake": metadata.cmake_version, + "cgal": first["dependency.cgal_version"], + "tbb": first["dependency.tbb_version"], + } + manifest.pop("verification", None) manifest["build"]["profile"] = first["build.configuration"] manifest["host"].update( { @@ -353,6 +422,20 @@ def make_scaling_manifest( return manifest +def record_regeneration_verification( + manifest: dict[str, Any], + metadata: RegenerationMetadata, +) -> None: + """Record only evidence produced by the current regeneration run.""" + manifest["verification"] = { + "producer_regeneration": { + "status": "passed", + "source_revision": metadata.revision, + "recorded_at_utc": metadata.recorded_at_utc, + } + } + + def validate_generated_json( fixture_json: dict[str, Any], reference_manifest: dict[str, Any], @@ -426,8 +509,12 @@ def regenerate( [display_path(initialize_binary), *persistence_command[1:]], ] scaling_commands = [[display_path(benchmark_binary), *command[1:]] for command in benchmark_commands] - reference_manifest = make_reference_manifest(generated, fixture_json, revision, recorded, reference_commands) - scaling_manifest = make_scaling_manifest(generated, scaling_records, revision, recorded, scaling_commands) + validate_producer_paths(reference_commands, scaling_commands) + metadata = RegenerationMetadata(revision, recorded, configured_cmake_version()) + reference_manifest = make_reference_manifest(generated, fixture_json, metadata, reference_commands) + scaling_manifest = make_scaling_manifest(generated, scaling_records, metadata, scaling_commands) + record_regeneration_verification(reference_manifest, metadata) + record_regeneration_verification(scaling_manifest, metadata) validate_generated_json(fixture_json, reference_manifest, scaling_manifest) generated["reference/manifests/v1/macos-arm64.json"] = (json.dumps(reference_manifest, indent=2, ensure_ascii=False) + "\n").encode() generated["reference/manifests/v1/scaling-macos-arm64.json"] = (json.dumps(scaling_manifest, indent=2, ensure_ascii=False) + "\n").encode() diff --git a/scripts/tests/test_generate_reference_fixtures.py b/scripts/tests/test_generate_reference_fixtures.py index 19d540ff4..2faed7811 100644 --- a/scripts/tests/test_generate_reference_fixtures.py +++ b/scripts/tests/test_generate_reference_fixtures.py @@ -2,7 +2,10 @@ from __future__ import annotations +import tempfile import unittest +from pathlib import Path +from unittest import mock from scripts import generate_reference_fixtures as generator @@ -40,6 +43,92 @@ def test_platform_identity_must_match_manifest_names(self) -> None: with self.assertRaisesRegex(RuntimeError, "macOS-arm64-specific"): generator.validate_platform_identity(fixture, scaling) + def test_platform_identity_accepts_the_recorded_host(self) -> None: + """The macOS-arm64 reference host regenerates without objection.""" + fixture = { + "implementation": { + "operating_system": "Darwin", + "hardware": "arm64", + } + } + scaling = [ + { + "build.system": "Darwin", + "build.processor": "arm64", + } + ] + + generator.validate_platform_identity(fixture, scaling) + + def test_command_records_must_match_generated_artifacts(self) -> None: + """Argv arrays cannot be attached to a mismatched command list.""" + with self.assertRaisesRegex(ValueError, "does not match the generated artifacts"): + generator.record_commands([{"id": "fixture", "artifacts": []}], []) + + def test_producer_paths_must_use_the_canonical_layout(self) -> None: + """A noncanonical producer is rejected with its option and path.""" + reference_commands = [ + ["build/custom/CDT_reference_fixture"], + [generator.CANONICAL_PRODUCER_PATHS["--cdt-binary"]], + [generator.CANONICAL_PRODUCER_PATHS["--initialize-binary"]], + ] + scaling_commands = [[generator.CANONICAL_PRODUCER_PATHS["--benchmark-binary"]]] + + with self.assertRaisesRegex(ValueError, r"--fixture-binary.*'build/custom/CDT_reference_fixture'"): + generator.validate_producer_paths(reference_commands, scaling_commands) + + def test_cmake_version_comes_from_the_configured_builds(self) -> None: + """Manifest provenance uses the CMake recorded in each build cache.""" + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + cmake = root / "cmake" + cmake.touch() + cache_paths = (root / "reference-cache.txt", root / "parallel-cache.txt") + for cache_path in cache_paths: + cache_path.write_text(f"CMAKE_COMMAND:INTERNAL={cmake}\n", encoding="utf-8") + + with mock.patch.object( + generator, + "run_command", + return_value="cmake version 4.4.0\n", + ) as run_command: + self.assertEqual(generator.configured_cmake_version(cache_paths), "4.4.0") + + self.assertEqual( + run_command.call_args_list, + [ + mock.call([str(cmake), "--version"], timeout=30), + mock.call([str(cmake), "--version"], timeout=30), + ], + ) + + def test_manifest_builders_discard_stale_provenance(self) -> None: + """Templates cannot carry old verification or unreported tools forward.""" + fixture = generator.load_json(generator.ROOT / "reference" / "raw" / "v1" / "cpp-reference.json") + scaling = [ + generator.parse_key_value_payload( + (generator.ROOT / "reference" / "raw" / "v1" / f"scaling-threads-{threads}.txt").read_text(encoding="utf-8"), + f"scaling-threads-{threads}.txt", + ) + for threads in (1, 2, 4) + ] + metadata = generator.RegenerationMetadata("a" * 40, "2026-07-25T08:02:03Z", "9.9.9") + reference_commands = [[path] for path in list(generator.CANONICAL_PRODUCER_PATHS.values())[:3]] + scaling_commands = [[generator.CANONICAL_PRODUCER_PATHS["--benchmark-binary"]] for _threads in (1, 2, 4)] + + with mock.patch.object(generator, "refresh_artifacts"): + reference_manifest = generator.make_reference_manifest({}, fixture, metadata, reference_commands) + scaling_manifest = generator.make_scaling_manifest({}, scaling, metadata, scaling_commands) + + schema = generator.load_json(generator.ROOT / "reference" / "schema" / "run-manifest-v1.schema.json") + for manifest in (reference_manifest, scaling_manifest): + self.assertNotIn("verification", manifest) + self.assertEqual(manifest["toolchain"]["cmake"], "9.9.9") + generator.record_regeneration_verification(manifest, metadata) + self.assertEqual(manifest["verification"]["producer_regeneration"]["source_revision"], "a" * 40) + generator.validate_document(manifest, schema, generator.ROOT / "generated-manifest.json") + self.assertNotIn("mpfr", reference_manifest["toolchain"]) + if __name__ == "__main__": unittest.main() diff --git a/scripts/tests/test_validate_reference_fixtures.py b/scripts/tests/test_validate_reference_fixtures.py index 3660cc6eb..7b57e388b 100644 --- a/scripts/tests/test_validate_reference_fixtures.py +++ b/scripts/tests/test_validate_reference_fixtures.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy +import tempfile import unittest from pathlib import Path from typing import override @@ -118,6 +119,42 @@ def test_archival_provenance_rejects_dirty_revisions(self) -> None: ): validator.validate_clean_provenance(raw, manifests) + def test_fnv1a64_uses_the_published_offset_basis(self) -> None: + """The persistence checksum matches the published empty-input vector.""" + self.assertEqual(validator.fnv1a64(b""), "cbf29ce484222325") + + def test_key_value_records_reject_malformed_lines(self) -> None: + """Every scaling-record line must use the declared key=value syntax.""" + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "record.txt" + path.write_text("record.schema=fixture-v1\nunexpected output\n", encoding="utf-8") + + with self.assertRaisesRegex(ValueError, r"record\.txt:2: expected a key=value record"): + validator.parse_key_value_record(path) + + def test_key_value_records_reject_duplicate_keys(self) -> None: + """Conflicting provenance cannot depend on first- or last-key wins.""" + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "record.txt" + path.write_text( + "implementation.revision=first\nimplementation.revision=second\n", + encoding="utf-8", + ) + + with self.assertRaisesRegex(ValueError, r"record\.txt:2: duplicate key 'implementation\.revision'"): + validator.parse_key_value_record(path) + + def test_scaling_records_require_every_raw_sample(self) -> None: + """Each timing series contains the declared number of repetitions.""" + records = [validator.parse_key_value_record(validator.REFERENCE / "raw" / "v1" / f"scaling-threads-{threads}.txt") for threads in (1, 2, 4)] + records[0]["bulk_insert_ns_samples"] = "1,2,3,4" + + with ( + mock.patch.object(validator, "parse_key_value_record", side_effect=records), + self.assertRaisesRegex(ValueError, "raw sample count does not match repetitions"), + ): + validator.validate_scaling_records() + def test_proposal_probabilities_are_derived_from_raw_domains(self) -> None: """Preserving a wrong Hastings ratio does not conceal scaled inputs.""" protocol = copy.deepcopy(self.protocol) diff --git a/scripts/validate_reference_fixtures.py b/scripts/validate_reference_fixtures.py index adf3362d2..9320dc065 100644 --- a/scripts/validate_reference_fixtures.py +++ b/scripts/validate_reference_fixtures.py @@ -79,36 +79,12 @@ def validate_entity_ids(entities: list[dict[str, Any]], prefix: str, state_id: s raise ValueError(message) -def validate_state(state: dict[str, Any]) -> None: # noqa: C901, PLR0912, PLR0915 - """Validate ordering, incidence, adjacency, classification, and f-vector.""" - state_id = str(state["id"]) - vertices = state["vertices"] - edges = state["edges"] - facets = state["facets"] - cells = state["cells"] - validate_entity_ids(vertices, "v", state_id) - validate_entity_ids(edges, "e", state_id) - validate_entity_ids(facets, "f", state_id) - validate_entity_ids(cells, "c", state_id) - - f_vector = [len(vertices), len(edges), len(facets), len(cells)] - if state["f_vector"] != f_vector: - message = f"{state_id}: f-vector does not match entity arrays" - raise ValueError(message) - if f_vector[0] - f_vector[1] + f_vector[2] - f_vector[3] != 1: - message = f"{state_id}: finite complex does not have Euler characteristic 1" - raise ValueError(message) - - vertex_order = [(vertex["time"], *vertex["position"]) for vertex in vertices] - if vertex_order != sorted(vertex_order): - message = f"{state_id}: vertices are not in canonical order" - raise ValueError(message) - vertex_by_id = {vertex["id"]: vertex for vertex in vertices} - observed_times = [int(vertex["time"]) for vertex in vertices] - if state["time_bounds"] != [min(observed_times), max(observed_times)]: - message = f"{state_id}: time bounds do not match vertices" - raise ValueError(message) - +def validate_edges( + edges: list[dict[str, Any]], + vertex_by_id: dict[str, dict[str, Any]], + state_id: str, +) -> list[list[str]]: + """Validate canonical edge incidence and causal classifications.""" edge_keys: list[list[str]] = [] for edge in edges: endpoints = edge["vertices"] @@ -124,7 +100,15 @@ def validate_state(state: dict[str, Any]) -> None: # noqa: C901, PLR0912, PLR09 if edge_keys != sorted(edge_keys) or len(edge_keys) != len({tuple(key) for key in edge_keys}): message = f"{state_id}: edges are not unique and canonical" raise ValueError(message) + return edge_keys + +def validate_facets( + facets: list[dict[str, Any]], + vertex_by_id: dict[str, dict[str, Any]], + state_id: str, +) -> list[list[str]]: + """Validate canonical facet incidence and causal classifications.""" facet_keys: list[list[str]] = [] for facet in facets: incident = facet["vertices"] @@ -143,7 +127,15 @@ def validate_state(state: dict[str, Any]) -> None: # noqa: C901, PLR0912, PLR09 if facet_keys != sorted(facet_keys) or len(facet_keys) != len({tuple(key) for key in facet_keys}): message = f"{state_id}: facets are not unique and canonical" raise ValueError(message) + return facet_keys + +def validate_cells( + cells: list[dict[str, Any]], + vertex_by_id: dict[str, dict[str, Any]], + state_id: str, +) -> list[list[str]]: + """Validate canonical cell incidence.""" cell_keys: list[list[str]] = [] for cell in cells: incident = cell["vertices"] @@ -154,7 +146,16 @@ def validate_state(state: dict[str, Any]) -> None: # noqa: C901, PLR0912, PLR09 if cell_keys != sorted(cell_keys) or len(cell_keys) != len({tuple(key) for key in cell_keys}): message = f"{state_id}: cells are not unique and canonical" raise ValueError(message) + return cell_keys + +def validate_incidence_closure( + edge_keys: list[list[str]], + facet_keys: list[list[str]], + cell_keys: list[list[str]], + state_id: str, +) -> None: + """Require edge and facet sets to equal the cell incidence closure.""" expected_edges = sorted({pair for cell in cell_keys for pair in itertools.combinations(cell, 2)}) if [tuple(key) for key in edge_keys] != expected_edges: message = f"{state_id}: edge set does not equal cell incidence" @@ -164,6 +165,42 @@ def validate_state(state: dict[str, Any]) -> None: # noqa: C901, PLR0912, PLR09 message = f"{state_id}: facet set does not equal cell incidence" raise ValueError(message) + +def validate_state(state: dict[str, Any]) -> None: + """Validate ordering, incidence, adjacency, classification, and f-vector.""" + state_id = str(state["id"]) + vertices = state["vertices"] + edges = state["edges"] + facets = state["facets"] + cells = state["cells"] + validate_entity_ids(vertices, "v", state_id) + validate_entity_ids(edges, "e", state_id) + validate_entity_ids(facets, "f", state_id) + validate_entity_ids(cells, "c", state_id) + + f_vector = [len(vertices), len(edges), len(facets), len(cells)] + if state["f_vector"] != f_vector: + message = f"{state_id}: f-vector does not match entity arrays" + raise ValueError(message) + if f_vector[0] - f_vector[1] + f_vector[2] - f_vector[3] != 1: + message = f"{state_id}: finite complex does not have Euler characteristic 1" + raise ValueError(message) + + vertex_order = [(vertex["time"], *vertex["position"]) for vertex in vertices] + if vertex_order != sorted(vertex_order): + message = f"{state_id}: vertices are not in canonical order" + raise ValueError(message) + vertex_by_id = {vertex["id"]: vertex for vertex in vertices} + observed_times = [int(vertex["time"]) for vertex in vertices] + if state["time_bounds"] != [min(observed_times), max(observed_times)]: + message = f"{state_id}: time bounds do not match vertices" + raise ValueError(message) + + edge_keys = validate_edges(edges, vertex_by_id, state_id) + facet_keys = validate_facets(facets, vertex_by_id, state_id) + cell_keys = validate_cells(cells, vertex_by_id, state_id) + validate_incidence_closure(edge_keys, facet_keys, cell_keys, state_id) + for index, cell in enumerate(cells): incident = cell["vertices"] times = [int(vertex_by_id[vertex]["time"]) for vertex in incident] @@ -631,7 +668,25 @@ def validate_end_to_end(protocol: dict[str, Any], path: Path | None = None) -> N def parse_key_value_record(path: Path) -> dict[str, str]: """Read one raw benchmark record without changing or normalizing it.""" - return dict(line.split("=", maxsplit=1) for line in path.read_text(encoding="utf-8").splitlines() if "=" in line) + return parse_key_value_payload(path.read_text(encoding="utf-8"), str(path)) + + +def parse_key_value_payload(payload: str, source: str) -> dict[str, str]: + """Parse an unambiguous key-value record with source-specific errors.""" + result: dict[str, str] = {} + for line_number, line in enumerate(payload.splitlines(), start=1): + if "=" not in line: + message = f"{source}:{line_number}: expected a key=value record" + raise ValueError(message) + key, value = line.split("=", maxsplit=1) + if not key: + message = f"{source}:{line_number}: key-value record has an empty key" + raise ValueError(message) + if key in result: + message = f"{source}:{line_number}: duplicate key {key!r}" + raise ValueError(message) + result[key] = value + return result def validate_scaling_records() -> None: diff --git a/semgrep.yaml b/semgrep.yaml index f53280023..e0f982ffe 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -336,6 +336,33 @@ rules: - metavariable-regex: metavariable: $METHOD regex: ^print(?:_|$) + - id: cdt.cpp.use-fmt-print-for-console-output + languages: + - cpp + severity: ERROR + message: >- + Use fmt::print for console output. Keep stream insertion for serialization + contracts, and use fmt::streamed only when a third-party type has no + native formatter. + metadata: + category: maintainability + rationale: >- + Typed format strings keep output construction explicit and avoid + mutable iostream formatting state across CLI, fixture, and benchmark + producers. + paths: + include: + - "/include/**/*.hpp" + - "/src/**/*.cpp" + - "/tests/*_test.cpp" + - "/tests/**/*_test.cpp" + - "/tests/*_benchmark.cpp" + - "/tests/Reference_fixture.cpp" + - "/tests/semgrep/formatted_output.cpp" + pattern-either: + - pattern: std::cout << $VALUE; + - pattern: std::cerr << $VALUE; + - pattern: std::clog << $VALUE; - id: cdt.cpp.test-temp-paths-are-unique languages: - cpp diff --git a/src/cdt-viewer.cpp b/src/cdt-viewer.cpp index ca9345fad..6cc6d454c 100644 --- a/src/cdt-viewer.cpp +++ b/src/cdt-viewer.cpp @@ -8,6 +8,7 @@ Copyright © 2022 Adam Getchell /// @author Adam Getchell #include +#include #include #include @@ -52,7 +53,7 @@ try if (args.count("help")) { - std::cout << description << "\n"; + fmt::print("{}\n", fmt::streamed(description)); return EXIT_SUCCESS; } diff --git a/src/cdt.cpp b/src/cdt.cpp index 393e4fbfd..62761355f 100644 --- a/src/cdt.cpp +++ b/src/cdt.cpp @@ -11,6 +11,7 @@ /// https://github.com/ucdavis/CDT. #include +#include #include #if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ @@ -120,7 +121,7 @@ try if (args.count("help")) { - cout << description << "\n"; + fmt::print("{}\n", fmt::streamed(description)); return EXIT_SUCCESS; } diff --git a/src/initialize.cpp b/src/initialize.cpp index 08e7c6f86..21687e613 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -8,6 +8,8 @@ /// @brief Generates initial spacetimes /// @author Adam Getchell +#include + #include #if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ CDT_ENABLE_PARALLEL_TRIANGULATION @@ -88,7 +90,7 @@ try if (args.count("help")) { - cout << description << "\n"; + fmt::print("{}\n", fmt::streamed(description)); return EXIT_SUCCESS; } diff --git a/tests/CGAL_benchmark.cpp b/tests/CGAL_benchmark.cpp index 6124b5499..b5b091da9 100644 --- a/tests/CGAL_benchmark.cpp +++ b/tests/CGAL_benchmark.cpp @@ -14,6 +14,8 @@ #include #endif +#include + #include #include #include @@ -21,9 +23,9 @@ #include #include #include +#include #include #include -#include #include #include #include @@ -62,16 +64,17 @@ namespace { auto ordered = samples; std::ranges::sort(ordered); - std::cout << name << "_ns_min=" << ordered.front() << '\n' - << name << "_ns_median=" << ordered[ordered.size() / 2] << '\n' - << name << "_ns_max=" << ordered.back() << '\n' - << name << "_ns_samples="; + fmt::print( + "{}_ns_min={}\n{}_ns_median={}\n{}_ns_max={}\n" + "{}_ns_samples=", + name, ordered.front(), name, ordered[ordered.size() / 2], name, + ordered.back(), name); for (std::size_t index = 0; index < samples.size(); ++index) { - if (index != 0) { std::cout << ','; } - std::cout << samples[index]; + if (index != 0) { fmt::print(","); } + fmt::print("{}", samples[index]); } - std::cout << '\n'; + fmt::print("\n"); } }; @@ -278,46 +281,53 @@ try } } - std::cout << "record.schema=cdt-cgal-benchmark-v1\n" - << "implementation.name=CDT-plusplus\n" - << "implementation.version=" << cdt::VERSION << '\n' - << "implementation.revision=" << cdt::SOURCE_REVISION << '\n' - << "build.compiler_id=" << cdt::BUILD_COMPILER_ID << '\n' - << "build.compiler_version=" << cdt::BUILD_COMPILER_VERSION << '\n' - << "build.configuration=" << cdt::BUILD_CONFIGURATION << '\n' - << "build.system=" << cdt::BUILD_SYSTEM_NAME << '\n' - << "build.processor=" << cdt::BUILD_SYSTEM_PROCESSOR << '\n' - << "build.cxx_standard=23\n" - << "build.standard_library=" - << cdt::utilities::detail::standard_library_name() << '\n' - << "hardware.logical_threads=" - << std::thread::hardware_concurrency() << '\n' - << "dependency.cgal_version=" << CGAL_VERSION_STR << '\n' + fmt::print( + "record.schema=cdt-cgal-benchmark-v1\n" + "implementation.name=CDT-plusplus\n" + "implementation.version={}\n" + "implementation.revision={}\n" + "build.compiler_id={}\n" + "build.compiler_version={}\n" + "build.configuration={}\n" + "build.system={}\n" + "build.processor={}\n" + "build.cxx_standard=23\n" + "build.standard_library={}\n" + "hardware.logical_threads={}\n" + "dependency.cgal_version={}\n", + cdt::VERSION, cdt::SOURCE_REVISION, cdt::BUILD_COMPILER_ID, + cdt::BUILD_COMPILER_VERSION, cdt::BUILD_CONFIGURATION, + cdt::BUILD_SYSTEM_NAME, cdt::BUILD_SYSTEM_PROCESSOR, + cdt::utilities::detail::standard_library_name(), + std::thread::hardware_concurrency(), CGAL_VERSION_STR); #if defined(CDT_ENABLE_PARALLEL_TRIANGULATION) && \ CDT_ENABLE_PARALLEL_TRIANGULATION - << "dependency.tbb_version=" << TBB_VERSION_STRING << '\n' + fmt::print("dependency.tbb_version={}\n", TBB_VERSION_STRING); #else - << "dependency.tbb_version=disabled\n" + fmt::print("dependency.tbb_version=disabled\n"); #endif - << "parallel_tds=" << CDT_ENABLE_PARALLEL_TRIANGULATION << '\n' - << "requested_threads=" << thread_count << '\n' - << "active_threads=" << active_threads << '\n' - << "fixture.id=generated-foliated-ball-v1\n" - << "random.seed=" << seed << '\n' - << "random.initialization_stream=" - << cdt::random_streams::initialization << '\n' - << "random.transition_stream=" << cdt::random_streams::transitions - << '\n' - << "requested_simplices=" << simplices << '\n' - << "timeslices=" << timeslices << '\n' - << "generated_points=" << input.size() << '\n' - << "warmups=" << warmups << '\n' - << "repetitions=" << repetitions << '\n' - << "moves_per_repetition=" << move_count << '\n' - << "final_vertices=" << final_vertices << '\n' - << "final_cells=" << final_cells << '\n' - << "sample.unit=nanoseconds\n" - << "checksum=" << checksum << '\n'; + fmt::print( + "parallel_tds={}\n" + "requested_threads={}\n" + "active_threads={}\n" + "fixture.id=generated-foliated-ball-v1\n" + "random.seed={}\n" + "random.initialization_stream={}\n" + "random.transition_stream={}\n" + "requested_simplices={}\n" + "timeslices={}\n" + "generated_points={}\n" + "warmups={}\n" + "repetitions={}\n" + "moves_per_repetition={}\n" + "final_vertices={}\n" + "final_cells={}\n" + "sample.unit=nanoseconds\n" + "checksum={}\n", + CDT_ENABLE_PARALLEL_TRIANGULATION, thread_count, active_threads, seed, + cdt::random_streams::initialization, cdt::random_streams::transitions, + simplices, timeslices, input.size(), warmups, repetitions, move_count, + final_vertices, final_cells, checksum); bulk_insert.print("bulk_insert"); foliation_repair.print("foliation_repair"); cache_rebuild.print("cache_rebuild"); @@ -329,6 +339,6 @@ try } catch (std::exception const& error) { - std::cerr << "CGAL benchmark: " << error.what() << '\n'; + fmt::print(stderr, "CGAL benchmark: {}\n", error.what()); return 2; } diff --git a/tests/Metropolis_test.cpp b/tests/Metropolis_test.cpp index 14f0d718c..d72122c28 100644 --- a/tests/Metropolis_test.cpp +++ b/tests/Metropolis_test.cpp @@ -529,6 +529,8 @@ SCENARIO("Metropolis proposal domains match the sampled raw sites" * THEN("A predeclared Pearson statistic does not reject uniformity.") { + static_assert(sites.size() == 5, + "critical_value assumes sites.size() - 1 == 4"); constexpr auto expected = static_cast(draws) / static_cast(sites.size()); long double chi_squared{}; diff --git a/tests/Random_benchmark.cpp b/tests/Random_benchmark.cpp index 35041327c..2768974bd 100644 --- a/tests/Random_benchmark.cpp +++ b/tests/Random_benchmark.cpp @@ -7,11 +7,13 @@ /// @file Random_benchmark.cpp /// @brief Before/after benchmark for move-heavy random selection +#include + #include #include #include #include -#include +#include #include #include @@ -75,16 +77,15 @@ try } auto const speedup = static_cast(entropy_time.count()) / static_cast(owned_ns); - std::cout << "draws=" << draws << '\n' - << "before_entropy_per_draw_ns=" << entropy_time.count() << '\n' - << "after_run_owned_pcg_ns=" << owned_time.count() << '\n' - << "speedup=" << static_cast(speedup) << '\n' - << "checksums=" << entropy_checksum << ',' << owned_checksum - << '\n'; + fmt::print( + "draws={}\nbefore_entropy_per_draw_ns={}\nafter_run_owned_pcg_ns={}\n" + "speedup={}\nchecksums={},{}\n", + draws, entropy_time.count(), owned_time.count(), + static_cast(speedup), entropy_checksum, owned_checksum); return 0; } catch (std::exception const& error) { - std::cerr << "rng benchmark: " << error.what() << '\n'; + fmt::print(stderr, "rng benchmark: {}\n", error.what()); return 2; } diff --git a/tests/Reference_fixture.cpp b/tests/Reference_fixture.cpp index b75ecb65b..37d3b43a7 100644 --- a/tests/Reference_fixture.cpp +++ b/tests/Reference_fixture.cpp @@ -8,12 +8,12 @@ /// @brief Emit the bounded, canonical C++ reference record for issue #94 #include +#include #include #include #include -#include -#include +#include #include #include #include @@ -38,6 +38,9 @@ namespace using cdt::Point_t; using Manifold = cdt::manifolds::Manifold_3; + inline constexpr auto output_precision = + std::numeric_limits::max_digits10; + struct Vertex_record { cdt::Vertex_handle_t<3> handle; @@ -288,94 +291,89 @@ namespace 1}; } - void write_vertex_id(std::ostream& output, char const prefix, - std::size_t const id) - { output << '"' << prefix << std::setw(2) << std::setfill('0') << id << '"'; } + void write_vertex_id(char const prefix, std::size_t const id) + { fmt::print("\"{}{:02}\"", prefix, id); } template - void write_vertex_ids(std::ostream& output, - std::array const& vertices) + void write_vertex_ids(std::array const& vertices) { - output << '['; + fmt::print("["); for (auto index = std::size_t{}; index < vertices.size(); ++index) { - if (index != 0) { output << ','; } - write_vertex_id(output, 'v', vertices[index]); + if (index != 0) { fmt::print(","); } + write_vertex_id('v', vertices[index]); } - output << ']'; + fmt::print("]"); } template - void write_state(std::ostream& output, std::string_view const id, - StateSource const& source) + void write_state(std::string_view const id, StateSource const& source) { auto const state = canonical_state(source); - output << "{\"id\":\"" << id - << "\",\"coordinate_units\":\"initial_radius\",\"time_units\":" - "\"timeslice\",\"time_bounds\":[" - << state.minimum_time << ',' << state.maximum_time - << "],\"f_vector\":[" << state.vertices.size() << ',' - << state.edges.size() << ',' << state.facets.size() << ',' - << state.cells.size() << "],\"vertices\":["; + fmt::print( + "{{\"id\":\"{}\",\"coordinate_units\":\"initial_radius\"," + "\"time_units\":\"timeslice\",\"time_bounds\":[{},{}]," + "\"f_vector\":[{},{},{},{}],\"vertices\":[", + id, state.minimum_time, state.maximum_time, state.vertices.size(), + state.edges.size(), state.facets.size(), state.cells.size()); for (auto index = std::size_t{}; index < state.vertices.size(); ++index) { - if (index != 0) { output << ','; } + if (index != 0) { fmt::print(","); } auto const& vertex = state.vertices[index]; - output << "{\"id\":"; - write_vertex_id(output, 'v', index); - output << ",\"position\":[" << vertex.position[0] << ',' - << vertex.position[1] << ',' << vertex.position[2] - << "],\"time\":" << vertex.time << '}'; + fmt::print("{{\"id\":"); + write_vertex_id('v', index); + fmt::print(",\"position\":[{:.{}g},{:.{}g},{:.{}g}],\"time\":{}}}", + vertex.position[0], output_precision, vertex.position[1], + output_precision, vertex.position[2], output_precision, + vertex.time); } - output << "],\"edges\":["; + fmt::print("],\"edges\":["); for (auto index = std::size_t{}; index < state.edges.size(); ++index) { - if (index != 0) { output << ','; } - output << "{\"id\":"; - write_vertex_id(output, 'e', index); - output << ",\"vertices\":"; - write_vertex_ids(output, state.edges[index].vertices); - output << ",\"type\":\"" << state.edges[index].type << "\"}"; + if (index != 0) { fmt::print(","); } + fmt::print("{{\"id\":"); + write_vertex_id('e', index); + fmt::print(",\"vertices\":"); + write_vertex_ids(state.edges[index].vertices); + fmt::print(",\"type\":\"{}\"}}", state.edges[index].type); } - output << "],\"facets\":["; + fmt::print("],\"facets\":["); for (auto index = std::size_t{}; index < state.facets.size(); ++index) { - if (index != 0) { output << ','; } - output << "{\"id\":"; - write_vertex_id(output, 'f', index); - output << ",\"vertices\":"; - write_vertex_ids(output, state.facets[index].vertices); - output << ",\"spacelike\":" - << (state.facets[index].spacelike ? "true" : "false"); + if (index != 0) { fmt::print(","); } + fmt::print("{{\"id\":"); + write_vertex_id('f', index); + fmt::print(",\"vertices\":"); + write_vertex_ids(state.facets[index].vertices); + fmt::print(",\"spacelike\":{}", state.facets[index].spacelike); if (state.facets[index].spacelike) { - output << ",\"time\":" << state.facets[index].time; + fmt::print(",\"time\":{}", state.facets[index].time); } - output << '}'; + fmt::print("}}"); } - output << "],\"cells\":["; + fmt::print("],\"cells\":["); for (auto index = std::size_t{}; index < state.cells.size(); ++index) { - if (index != 0) { output << ','; } - output << "{\"id\":"; - write_vertex_id(output, 'c', index); - output << ",\"vertices\":"; - write_vertex_ids(output, state.cells[index].vertices); - output << ",\"type\":\"" << state.cells[index].type - << "\",\"adjacent_cells\":["; + if (index != 0) { fmt::print(","); } + fmt::print("{{\"id\":"); + write_vertex_id('c', index); + fmt::print(",\"vertices\":"); + write_vertex_ids(state.cells[index].vertices); + fmt::print(",\"type\":\"{}\",\"adjacent_cells\":[", + state.cells[index].type); for (auto adjacency = std::size_t{}; adjacency < state.cells[index].adjacent_cells.size(); ++adjacency) { - if (adjacency != 0) { output << ','; } - write_vertex_id(output, 'c', - state.cells[index].adjacent_cells[adjacency]); + if (adjacency != 0) { fmt::print(","); } + write_vertex_id('c', state.cells[index].adjacent_cells[adjacency]); } - output << "]}"; + fmt::print("]}}"); } - output << "]}"; + fmt::print("]}}"); } - void write_action_records(std::ostream& output) + void write_action_records() { constexpr auto n1 = Int_precision{11}; constexpr auto n31 = Int_precision{7}; @@ -393,18 +391,25 @@ namespace auto const generalized = cdt::s3_action::s3_bulk_action(n1, n31, n22, parameters); - output << "[{\"id\":\"alpha-minus-one-imaginary-coefficient\",\"counts\":" - "{\"n1_timelike\":11,\"n3_31_13\":7,\"n3_22\":5},\"parameters\":" - "{\"alpha\":-1,\"k\":1.1,\"lambda\":0.1},\"value\":" - << cdt::mpfr_values::to_long_double(imaginary) - << "},{\"id\":\"alpha-one-rounded\",\"counts\":{\"n1_timelike\":11," - "\"n3_31_13\":7,\"n3_22\":5},\"parameters\":{\"alpha\":1,\"k\":" - "1.1,\"lambda\":0.1},\"value\":" - << cdt::mpfr_values::to_long_double(rounded) - << "},{\"id\":\"alpha-generalized\",\"counts\":{\"n1_timelike\":11," - "\"n3_31_13\":7,\"n3_22\":5},\"parameters\":{\"alpha\":0.6,\"k\":" - "1.1,\"lambda\":0.1},\"value\":" - << cdt::mpfr_values::to_long_double(generalized) << "}]"; + auto const write_inputs = [&](long double const parameter_alpha) { + fmt::print( + "\"counts\":{{\"n1_timelike\":{},\"n3_31_13\":{},\"n3_22\":{}}}," + "\"parameters\":{{\"alpha\":{:.{}g},\"k\":{:.{}g}," + "\"lambda\":{:.{}g}}}", + n1, n31, n22, parameter_alpha, output_precision, k, output_precision, + lambda, output_precision); + }; + + fmt::print("[{{\"id\":\"alpha-minus-one-imaginary-coefficient\","); + write_inputs(-1.0L); + fmt::print(",\"value\":{:.{}g}}},{{\"id\":\"alpha-one-rounded\",", + cdt::mpfr_values::to_long_double(imaginary), output_precision); + write_inputs(1.0L); + fmt::print(",\"value\":{:.{}g}}},{{\"id\":\"alpha-generalized\",", + cdt::mpfr_values::to_long_double(rounded), output_precision); + write_inputs(alpha); + fmt::print(",\"value\":{:.{}g}}}]", + cdt::mpfr_values::to_long_double(generalized), output_precision); } } // namespace @@ -428,44 +433,42 @@ try "A deterministic move fixture was not applicable."}; } - std::cout << std::setprecision(std::numeric_limits::max_digits10) - << "{\"schema\":\"cdt-reference-raw-v1\",\"implementation\":" - "{\"name\":\"CDT-plusplus\",\"version\":\"" - << cdt::VERSION << "\",\"revision\":\"" << cdt::SOURCE_REVISION - << "\",\"compiler\":\"" << cdt::BUILD_COMPILER_ID << ' ' - << cdt::BUILD_COMPILER_VERSION << "\",\"build_profile\":\"" - << cdt::BUILD_CONFIGURATION << "\",\"operating_system\":\"" - << cdt::BUILD_SYSTEM_NAME << "\",\"hardware\":\"" - << cdt::BUILD_SYSTEM_PROCESSOR - << "\",\"logical_threads\":" << std::thread::hardware_concurrency() - << ",\"cxx_standard\":23,\"standard_library\":\"" - << cdt::utilities::detail::standard_library_name() - << "\",\"cgal_version\":\"" << CGAL_VERSION_STR - << "\"},\"states\":["; - write_state(std::cout, "causal-simplex", causal); - std::cout << ','; - write_state(std::cout, "causality-filter-before", filter_before); - std::cout << ','; - write_state(std::cout, "causality-filter-after", filter_after); - std::cout << ','; - write_state(std::cout, "move-23-before", before_23); - std::cout << ','; - write_state(std::cout, "move-23-after", *after_23); - std::cout << ','; - write_state(std::cout, "move-26-before", before_26); - std::cout << ','; - write_state(std::cout, "move-26-after", *after_26); - std::cout << ','; - write_state(std::cout, "move-44-before", before_44); - std::cout << ','; - write_state(std::cout, "move-44-after", *after_44); - std::cout << "],\"actions\":"; - write_action_records(std::cout); - std::cout << "}\n"; + fmt::print( + "{{\"schema\":\"cdt-reference-raw-v1\",\"implementation\":{{" + "\"name\":\"CDT-plusplus\",\"version\":\"{}\",\"revision\":\"{}\"," + "\"compiler\":\"{} {}\",\"build_profile\":\"{}\"," + "\"operating_system\":\"{}\",\"hardware\":\"{}\"," + "\"logical_threads\":{},\"cxx_standard\":23," + "\"standard_library\":\"{}\",\"cgal_version\":\"{}\"}},\"states\":[", + cdt::VERSION, cdt::SOURCE_REVISION, cdt::BUILD_COMPILER_ID, + cdt::BUILD_COMPILER_VERSION, cdt::BUILD_CONFIGURATION, + cdt::BUILD_SYSTEM_NAME, cdt::BUILD_SYSTEM_PROCESSOR, + std::thread::hardware_concurrency(), + cdt::utilities::detail::standard_library_name(), CGAL_VERSION_STR); + write_state("causal-simplex", causal); + fmt::print(","); + write_state("causality-filter-before", filter_before); + fmt::print(","); + write_state("causality-filter-after", filter_after); + fmt::print(","); + write_state("move-23-before", before_23); + fmt::print(","); + write_state("move-23-after", *after_23); + fmt::print(","); + write_state("move-26-before", before_26); + fmt::print(","); + write_state("move-26-after", *after_26); + fmt::print(","); + write_state("move-44-before", before_44); + fmt::print(","); + write_state("move-44-after", *after_44); + fmt::print("],\"actions\":"); + write_action_records(); + fmt::print("}}\n"); return 0; } catch (std::exception const& error) { - std::cerr << "Reference fixture: " << error.what() << '\n'; + fmt::print(stderr, "Reference fixture: {}\n", error.what()); return 2; } diff --git a/tests/semgrep/formatted_output.cpp b/tests/semgrep/formatted_output.cpp new file mode 100644 index 000000000..14a588579 --- /dev/null +++ b/tests/semgrep/formatted_output.cpp @@ -0,0 +1,23 @@ +void streamed_console_output(int const value) +{ + // ruleid: cdt.cpp.use-fmt-print-for-console-output + std::cout << "value: " << value << '\n'; + // ruleid: cdt.cpp.use-fmt-print-for-console-output + std::cerr << "error: " << value << '\n'; + // ruleid: cdt.cpp.use-fmt-print-for-console-output + std::clog << "diagnostic: " << value << '\n'; +} + +void formatted_console_output(int const value) +{ + // ok: cdt.cpp.use-fmt-print-for-console-output + fmt::print("value: {}\n", value); + // ok: cdt.cpp.use-fmt-print-for-console-output + fmt::print(stderr, "error: {}\n", value); +} + +void serialization_contract(std::ostream& output, int const value) +{ + // ok: cdt.cpp.use-fmt-print-for-console-output + output << value; +} From b0c3169b90111da99731459155490ffc5ed50544 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 25 Jul 2026 10:25:29 -0700 Subject: [PATCH 3/9] fix(ci): stabilize cross-platform validation - Load Linux tests with the configured compiler's libstdc++. - Reduce sanitizer smoke work to avoid CI timeouts. - Validate canonical reference producers before execution. - Serialize fixture coordinates at double precision. --- scripts/build.sh | 15 ++++++ scripts/generate_reference_fixtures.py | 14 ++--- scripts/sanitizer.sh | 2 +- .../tests/test_generate_reference_fixtures.py | 52 +++++++++++++++++++ tests/Reference_fixture.cpp | 6 ++- 5 files changed, 79 insertions(+), 10 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 7a0550369..f2a4a4ddb 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -26,5 +26,20 @@ prepare_cmake_cache "${build_dir}" cd -- "${repo_root}" cmake --preset "${preset}" -S "${repo_root}" + +if [[ "$(uname -s)" == "Linux" ]]; then + compiler="$( + sed -n 's/^CMAKE_CXX_COMPILER:FILEPATH=//p' \ + "${build_dir}/CMakeCache.txt" | head -n 1 + )" + runtime="$("${compiler}" -print-file-name=libstdc++.so.6)" + if [[ "${runtime}" != /* || ! -r "${runtime}" ]]; then + printf 'Cannot resolve libstdc++ for %s: %s\n' "${compiler}" "${runtime}" >&2 + exit 1 + fi + runtime_dir="$(cd -- "$(dirname -- "${runtime}")" && pwd -P)" + export LD_LIBRARY_PATH="${runtime_dir}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" +fi + cmake --build --preset "${preset}" ctest --preset "${preset}-smoke" diff --git a/scripts/generate_reference_fixtures.py b/scripts/generate_reference_fixtures.py index aa50e3015..fee339be8 100644 --- a/scripts/generate_reference_fixtures.py +++ b/scripts/generate_reference_fixtures.py @@ -494,6 +494,13 @@ def regenerate( executable(benchmark_binary), ) fixture_command, bounded_command, persistence_command, benchmark_commands = producer_commands(*paths) + reference_commands = [ + [display_path(fixture_binary)], + [display_path(cdt_binary), *bounded_command[1:]], + [display_path(initialize_binary), *persistence_command[1:]], + ] + scaling_commands = [[display_path(benchmark_binary), *command[1:]] for command in benchmark_commands] + validate_producer_paths(reference_commands, scaling_commands) generated, fixture_json, scaling_records = produce_raw_artifacts( revision, fixture_command, @@ -503,13 +510,6 @@ def regenerate( ) validate_platform_identity(fixture_json, scaling_records) recorded = canonical_timestamp(recorded_at_utc) - reference_commands = [ - [display_path(fixture_binary)], - [display_path(cdt_binary), *bounded_command[1:]], - [display_path(initialize_binary), *persistence_command[1:]], - ] - scaling_commands = [[display_path(benchmark_binary), *command[1:]] for command in benchmark_commands] - validate_producer_paths(reference_commands, scaling_commands) metadata = RegenerationMetadata(revision, recorded, configured_cmake_version()) reference_manifest = make_reference_manifest(generated, fixture_json, metadata, reference_commands) scaling_manifest = make_scaling_manifest(generated, scaling_records, metadata, scaling_commands) diff --git a/scripts/sanitizer.sh b/scripts/sanitizer.sh index 48ac60276..9e272e9ae 100755 --- a/scripts/sanitizer.sh +++ b/scripts/sanitizer.sh @@ -41,4 +41,4 @@ ctest --test-dir "${build_dir}" --label-exclude full-suite-duplicate --no-tests= mkdir -p -- "${run_dir}" cd -- "${run_dir}" "${build_dir}/src/initialize" -s -n32000 -t11 --seed 92 -"${build_dir}/src/cdt" -s -n64 -t3 -a.6 -k1.1 -l.1 -p10 --no-output --seed 92 +"${build_dir}/src/cdt" -s -n64 -t3 -a.6 -k1.1 -l.1 -p1 --no-output --seed 92 diff --git a/scripts/tests/test_generate_reference_fixtures.py b/scripts/tests/test_generate_reference_fixtures.py index 2faed7811..79102095f 100644 --- a/scripts/tests/test_generate_reference_fixtures.py +++ b/scripts/tests/test_generate_reference_fixtures.py @@ -77,6 +77,27 @@ def test_producer_paths_must_use_the_canonical_layout(self) -> None: with self.assertRaisesRegex(ValueError, r"--fixture-binary.*'build/custom/CDT_reference_fixture'"): generator.validate_producer_paths(reference_commands, scaling_commands) + def test_producer_paths_are_validated_before_execution(self) -> None: + """A noncanonical producer cannot run before path validation.""" + binaries = {option: Path(path) for option, path in generator.CANONICAL_PRODUCER_PATHS.items()} + binaries["--fixture-binary"] = Path("build/custom/CDT_reference_fixture") + + with ( + mock.patch.object(generator, "clean_source_revision", return_value="a" * 40), + mock.patch.object(generator, "executable", side_effect=lambda path: path), + mock.patch.object(generator, "produce_raw_artifacts") as produce_raw_artifacts, + self.assertRaisesRegex(ValueError, r"--fixture-binary.*'build/custom/CDT_reference_fixture'"), + ): + generator.regenerate( + binaries["--fixture-binary"], + binaries["--cdt-binary"], + binaries["--initialize-binary"], + binaries["--benchmark-binary"], + None, + ) + + produce_raw_artifacts.assert_not_called() + def test_cmake_version_comes_from_the_configured_builds(self) -> None: """Manifest provenance uses the CMake recorded in each build cache.""" with tempfile.TemporaryDirectory() as temporary: @@ -102,6 +123,37 @@ def test_cmake_version_comes_from_the_configured_builds(self) -> None: ], ) + def test_configured_builds_must_use_the_same_cmake_version(self) -> None: + """Divergent producer toolchains cannot share one manifest.""" + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + cmake = root / "cmake" + cmake.touch() + cache_paths = (root / "reference-cache.txt", root / "parallel-cache.txt") + for cache_path in cache_paths: + cache_path.write_text(f"CMAKE_COMMAND:INTERNAL={cmake}\n", encoding="utf-8") + + with ( + mock.patch.object( + generator, + "run_command", + side_effect=[ + "cmake version 4.4.0\n", + "cmake version 4.5.0\n", + ], + ) as run_command, + self.assertRaisesRegex(RuntimeError, "different CMake versions"), + ): + generator.configured_cmake_version(cache_paths) + + self.assertEqual( + run_command.call_args_list, + [ + mock.call([str(cmake), "--version"], timeout=30), + mock.call([str(cmake), "--version"], timeout=30), + ], + ) + def test_manifest_builders_discard_stale_provenance(self) -> None: """Templates cannot carry old verification or unreported tools forward.""" fixture = generator.load_json(generator.ROOT / "reference" / "raw" / "v1" / "cpp-reference.json") diff --git a/tests/Reference_fixture.cpp b/tests/Reference_fixture.cpp index 37d3b43a7..3b490b217 100644 --- a/tests/Reference_fixture.cpp +++ b/tests/Reference_fixture.cpp @@ -38,6 +38,8 @@ namespace using cdt::Point_t; using Manifold = cdt::manifolds::Manifold_3; + inline constexpr auto coordinate_precision = + std::numeric_limits::max_digits10; inline constexpr auto output_precision = std::numeric_limits::max_digits10; @@ -323,8 +325,8 @@ namespace fmt::print("{{\"id\":"); write_vertex_id('v', index); fmt::print(",\"position\":[{:.{}g},{:.{}g},{:.{}g}],\"time\":{}}}", - vertex.position[0], output_precision, vertex.position[1], - output_precision, vertex.position[2], output_precision, + vertex.position[0], coordinate_precision, vertex.position[1], + coordinate_precision, vertex.position[2], coordinate_precision, vertex.time); } fmt::print("],\"edges\":["); From 390adf3bd7b34d310d9092e8cb4305ce397f3b90 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 25 Jul 2026 11:21:05 -0700 Subject: [PATCH 4/9] test(reference): make producer path assertion cross-platform --- Justfile | 2 +- scripts/tests/test_generate_reference_fixtures.py | 4 +++- semgrep.yaml | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index 93fae4c2c..535e111f7 100644 --- a/Justfile +++ b/Justfile @@ -192,7 +192,7 @@ semgrep: _sync-python-dev set -euo pipefail state_dir="$(mktemp -d "${TMPDIR:-/tmp}/cdt-semgrep-state.XXXXXX")" trap 'rm -rf "$state_dir"' EXIT - SEMGREP_LOG_FILE="$state_dir/semgrep.log" SEMGREP_SEND_METRICS=off \ + SEMGREP_ENABLE_VERSION_CHECK=0 SEMGREP_LOG_FILE="$state_dir/semgrep.log" SEMGREP_SEND_METRICS=off \ SEMGREP_SETTINGS_FILE="$state_dir/settings.yml" SEMGREP_VERSION_CACHE_PATH="$state_dir/version-cache" \ uv run --no-sync semgrep scan --error --strict --timeout 120 --no-git-ignore \ --config semgrep.yaml --exclude tests/semgrep .github include src tests diff --git a/scripts/tests/test_generate_reference_fixtures.py b/scripts/tests/test_generate_reference_fixtures.py index 79102095f..7979dcd75 100644 --- a/scripts/tests/test_generate_reference_fixtures.py +++ b/scripts/tests/test_generate_reference_fixtures.py @@ -86,7 +86,7 @@ def test_producer_paths_are_validated_before_execution(self) -> None: mock.patch.object(generator, "clean_source_revision", return_value="a" * 40), mock.patch.object(generator, "executable", side_effect=lambda path: path), mock.patch.object(generator, "produce_raw_artifacts") as produce_raw_artifacts, - self.assertRaisesRegex(ValueError, r"--fixture-binary.*'build/custom/CDT_reference_fixture'"), + self.assertRaises(ValueError) as raised, ): generator.regenerate( binaries["--fixture-binary"], @@ -96,6 +96,8 @@ def test_producer_paths_are_validated_before_execution(self) -> None: None, ) + self.assertIn("--fixture-binary", str(raised.exception)) + self.assertIn(str(binaries["--fixture-binary"]), str(raised.exception)) produce_raw_artifacts.assert_not_called() def test_cmake_version_comes_from_the_configured_builds(self) -> None: diff --git a/semgrep.yaml b/semgrep.yaml index e0f982ffe..aa2db4bed 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -176,6 +176,8 @@ rules: - "/include/Ergodic_moves_3.hpp" - "/include/Move_tracker.hpp" - "/tests/semgrep/**/*.cpp" + exclude: + - "/tests/semgrep/formatted_output.cpp" pattern-either: - pattern: spdlog::trace(...) - pattern: spdlog::debug(...) @@ -328,6 +330,8 @@ rules: - "/tests/*_test.cpp" - "/tests/**/*_test.cpp" - "/tests/semgrep/**/*.cpp" + exclude: + - "/tests/semgrep/formatted_output.cpp" pattern-either: - pattern: fmt::print(...) - pattern: std::cout << $VALUE; From b282c0bc126f9d0fcc8bddc248ddd149d1144b4b Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 25 Jul 2026 12:07:04 -0700 Subject: [PATCH 5/9] fix(ci): stabilize Windows and Semgrep validation - Match producer-path assertions to repr-formatted Windows errors. - Discover Semgrep targets from one repository root to avoid RPC stalls. --- Justfile | 2 +- scripts/tests/test_generate_reference_fixtures.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index 535e111f7..a6261c24a 100644 --- a/Justfile +++ b/Justfile @@ -195,7 +195,7 @@ semgrep: _sync-python-dev SEMGREP_ENABLE_VERSION_CHECK=0 SEMGREP_LOG_FILE="$state_dir/semgrep.log" SEMGREP_SEND_METRICS=off \ SEMGREP_SETTINGS_FILE="$state_dir/settings.yml" SEMGREP_VERSION_CACHE_PATH="$state_dir/version-cache" \ uv run --no-sync semgrep scan --error --strict --timeout 120 --no-git-ignore \ - --config semgrep.yaml --exclude tests/semgrep .github include src tests + --config semgrep.yaml --exclude tests/semgrep . # Test repository-owned Semgrep rules against annotated positive and negative fixtures. [group('workflows')] diff --git a/scripts/tests/test_generate_reference_fixtures.py b/scripts/tests/test_generate_reference_fixtures.py index 7979dcd75..8904ab0f3 100644 --- a/scripts/tests/test_generate_reference_fixtures.py +++ b/scripts/tests/test_generate_reference_fixtures.py @@ -97,7 +97,7 @@ def test_producer_paths_are_validated_before_execution(self) -> None: ) self.assertIn("--fixture-binary", str(raised.exception)) - self.assertIn(str(binaries["--fixture-binary"]), str(raised.exception)) + self.assertIn(repr(str(binaries["--fixture-binary"])), str(raised.exception)) produce_raw_artifacts.assert_not_called() def test_cmake_version_comes_from_the_configured_builds(self) -> None: From e29f69a676bc7c10c12afbea4791fa70e9298de9 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 25 Jul 2026 14:38:54 -0700 Subject: [PATCH 6/9] build(tooling): streamline and harden supported build workflows - Make Release the canonical optimized configuration and add a focused Debug CLI workflow. - Add pinned ccache support for local pkgx builds and Ubuntu CI jobs. - Enforce pinned, approved GitHub Actions with readable version annotations. - Bound repository Semgrep scans to relevant source directories. --- .github/CONTRIBUTING.md | 14 +++-- .github/workflows/ci.yml | 37 ++++++++++++ CMakeLists.txt | 7 ++- CMakePresets.json | 39 ++++++++++++- Justfile | 21 ++++++- README.md | 40 +++++++++++-- cmake/Cache.cmake | 4 +- cmake/StandardProjectSettings.cmake | 9 ++- scripts/build.bat | 8 ++- scripts/build.sh | 39 +++++++++++-- scripts/codeql-build.sh | 2 +- scripts/coverage.sh | 2 +- scripts/iwyu.sh | 2 +- scripts/pkgx-build.sh | 18 +++++- scripts/pkgx-env.sh | 44 ++++++++++---- scripts/slurm.sh | 2 +- semgrep.yaml | 58 +++++++++++++++++++ src/CMakeLists.txt | 4 +- .../semgrep/.github/actions/action_policy.yml | 23 ++++++++ .../.github/workflows/action_policy.yml | 32 ++++++++++ 20 files changed, 361 insertions(+), 44 deletions(-) create mode 100644 tests/semgrep/.github/actions/action_policy.yml create mode 100644 tests/semgrep/.github/workflows/action_policy.yml diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9f74a05fc..d9a5fbfbf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -50,13 +50,19 @@ substantial implementation so its maintenance value and scope can be agreed upon `just check` is the fast, non-mutating source and tooling gate, including the repository-owned Semgrep policy and its fixtures. `just ci` adds the supported build and complete 24-entry CTest suite: all 103 doctest unit scenarios - and 23 CLI integration tests. `just build-parallel` builds the distinct - CGAL/oneTBB configuration and runs the same scientific suite plus the + and 23 CLI integration tests. `just build-parallel` builds the distinct CGAL/oneTBB configuration and runs the same + scientific suite plus the replayable parallel stress launcher, for 25 entries. When changing C++ behavior, also run `just clang-tidy` with the pinned LLVM 22 toolchain and review its advisory diagnostics. GitHub Actions runs `just ci` in its Ubuntu GCC, Ubuntu Clang, macOS AppleClang, and Windows MSVC jobs. The two - Ubuntu jobs also run `just build-parallel` to exercise the opt-in CGAL/oneTBB contract. The Windows job continues - to compile with native MSVC; LLVM tooling is used only for source formatting. Toolchain setup is pkgx-first; + Ubuntu jobs also run `just build-parallel` to exercise the opt-in CGAL/oneTBB contract. Sanitizer and coverage + builds keep Release assertion semantics while adding their own debug information and optimization settings. A + separate full-suite Debug job is intentionally omitted because several fixtures traverse invalid intermediate + triangulations and abort on CDT++ invariant assertions. Use `just build-debug` to compile production targets with + CDT++ assertions enabled and run the 21 compatible CLI integration CTests. That preset defines `CGAL_NDEBUG` for + supported move paths while excluding `cdt`, `cdt-no-output`, and the doctest unit fixtures because they trip + project assertions on deliberately invalid intermediate states. The Windows job continues to compile with native + MSVC; LLVM tooling is used only for source formatting. Toolchain setup 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`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3d753e18..6f242f8c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,24 +31,32 @@ jobs: compiler_package: gnu.org/gcc@16 cc: gcc cxx: g++ + compiler_cache: ccache + compiler_cache_key: gcc-16 run_parallel_contract: true - name: Ubuntu Clang os: ubuntu-latest compiler_package: llvm.org@22 cc: clang cxx: clang++ + compiler_cache: ccache + compiler_cache_key: clang-22 run_parallel_contract: true - name: macOS AppleClang os: macos-latest compiler_package: "" cc: clang cxx: clang++ + compiler_cache: "" + compiler_cache_key: none run_parallel_contract: false - name: Windows MSVC os: windows-latest compiler_package: "" cc: cl cxx: cl + compiler_cache: "" + compiler_cache_key: none run_parallel_contract: false steps: @@ -81,6 +89,7 @@ jobs: run: | { echo "cmake=$(just --evaluate cmake_version)" + echo "ccache=$(just --evaluate ccache_version)" echo "llvm=$(just --evaluate llvm_version)" echo "ninja=$(just --evaluate ninja_version)" echo "ninja-windows-wheel=$(just --evaluate ninja_windows_wheel_version)" @@ -153,9 +162,23 @@ jobs: with: vcpkgDirectory: ${{ github.workspace }}/.cache/vcpkg + - name: Restore compiler cache + if: matrix.compiler_cache != '' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .cache/ccache + key: ccache-v1-${{ runner.os }}-${{ matrix.compiler_cache_key }}-${{ github.sha }} + restore-keys: | + ccache-v1-${{ runner.os }}-${{ matrix.compiler_cache_key }}- + - name: Run the canonical local CI contract env: + CCACHE_BASEDIR: ${{ github.workspace }} + CCACHE_COMPILERCHECK: content + CCACHE_DIR: ${{ github.workspace }}/.cache/ccache + CCACHE_MAXSIZE: 1G CC: ${{ matrix.cc }} + CDT_COMPILER_CACHE: ${{ matrix.compiler_cache }} CDT_PKGX_COMPILER_PACKAGE: ${{ matrix.compiler_package }} CXX: ${{ matrix.cxx }} run: just ci @@ -163,11 +186,25 @@ jobs: - name: Run the opt-in CGAL/oneTBB contract if: matrix.run_parallel_contract env: + CCACHE_BASEDIR: ${{ github.workspace }} + CCACHE_COMPILERCHECK: content + CCACHE_DIR: ${{ github.workspace }}/.cache/ccache + CCACHE_MAXSIZE: 1G CC: ${{ matrix.cc }} + CDT_COMPILER_CACHE: ${{ matrix.compiler_cache }} CDT_PKGX_COMPILER_PACKAGE: ${{ matrix.compiler_package }} CXX: ${{ matrix.cxx }} run: just build-parallel + - name: Report compiler cache statistics + if: ${{ always() && matrix.compiler_cache != '' }} + env: + CCACHE_DIR: ${{ github.workspace }}/.cache/ccache + CCACHE_VERSION: ${{ steps.tool-versions.outputs.ccache }} + run: >- + pkgx "+ccache.dev@${CCACHE_VERSION}" -- + ccache --show-stats + build: name: build if: ${{ always() }} diff --git a/CMakeLists.txt b/CMakeLists.txt index db70ee43c..c35ab2bd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,8 +79,13 @@ include(cmake/PreventInSourceBuilds.cmake) # Link this 'library' to set the c++ standard / compile-time options requested add_library(project_options INTERFACE) target_compile_features(project_options INTERFACE cxx_std_23) +# Debug keeps CDT++ assertions enabled. CGAL's internal checks are disabled +# because supported move paths deliberately traverse invalid intermediate +# triangulations before restoring their invariants. target_compile_definitions( - project_options INTERFACE CDT_BUILD_CONFIGURATION="$") + project_options + INTERFACE CDT_BUILD_CONFIGURATION="$" + $<$:CGAL_NDEBUG>) if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") option(ENABLE_BUILD_WITH_TIME_TRACE "Enable -ftime-trace to generate time tracing .json files on clang" OFF) diff --git a/CMakePresets.json b/CMakePresets.json index 5d32a5c96..9821f4da6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -48,7 +48,7 @@ }, "cacheVariables": { "CMAKE_CXX_COMPILER": "clang++", - "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_BUILD_TYPE": "Release", "CMAKE_CXX_FLAGS": "-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast", "ENABLE_CACHE": false, "ENABLE_TESTING": true @@ -61,7 +61,7 @@ "inherits": "common", "binaryDir": "${sourceDir}/out/build/reference", "cacheVariables": { - "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_BUILD_TYPE": "Release", "ENABLE_CACHE": false, "ENABLE_TESTING": true } @@ -79,9 +79,12 @@ { "name": "debug", "displayName": "Local debug build", - "description": "Configure an optional debug build for interactive diagnostics", + "description": "Configure an optional debug build with CDT++ assertions enabled and CGAL checks disabled", "inherits": "common", + "binaryDir": "${sourceDir}/out/build/debug", "cacheVariables": { + "BUILD_TESTING": true, + "CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE": true, "CMAKE_BUILD_TYPE": "Debug", "ENABLE_CACHE": false, "ENABLE_TESTING": true @@ -132,6 +135,15 @@ "name": "parallel", "displayName": "Build the supported CGAL/oneTBB targets", "configurePreset": "parallel" + }, + { + "name": "debug", + "displayName": "Build production targets with CDT++ assertions enabled", + "configurePreset": "debug", + "targets": [ + "cdt", + "initialize" + ] } ], "testPresets": [ @@ -160,6 +172,27 @@ "noTestsAction": "error", "stopOnFailure": false } + }, + { + "name": "debug-cli", + "displayName": "Run CLI integration tests with CDT++ assertions", + "description": "Run only the Debug CLI tests; unit fixtures that trip CDT++ assertions on deliberately invalid intermediate states are excluded", + "configurePreset": "debug", + "filter": { + "exclude": { + "label": "^debug-incompatible$" + }, + "include": { + "label": "^integration$" + } + }, + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error", + "stopOnFailure": false + } } ] } diff --git a/Justfile b/Justfile index a6261c24a..0e2843934 100644 --- a/Justfile +++ b/Justfile @@ -15,6 +15,7 @@ cmake_minimum_version := "4.4.0" cmake_version := "4.4.0" ninja_version := "1.13.2" ninja_windows_wheel_version := "1.13.0" +ccache_version := "4.13.6" doxygen_version := "1.17.0" graphviz_version := "15.1.0" zizmor_version := "1.28.0" @@ -34,6 +35,11 @@ build: build-parallel: {{ if os_family() == "windows" { "cmd.exe //d //c 'scripts\\build.bat parallel'" } else { "just _build-parallel-unix" } }} +# Build production targets in Debug mode and run the supported CLI integration tests. +[group('workflows')] +build-debug: + {{ if os_family() == "windows" { "cmd.exe //d //c 'scripts\\build.bat debug'" } else { "just _build-debug-unix" } }} + # Run fast, non-mutating local validation. [group('workflows')] check: _justfile-check _format-check _yaml-check _action-lint _zizmor _whitespace-check _cmake-check release-check python-check reference-check semgrep semgrep-test @@ -195,7 +201,11 @@ semgrep: _sync-python-dev SEMGREP_ENABLE_VERSION_CHECK=0 SEMGREP_LOG_FILE="$state_dir/semgrep.log" SEMGREP_SEND_METRICS=off \ SEMGREP_SETTINGS_FILE="$state_dir/settings.yml" SEMGREP_VERSION_CACHE_PATH="$state_dir/version-cache" \ uv run --no-sync semgrep scan --error --strict --timeout 120 --no-git-ignore \ - --config semgrep.yaml --exclude tests/semgrep . + --config semgrep.yaml \ + --exclude .cache --exclude .venv --exclude venv \ + --exclude 'build*' --exclude 'cmake-build*' --exclude cov-int --exclude coverage \ + --exclude html --exclude out --exclude Testing --exclude vcpkg_installed \ + --exclude tests/semgrep . # Test repository-owned Semgrep rules against annotated positive and negative fixtures. [group('workflows')] @@ -344,6 +354,15 @@ _build-parallel-unix: fi exec ./scripts/build.sh parallel +[private] +_build-debug-unix: + #!/usr/bin/env bash + set -euo pipefail + if command -v pkgx >/dev/null; then + exec ./scripts/pkgx-build.sh --preset debug + fi + exec ./scripts/build.sh debug + [private] _codeql-phase phase: #!/usr/bin/env bash diff --git a/README.md b/README.md index ce3ea96d3..94b65a20e 100644 --- a/README.md +++ b/README.md @@ -172,14 +172,41 @@ From a fresh checkout, the primary supported headless build, dependency bootstra just build ``` -The `build` recipe uses the pkgx launcher on Unix when pkgx is available, then delegates to `scripts/build.sh`. Its +The `build` recipe uses the Release configuration and the pkgx launcher on Unix when pkgx is available, then delegates +to `scripts/build.sh`. 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: + +```bash +just build-debug +``` + +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. + +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: + +```bash +CDT_COMPILER_CACHE=ccache just build +``` + +Compiler caching covers project compilation; vcpkg binary caching remains responsible for reusing compiled +third-party packages. + The optional [pkgx](https://pkgx.sh/) entry point supplies the complete Unix developer-tool environment ephemerally -and invokes the same build contract directly, without requiring Just: +and invokes the same build contract directly: ```bash ./scripts/pkgx-build.sh @@ -225,7 +252,7 @@ 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 when invoking the recipes directly; `scripts/pkgx-build.sh` does not require it +- Just, used by the recipes and `scripts/pkgx-build.sh` to resolve the repository's tool-version pins - Python 3.12 for native dependency bootstrap, and uv when checking or running the Python support scripts - Doxygen 1.17.0 and Graphviz 15.1.0 when checking or generating API documentation; pkgx can supply both @@ -254,6 +281,7 @@ The repository-root [Justfile](Justfile) provides the same small command vocabul ```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 @@ -280,7 +308,11 @@ fields, YAML, GitHub Actions syntax and security, whitespace, and CMake preset p 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`. Windows continues to compile with native MSVC; the +run `just build-parallel`. 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 diff --git a/cmake/Cache.cmake b/cmake/Cache.cmake index bf8423f05..1643c36d8 100644 --- a/cmake/Cache.cmake +++ b/cmake/Cache.cmake @@ -1,4 +1,4 @@ -option(ENABLE_CACHE "Enable cache if available" ON) +option(ENABLE_CACHE "Enable cache if available" OFF) if(NOT ENABLE_CACHE) return() endif() @@ -6,7 +6,7 @@ endif() set(CACHE_OPTION "ccache" CACHE STRING "Compiler cache to be used") -set(CACHE_OPTION_VALUES "ccache" "sccache") +set(CACHE_OPTION_VALUES "ccache") set_property(CACHE CACHE_OPTION PROPERTY STRINGS ${CACHE_OPTION_VALUES}) list( FIND diff --git a/cmake/StandardProjectSettings.cmake b/cmake/StandardProjectSettings.cmake index ac6a9a00e..625d64e72 100644 --- a/cmake/StandardProjectSettings.cmake +++ b/cmake/StandardProjectSettings.cmake @@ -2,21 +2,20 @@ # conflicts with CGAL_SetupFlags.cmake. if(CMAKE_CONFIGURATION_TYPES) set(CMAKE_CONFIGURATION_TYPES - "Release" "Debug" "RelWithDebInfo" + "Release" "Debug" CACHE STRING "" FORCE) elseif(NOT CMAKE_BUILD_TYPE) # Set a default build type for single-config generators when none was specified. - message(STATUS "Setting build type to 'RelWithDebInfo' as none was specified.") + message(STATUS "Setting build type to 'Release' as none was specified.") set(CMAKE_BUILD_TYPE - RelWithDebInfo + Release CACHE STRING "Choose the type of build." FORCE) # Set the possible values of build type for cmake-gui, ccmake set_property( CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" - "Release" - "RelWithDebInfo") + "Release") endif() # Compile commands for ClangTidy et. al diff --git a/scripts/build.bat b/scripts/build.bat index f8ff5608a..c97f8c5a2 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -8,9 +8,10 @@ SET "PRESET=%~1" IF NOT DEFINED PRESET SET "PRESET=reference" IF /I "%PRESET%"=="reference" GOTO PRESET_READY IF /I "%PRESET%"=="parallel" GOTO PRESET_READY +IF /I "%PRESET%"=="debug" GOTO PRESET_READY :PRESET_USAGE -echo Usage: %~nx0 [reference^|parallel] 1>&2 +echo Usage: %~nx0 [reference^|parallel^|debug] 1>&2 EXIT /B 2 :PRESET_READY @@ -38,6 +39,11 @@ CD /D "%REPO_ROOT%" || EXIT /B 1 CALL :PREPARE_CMAKE_CACHE || EXIT /B 1 cmake --preset "%PRESET%" -S . || EXIT /B 1 cmake --build --preset "%PRESET%" --parallel 2 || EXIT /B 1 +IF /I "%PRESET%"=="debug" ( + ctest --preset debug-cli || EXIT /B 1 + echo Debug-compatible CLI integration tests passed; assertion-incompatible paths were excluded. + EXIT /B 0 +) ctest --preset "%PRESET%-smoke" || EXIT /B 1 EXIT /B 0 diff --git a/scripts/build.sh b/scripts/build.sh index f2a4a4ddb..fe4d22d9a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -5,19 +5,45 @@ set -euo pipefail script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd -- "${script_dir}/.." && pwd)" if [[ "$#" -gt 1 ]]; then - printf 'Usage: %s [reference|parallel]\n' "$0" >&2 + printf 'Usage: %s [reference|parallel|debug]\n' "$0" >&2 exit 2 fi preset="${1:-reference}" case "${preset}" in - reference | parallel) ;; + reference | parallel | debug) ;; *) - printf 'Unsupported build preset %s; expected reference or parallel.\n' \ + printf 'Unsupported build preset %s; expected reference, parallel, or debug.\n' \ "${preset}" >&2 exit 2 ;; esac build_dir="${repo_root}/out/build/${preset}" +compiler_cache="${CDT_COMPILER_CACHE:-}" +cache_arguments=(-D ENABLE_CACHE:BOOL=OFF) +case "${compiler_cache}" in + "" | off) ;; + ccache) + if ! command -v "${compiler_cache}" >/dev/null 2>&1; then + printf 'CDT_COMPILER_CACHE=%s requires %s on PATH.\n' \ + "${compiler_cache}" "${compiler_cache}" >&2 + exit 1 + fi + CCACHE_BASEDIR="${CCACHE_BASEDIR:-${repo_root}}" + CCACHE_COMPILERCHECK="${CCACHE_COMPILERCHECK:-content}" + CCACHE_DIR="${CCACHE_DIR:-${repo_root}/.cache/ccache}" + CCACHE_MAXSIZE="${CCACHE_MAXSIZE:-1G}" + export CCACHE_BASEDIR CCACHE_COMPILERCHECK CCACHE_DIR CCACHE_MAXSIZE + cache_arguments=( + -D ENABLE_CACHE:BOOL=ON + -D "CACHE_OPTION:STRING=${compiler_cache}" + ) + ;; + *) + printf 'Unsupported CDT_COMPILER_CACHE=%s; expected ccache, off, or an empty value.\n' \ + "${compiler_cache}" >&2 + exit 2 + ;; +esac source "${script_dir}/prepare-vcpkg.sh" prepare_reference_environment @@ -25,7 +51,7 @@ prepare_vcpkg "${repo_root}" prepare_cmake_cache "${build_dir}" cd -- "${repo_root}" -cmake --preset "${preset}" -S "${repo_root}" +cmake --preset "${preset}" -S "${repo_root}" "${cache_arguments[@]}" if [[ "$(uname -s)" == "Linux" ]]; then compiler="$( @@ -42,4 +68,9 @@ if [[ "$(uname -s)" == "Linux" ]]; then fi cmake --build --preset "${preset}" +if [[ "${preset}" == "debug" ]]; then + ctest --preset debug-cli + printf 'Debug-compatible CLI integration tests passed; assertion-incompatible paths were excluded.\n' + exit 0 +fi ctest --preset "${preset}-smoke" diff --git a/scripts/codeql-build.sh b/scripts/codeql-build.sh index 694f8bd4b..32b012a4e 100755 --- a/scripts/codeql-build.sh +++ b/scripts/codeql-build.sh @@ -24,7 +24,7 @@ case "${phase}" in # builds third-party sources outside the CodeQL database, while the build # directory keeps installed dependency headers outside the checkout. cmake -S "${repo_root}" -B "${codeql_build_dir}" -G Ninja \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_CXX_EXTENSIONS=OFF \ -DCMAKE_CXX_STANDARD=23 \ -DCMAKE_CXX_STANDARD_REQUIRED=ON \ diff --git a/scripts/coverage.sh b/scripts/coverage.sh index b7003f9bb..50684825f 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -58,7 +58,7 @@ cmake \ -G Ninja \ -S "${repo_root}" \ -B "${cmake_build_dir}" \ - -D CMAKE_BUILD_TYPE=RelWithDebInfo \ + -D CMAKE_BUILD_TYPE=Release \ -D CMAKE_CXX_COMPILER="${cxx}" \ -D ENABLE_COVERAGE:BOOL=TRUE \ -D ENABLE_TESTING:BOOL=TRUE diff --git a/scripts/iwyu.sh b/scripts/iwyu.sh index cef63100a..f420db09b 100755 --- a/scripts/iwyu.sh +++ b/scripts/iwyu.sh @@ -13,5 +13,5 @@ repo_root="$(cd -- "${script_dir}/.." && pwd)" build_dir="${repo_root}/build" rm -rf -- "${build_dir}" -cmake -G Ninja -D CMAKE_BUILD_TYPE=RelWithDebInfo -D ENABLE_TESTING:BOOL=TRUE -D ENABLE_INCLUDE_WHAT_YOU_USE:BOOL=TRUE -S "${repo_root}" -B "${build_dir}" +cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D ENABLE_TESTING:BOOL=TRUE -D ENABLE_INCLUDE_WHAT_YOU_USE:BOOL=TRUE -S "${repo_root}" -B "${build_dir}" cmake --build "${build_dir}" diff --git a/scripts/pkgx-build.sh b/scripts/pkgx-build.sh index b12de4de4..598a67703 100755 --- a/scripts/pkgx-build.sh +++ b/scripts/pkgx-build.sh @@ -7,6 +7,7 @@ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd -- "${script_dir}/.." && pwd)" cmake_version="$(just --justfile "${repo_root}/Justfile" --evaluate cmake_version)" ninja_version="$(just --justfile "${repo_root}/Justfile" --evaluate ninja_version)" +ccache_version="$(just --justfile "${repo_root}/Justfile" --evaluate ccache_version)" pkgx_tools=( +git-scm.org@2.55.0 @@ -26,6 +27,16 @@ if [[ -n "${CDT_PKGX_COMPILER_PACKAGE:-}" ]]; then pkgx_tools+=("+${CDT_PKGX_COMPILER_PACKAGE}") fi +case "${CDT_COMPILER_CACHE:-}" in + "" | off) ;; + ccache) pkgx_tools+=("+ccache.dev@${ccache_version}") ;; + *) + printf 'Unsupported CDT_COMPILER_CACHE=%s; expected ccache, off, or an empty value.\n' \ + "${CDT_COMPILER_CACHE}" >&2 + exit 2 + ;; +esac + cd -- "${repo_root}" if [[ "${1:-}" == "--codeql" ]]; then @@ -34,8 +45,9 @@ if [[ "${1:-}" == "--codeql" ]]; then fi if [[ "${1:-}" == "--preset" ]]; then - if [[ "$#" -ne 2 ]] || [[ "$2" != "reference" && "$2" != "parallel" ]]; then - printf 'Usage: %s [--preset reference|parallel] [--codeql prepare|build]\n' \ + if [[ "$#" -ne 2 ]] || + [[ "$2" != "reference" && "$2" != "parallel" && "$2" != "debug" ]]; then + printf 'Usage: %s [--preset reference|parallel|debug] [--codeql prepare|build]\n' \ "$0" >&2 exit 2 fi @@ -44,7 +56,7 @@ if [[ "${1:-}" == "--preset" ]]; then fi if [[ "$#" -ne 0 ]]; then - printf 'Usage: %s [--preset reference|parallel] [--codeql prepare|build]\n' \ + printf 'Usage: %s [--preset reference|parallel|debug] [--codeql prepare|build]\n' \ "$0" >&2 exit 2 fi diff --git a/scripts/pkgx-env.sh b/scripts/pkgx-env.sh index 29f854869..22854c16e 100644 --- a/scripts/pkgx-env.sh +++ b/scripts/pkgx-env.sh @@ -16,16 +16,40 @@ fi script_dir="$(cd -- "$(dirname -- "${script_file}")" && pwd -P)" repo_root="$(cd -- "${script_dir}/.." && pwd -P)" -if ! pkgx_environment="$(pkgx \ - +freedesktop.org/pkg-config@0.29.2 \ - +gnu.org/m4@1.4.21 \ - +gnu.org/autoconf@2.73.0 \ - +gnu.org/autoconf-archive@2024.10.16 \ - +gnu.org/automake@1.18.1 \ - +gnu.org/libtool@2.5.4 \ - +gnu.org/texinfo@7.3.0)"; then +pkgx_packages=( + +freedesktop.org/pkg-config@0.29.2 + +gnu.org/m4@1.4.21 + +gnu.org/autoconf@2.73.0 + +gnu.org/autoconf-archive@2024.10.16 + +gnu.org/automake@1.18.1 + +gnu.org/libtool@2.5.4 + +gnu.org/texinfo@7.3.0 +) +case "${CDT_COMPILER_CACHE:-}" in + "" | off) ;; + ccache) + if ! command -v just >/dev/null 2>&1; then + printf 'Just is required to resolve the pinned ccache version.\n' >&2 + unset pkgx_packages repo_root script_dir script_file + return 1 2>/dev/null || exit 1 + fi + cache_version="$( + just --justfile "${repo_root}/Justfile" \ + --evaluate ccache_version + )" + pkgx_packages+=("+ccache.dev@${cache_version}") + ;; + *) + printf 'Unsupported CDT_COMPILER_CACHE=%s; expected ccache, off, or an empty value.\n' \ + "${CDT_COMPILER_CACHE}" >&2 + unset pkgx_packages repo_root script_dir script_file + return 2 2>/dev/null || exit 2 + ;; +esac + +if ! pkgx_environment="$(pkgx "${pkgx_packages[@]}")"; then printf 'Unable to resolve the pkgx build environment.\n' >&2 - unset pkgx_environment repo_root script_dir script_file + unset cache_version pkgx_environment pkgx_packages repo_root script_dir script_file return 1 2>/dev/null || exit 1 fi @@ -66,4 +90,4 @@ fi export VCPKG_ROOT="${repo_root}/.cache/vcpkg" -unset had_allexport pkgx_environment repo_root script_dir script_file +unset cache_version had_allexport pkgx_environment pkgx_packages repo_root script_dir script_file diff --git a/scripts/slurm.sh b/scripts/slurm.sh index 3987cc9a6..5bb1949c6 100755 --- a/scripts/slurm.sh +++ b/scripts/slurm.sh @@ -13,6 +13,6 @@ module load cmake/4.4.0 module load gcc/13.3.0 module load autoconf-archive/2022.02.11 rm -rf -- "${build_dir}" -cmake -D CMAKE_BUILD_TYPE=RelWithDebInfo -D ENABLE_TESTING:BOOL=TRUE -S "${repo_root}" -B "${build_dir}" +cmake -D CMAKE_BUILD_TYPE=Release -D ENABLE_TESTING:BOOL=TRUE -S "${repo_root}" -B "${build_dir}" cmake --build "${build_dir}" ctest --test-dir "${build_dir}" --no-tests=error --output-on-failure -j2 diff --git a/semgrep.yaml b/semgrep.yaml index aa2db4bed..05d5b96ca 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -42,6 +42,64 @@ rules: ... runs-on: windows-latest ... + - id: cdt.github-actions.external-action-sha-pinned + languages: + - regex + severity: WARNING + message: "Pin external GitHub Actions to a full 40-character commit SHA." + metadata: + category: security + rationale: "Moving tags can change workflow behavior without review." + paths: + include: + - "/.github/actions/**/*.yaml" + - "/.github/actions/**/*.yml" + - "/.github/workflows/**/*.yaml" + - "/.github/workflows/**/*.yml" + - "/tests/semgrep/.github/**/*.yaml" + - "/tests/semgrep/.github/**/*.yml" + patterns: + - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@(?![a-fA-F0-9]{40}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length + - id: cdt.github-actions.external-action-approved-allowlist + languages: + - regex + severity: WARNING + message: >- + Use only approved external GitHub Actions, or update the repository + allowlist deliberately. + metadata: + category: security + rationale: "A small allowlist keeps workflow supply-chain review explicit." + paths: + include: + - "/.github/actions/**/*.yaml" + - "/.github/actions/**/*.yml" + - "/.github/workflows/**/*.yaml" + - "/.github/workflows/**/*.yml" + - "/tests/semgrep/.github/**/*.yaml" + - "/tests/semgrep/.github/**/*.yml" + patterns: + - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)(?!(?:actions/(?:cache|checkout|setup-python|upload-artifact)|astral-sh/setup-uv|codecov/codecov-action|DenverCoder1/doxygen-github-pages-action|github/codeql-action/(?:analyze|init)|ilammy/msvc-dev-cmd|JamesIves/github-pages-deploy-action|lukka/run-vcpkg|pkgxdev/setup|taiki-e/(?:cache-cargo-install-action|install-action))@)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@' # yamllint disable-line rule:line-length + - id: cdt.github-actions.external-action-version-comment + languages: + - regex + severity: WARNING + message: "Keep a readable version comment next to external GitHub Action SHA pins." + metadata: + category: maintainability + rationale: >- + Version comments make automated dependency updates and human review + manageable. + paths: + include: + - "/.github/actions/**/*.yaml" + - "/.github/actions/**/*.yml" + - "/.github/workflows/**/*.yaml" + - "/.github/workflows/**/*.yml" + - "/tests/semgrep/.github/**/*.yaml" + - "/tests/semgrep/.github/**/*.yml" + patterns: + - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@[a-fA-F0-9]{40}\s*$' # yamllint disable-line rule:line-length - id: cdt.cpp.no-crtp-inheritance languages: - cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b170c4954..37c29889c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -52,7 +52,7 @@ add_test( "-DEXPECTED_THREADS=1" "-DTEST_ARGUMENTS=-s;-n127;-t4;-a0.6;-k1.1;-l0.1;-p1;--seed;92" -P ${PROJECT_SOURCE_DIR}/cmake/RunPersistenceOutputTest.cmake) -set_tests_properties(cdt PROPERTIES LABELS "integration;persistence") +set_tests_properties(cdt PROPERTIES LABELS "integration;persistence;debug-incompatible") add_test( NAME cdt-no-output @@ -62,7 +62,7 @@ add_test( -DTEST_DIRECTORY=${CMAKE_CURRENT_BINARY_DIR}/cdt-no-output -DTEST_THREADS=${cdt_cli_explicit_threads} -P ${PROJECT_SOURCE_DIR}/cmake/RunCdtNoOutputTest.cmake) -set_tests_properties(cdt-no-output PROPERTIES LABELS "integration") +set_tests_properties(cdt-no-output PROPERTIES LABELS "integration;debug-incompatible") add_cli_failure_test(cdt-triangle-inequalities cdt "Triangle inequalities violated" -s -n64 -t3 -a0.4 -k1.1 -l0.1 --seed 92) diff --git a/tests/semgrep/.github/actions/action_policy.yml b/tests/semgrep/.github/actions/action_policy.yml new file mode 100644 index 000000000..81c44cfde --- /dev/null +++ b/tests/semgrep/.github/actions/action_policy.yml @@ -0,0 +1,23 @@ +--- +name: Action policy fixtures +description: Exercise action policy rules for local composite actions. +runs: + using: composite + steps: + - name: Allowed pinned action + # ok: cdt.github-actions.external-action-sha-pinned + # ok: cdt.github-actions.external-action-approved-allowlist + # ok: cdt.github-actions.external-action-version-comment + uses: taiki-e/install-action@7572810d7dd469b651bb7793945692cf78da5dd7 # v2.85.0 + + - name: Moving tag action + # ruleid: cdt.github-actions.external-action-sha-pinned + uses: taiki-e/install-action@v2 + + - name: Unapproved external action + # ruleid: cdt.github-actions.external-action-approved-allowlist + uses: unapproved/composite-action@2222222222222222222222222222222222222222 # v2.0.0 + + - name: Missing readable version comment + # ruleid: cdt.github-actions.external-action-version-comment + uses: taiki-e/install-action@7572810d7dd469b651bb7793945692cf78da5dd7 diff --git a/tests/semgrep/.github/workflows/action_policy.yml b/tests/semgrep/.github/workflows/action_policy.yml new file mode 100644 index 000000000..b3cae3efc --- /dev/null +++ b/tests/semgrep/.github/workflows/action_policy.yml @@ -0,0 +1,32 @@ +--- +name: Action policy fixtures +on: # yamllint disable-line rule:truthy + workflow_dispatch: + +jobs: + fixtures: + runs-on: ubuntu-latest + steps: + - name: Allowed pinned action + # ok: cdt.github-actions.external-action-sha-pinned + # ok: cdt.github-actions.external-action-approved-allowlist + # ok: cdt.github-actions.external-action-version-comment + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Moving tag action + # ruleid: cdt.github-actions.external-action-sha-pinned + uses: actions/checkout@v7 + + - name: Unapproved external action + # ruleid: cdt.github-actions.external-action-approved-allowlist + uses: unapproved/example-action@1111111111111111111111111111111111111111 # v1.0.0 + + - name: Missing readable version comment + # ruleid: cdt.github-actions.external-action-version-comment + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + + - name: Local action + # ok: cdt.github-actions.external-action-sha-pinned + # ok: cdt.github-actions.external-action-approved-allowlist + # ok: cdt.github-actions.external-action-version-comment + uses: ./.github/actions/setup-just From 930238ab99aff4dfc90d3834a8eefe99f1c57966 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 25 Jul 2026 16:25:23 -0700 Subject: [PATCH 7/9] fix(ci): stabilize cached builds and action policy validation - Isolate ccache from compiler-toolchain dependency resolution. - Normalize Windows preset names before invoking CMake. - Require immutable container digests and readable action version comments. - Document the platform-specific build launchers. --- README.md | 2 +- scripts/build.bat | 15 ++++++++++++--- scripts/pkgx-build.sh | 16 ++++++++++++---- semgrep.yaml | 11 +++++++---- .../semgrep/.github/workflows/action_policy.yml | 16 ++++++++++++++++ 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 94b65a20e..71d8375d1 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ 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`. Its +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 diff --git a/scripts/build.bat b/scripts/build.bat index c97f8c5a2..d6433ce4d 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -6,9 +6,18 @@ FOR %%I IN ("%SCRIPT_DIR%..") DO SET "REPO_ROOT=%%~fI" IF NOT "%~2"=="" GOTO PRESET_USAGE SET "PRESET=%~1" IF NOT DEFINED PRESET SET "PRESET=reference" -IF /I "%PRESET%"=="reference" GOTO PRESET_READY -IF /I "%PRESET%"=="parallel" GOTO PRESET_READY -IF /I "%PRESET%"=="debug" GOTO PRESET_READY +IF /I "%PRESET%"=="reference" ( + SET "PRESET=reference" + GOTO PRESET_READY +) +IF /I "%PRESET%"=="parallel" ( + SET "PRESET=parallel" + GOTO PRESET_READY +) +IF /I "%PRESET%"=="debug" ( + SET "PRESET=debug" + GOTO PRESET_READY +) :PRESET_USAGE echo Usage: %~nx0 [reference^|parallel^|debug] 1>&2 diff --git a/scripts/pkgx-build.sh b/scripts/pkgx-build.sh index 598a67703..b01d8af6c 100755 --- a/scripts/pkgx-build.sh +++ b/scripts/pkgx-build.sh @@ -27,9 +27,17 @@ if [[ -n "${CDT_PKGX_COMPILER_PACKAGE:-}" ]]; then pkgx_tools+=("+${CDT_PKGX_COMPILER_PACKAGE}") fi +pkgx_launcher=(pkgx "${pkgx_tools[@]}" --) case "${CDT_COMPILER_CACHE:-}" in "" | off) ;; - ccache) pkgx_tools+=("+ccache.dev@${ccache_version}") ;; + ccache) + pkgx_launcher=( + pkgx + "+ccache.dev@${ccache_version}" + -- + "${pkgx_launcher[@]}" + ) + ;; *) printf 'Unsupported CDT_COMPILER_CACHE=%s; expected ccache, off, or an empty value.\n' \ "${CDT_COMPILER_CACHE}" >&2 @@ -41,7 +49,7 @@ cd -- "${repo_root}" if [[ "${1:-}" == "--codeql" ]]; then shift - exec pkgx "${pkgx_tools[@]}" -- "${script_dir}/codeql-build.sh" "$@" + exec "${pkgx_launcher[@]}" "${script_dir}/codeql-build.sh" "$@" fi if [[ "${1:-}" == "--preset" ]]; then @@ -52,7 +60,7 @@ if [[ "${1:-}" == "--preset" ]]; then exit 2 fi preset="$2" - exec pkgx "${pkgx_tools[@]}" -- "${script_dir}/build.sh" "${preset}" + exec "${pkgx_launcher[@]}" "${script_dir}/build.sh" "${preset}" fi if [[ "$#" -ne 0 ]]; then @@ -61,4 +69,4 @@ if [[ "$#" -ne 0 ]]; then exit 2 fi -exec pkgx "${pkgx_tools[@]}" -- "${script_dir}/build.sh" reference +exec "${pkgx_launcher[@]}" "${script_dir}/build.sh" reference diff --git a/semgrep.yaml b/semgrep.yaml index 05d5b96ca..8963ba72c 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -46,10 +46,12 @@ rules: languages: - regex severity: WARNING - message: "Pin external GitHub Actions to a full 40-character commit SHA." + message: >- + Pin external GitHub Actions to a full 40-character commit SHA and + container actions to a sha256 digest. metadata: category: security - rationale: "Moving tags can change workflow behavior without review." + rationale: "Moving action and container tags can change workflow behavior without review." paths: include: - "/.github/actions/**/*.yaml" @@ -58,8 +60,9 @@ rules: - "/.github/workflows/**/*.yml" - "/tests/semgrep/.github/**/*.yaml" - "/tests/semgrep/.github/**/*.yml" - patterns: + pattern-either: - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@(?![a-fA-F0-9]{40}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length + - pattern-regex: '(?m)^\s*uses:\s*docker://(?![^\s#]+@sha256:[a-fA-F0-9]{64}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length - id: cdt.github-actions.external-action-approved-allowlist languages: - regex @@ -99,7 +102,7 @@ rules: - "/tests/semgrep/.github/**/*.yaml" - "/tests/semgrep/.github/**/*.yml" patterns: - - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@[a-fA-F0-9]{40}\s*$' # yamllint disable-line rule:line-length + - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@[a-fA-F0-9]{40}(?![ \t]+#[ \t]+v[0-9]+(?:\.[0-9]+){1,2}(?:[-+][0-9A-Za-z.-]+)?[ \t]*$)[^\r\n]*$' # yamllint disable-line rule:line-length - id: cdt.cpp.no-crtp-inheritance languages: - cpp diff --git a/tests/semgrep/.github/workflows/action_policy.yml b/tests/semgrep/.github/workflows/action_policy.yml index b3cae3efc..7ef79e472 100644 --- a/tests/semgrep/.github/workflows/action_policy.yml +++ b/tests/semgrep/.github/workflows/action_policy.yml @@ -25,6 +25,22 @@ jobs: # ruleid: cdt.github-actions.external-action-version-comment uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + - name: Non-version action comment + # ruleid: cdt.github-actions.external-action-version-comment + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # pinned + + - name: Valid shortened version comment + # ok: cdt.github-actions.external-action-version-comment + uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 + + - name: Mutable container action + # ruleid: cdt.github-actions.external-action-sha-pinned + uses: docker://alpine:3.20 + + - name: Digest-pinned container action + # ok: cdt.github-actions.external-action-sha-pinned + uses: docker://alpine@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - name: Local action # ok: cdt.github-actions.external-action-sha-pinned # ok: cdt.github-actions.external-action-approved-allowlist From 745d4029b5fb5258ae69617b2d393df7169bccd1 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 25 Jul 2026 18:11:11 -0700 Subject: [PATCH 8/9] fix(ci): enforce action policy on nested paths Apply SHA pinning, allowlist, and version-comment checks to GitHub Action references with arbitrarily nested path segments. --- semgrep.yaml | 6 +++--- tests/semgrep/.github/workflows/action_policy.yml | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/semgrep.yaml b/semgrep.yaml index 8963ba72c..87a077ab3 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -61,7 +61,7 @@ rules: - "/tests/semgrep/.github/**/*.yaml" - "/tests/semgrep/.github/**/*.yml" pattern-either: - - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@(?![a-fA-F0-9]{40}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length + - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@(?![a-fA-F0-9]{40}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length - pattern-regex: '(?m)^\s*uses:\s*docker://(?![^\s#]+@sha256:[a-fA-F0-9]{64}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length - id: cdt.github-actions.external-action-approved-allowlist languages: @@ -82,7 +82,7 @@ rules: - "/tests/semgrep/.github/**/*.yaml" - "/tests/semgrep/.github/**/*.yml" patterns: - - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)(?!(?:actions/(?:cache|checkout|setup-python|upload-artifact)|astral-sh/setup-uv|codecov/codecov-action|DenverCoder1/doxygen-github-pages-action|github/codeql-action/(?:analyze|init)|ilammy/msvc-dev-cmd|JamesIves/github-pages-deploy-action|lukka/run-vcpkg|pkgxdev/setup|taiki-e/(?:cache-cargo-install-action|install-action))@)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@' # yamllint disable-line rule:line-length + - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)(?!(?:actions/(?:cache|checkout|setup-python|upload-artifact)|astral-sh/setup-uv|codecov/codecov-action|DenverCoder1/doxygen-github-pages-action|github/codeql-action/(?:analyze|init)|ilammy/msvc-dev-cmd|JamesIves/github-pages-deploy-action|lukka/run-vcpkg|pkgxdev/setup|taiki-e/(?:cache-cargo-install-action|install-action))@)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@' # yamllint disable-line rule:line-length - id: cdt.github-actions.external-action-version-comment languages: - regex @@ -102,7 +102,7 @@ rules: - "/tests/semgrep/.github/**/*.yaml" - "/tests/semgrep/.github/**/*.yml" patterns: - - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@[a-fA-F0-9]{40}(?![ \t]+#[ \t]+v[0-9]+(?:\.[0-9]+){1,2}(?:[-+][0-9A-Za-z.-]+)?[ \t]*$)[^\r\n]*$' # yamllint disable-line rule:line-length + - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@[a-fA-F0-9]{40}(?![ \t]+#[ \t]+v[0-9]+(?:\.[0-9]+){1,2}(?:[-+][0-9A-Za-z.-]+)?[ \t]*$)[^\r\n]*$' # yamllint disable-line rule:line-length - id: cdt.cpp.no-crtp-inheritance languages: - cpp diff --git a/tests/semgrep/.github/workflows/action_policy.yml b/tests/semgrep/.github/workflows/action_policy.yml index 7ef79e472..bbfa66dbb 100644 --- a/tests/semgrep/.github/workflows/action_policy.yml +++ b/tests/semgrep/.github/workflows/action_policy.yml @@ -33,6 +33,16 @@ jobs: # ok: cdt.github-actions.external-action-version-comment uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 + # yamllint disable rule:line-length + - name: Nested moving-tag action + # ruleid: cdt.github-actions.external-action-sha-pinned,cdt.github-actions.external-action-approved-allowlist + uses: unapproved/example-action/path/to/action@v1 + + - name: Nested pinned action without a version comment + # ruleid: cdt.github-actions.external-action-approved-allowlist,cdt.github-actions.external-action-version-comment + uses: unapproved/example-action/path/to/action@3333333333333333333333333333333333333333 + # yamllint enable rule:line-length + - name: Mutable container action # ruleid: cdt.github-actions.external-action-sha-pinned uses: docker://alpine:3.20 From 406a4827230459efe2026395dde11dab8cf7889c Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 25 Jul 2026 18:51:17 -0700 Subject: [PATCH 9/9] fix(ci): enforce action policy on shorthand steps Apply SHA pinning, allowlist, and version-comment checks to GitHub Action shorthand list entries, including container actions. --- semgrep.yaml | 8 ++++---- .../.github/workflows/action_policy.yml | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/semgrep.yaml b/semgrep.yaml index 87a077ab3..0460befd6 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -61,8 +61,8 @@ rules: - "/tests/semgrep/.github/**/*.yaml" - "/tests/semgrep/.github/**/*.yml" pattern-either: - - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@(?![a-fA-F0-9]{40}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length - - pattern-regex: '(?m)^\s*uses:\s*docker://(?![^\s#]+@sha256:[a-fA-F0-9]{64}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length + - pattern-regex: '(?m)^\s*(?:-[ \t]+)?uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@(?![a-fA-F0-9]{40}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length + - pattern-regex: '(?m)^\s*(?:-[ \t]+)?uses:\s*docker://(?![^\s#]+@sha256:[a-fA-F0-9]{64}(?:\s+#|$))[^\s#]+' # yamllint disable-line rule:line-length - id: cdt.github-actions.external-action-approved-allowlist languages: - regex @@ -82,7 +82,7 @@ rules: - "/tests/semgrep/.github/**/*.yaml" - "/tests/semgrep/.github/**/*.yml" patterns: - - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)(?!(?:actions/(?:cache|checkout|setup-python|upload-artifact)|astral-sh/setup-uv|codecov/codecov-action|DenverCoder1/doxygen-github-pages-action|github/codeql-action/(?:analyze|init)|ilammy/msvc-dev-cmd|JamesIves/github-pages-deploy-action|lukka/run-vcpkg|pkgxdev/setup|taiki-e/(?:cache-cargo-install-action|install-action))@)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@' # yamllint disable-line rule:line-length + - pattern-regex: '(?m)^\s*(?:-[ \t]+)?uses:\s*(?!\./)(?!docker://)(?!(?:actions/(?:cache|checkout|setup-python|upload-artifact)|astral-sh/setup-uv|codecov/codecov-action|DenverCoder1/doxygen-github-pages-action|github/codeql-action/(?:analyze|init)|ilammy/msvc-dev-cmd|JamesIves/github-pages-deploy-action|lukka/run-vcpkg|pkgxdev/setup|taiki-e/(?:cache-cargo-install-action|install-action))@)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@' # yamllint disable-line rule:line-length - id: cdt.github-actions.external-action-version-comment languages: - regex @@ -102,7 +102,7 @@ rules: - "/tests/semgrep/.github/**/*.yaml" - "/tests/semgrep/.github/**/*.yml" patterns: - - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@[a-fA-F0-9]{40}(?![ \t]+#[ \t]+v[0-9]+(?:\.[0-9]+){1,2}(?:[-+][0-9A-Za-z.-]+)?[ \t]*$)[^\r\n]*$' # yamllint disable-line rule:line-length + - pattern-regex: '(?m)^\s*(?:-[ \t]+)?uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@[a-fA-F0-9]{40}(?![ \t]+#[ \t]+v[0-9]+(?:\.[0-9]+){1,2}(?:[-+][0-9A-Za-z.-]+)?[ \t]*$)[^\r\n]*$' # yamllint disable-line rule:line-length - id: cdt.cpp.no-crtp-inheritance languages: - cpp diff --git a/tests/semgrep/.github/workflows/action_policy.yml b/tests/semgrep/.github/workflows/action_policy.yml index bbfa66dbb..7d1c9c399 100644 --- a/tests/semgrep/.github/workflows/action_policy.yml +++ b/tests/semgrep/.github/workflows/action_policy.yml @@ -33,6 +33,26 @@ jobs: # ok: cdt.github-actions.external-action-version-comment uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 + # ruleid: cdt.github-actions.external-action-sha-pinned + - uses: actions/checkout@v7 + + # ruleid: cdt.github-actions.external-action-approved-allowlist + - uses: unapproved/example-action@1111111111111111111111111111111111111111 # v1.0.0 + + # ruleid: cdt.github-actions.external-action-version-comment + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + + # ok: cdt.github-actions.external-action-sha-pinned + # ok: cdt.github-actions.external-action-approved-allowlist + # ok: cdt.github-actions.external-action-version-comment + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # ruleid: cdt.github-actions.external-action-sha-pinned + - uses: docker://alpine:3.20 + + # ok: cdt.github-actions.external-action-sha-pinned + - uses: docker://alpine@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + # yamllint disable rule:line-length - name: Nested moving-tag action # ruleid: cdt.github-actions.external-action-sha-pinned,cdt.github-actions.external-action-approved-allowlist