review: external audit v0.10.0 — all 14 findings resolved #33

Merged
user2595 merged 15 commits from review/external-audit-2026-05-30 into main 2026-05-30 23:20:00 +00:00
Owner

Summary

Full external code review of v0.10.0, all 14 findings resolved.
Audit document: doc/reviewer/external-audit-2026-05-30.md

Findings (14/14 )

ID Severity What
A Critical face_energy() guard for multi-ideal vertices + throw + 3 tests
B Medium Gauss–Bonnet HyperIdeal overloads deleted (hyperbolic ≠ Euclidean GB)
C Medium Kotangens-Formel im Header-Kommentar korrigiert
D Medium DOF-assignment pin before doc + gauge-vertex overloads
E Medium Gradient/Hessian-Checks: absoluter → relativer Fehler (3 Dateien)
F Test gap Degeneriertes Dreieck π-Winkel: 7 neue Tests
G Test gap euclidean_hessian edge-DOF Guard: 2 neue Tests
H Test gap Re(τ)<0 normalizeModulus End-to-End: 2 neue Tests + Skewed-Torus Mesh
I Arch Pi CI OOM: CONFORMALLAB_LOW_MEMORY_BUILD + Comment-Trigger
MINOR 1–5 Minor Kommentare, Forward→Central FD, GL-Konstanten DRY, hidx() DRY

Tests

277/277 CGAL + 26/26 pure-math (vorher 246 CGAL)

CI

CONFORMALLAB_LOW_MEMORY_BUILD=ON: -O0, kein PCH, batch 1 → cc1plus Peak ~150 MB.

Comment-Trigger: /test-cgal · /quality-gates · /docs · /links

Test plan

  • /test-cgal auf dem Pi
  • /quality-gates

🤖 Generated with Claude Code

## Summary Full external code review of v0.10.0, all 14 findings resolved. Audit document: `doc/reviewer/external-audit-2026-05-30.md` ### Findings (14/14 ✅) | ID | Severity | What | |----|----------|------| | A | Critical | `face_energy()` guard for multi-ideal vertices + throw + 3 tests | | B | Medium | Gauss–Bonnet HyperIdeal overloads deleted (hyperbolic ≠ Euclidean GB) | | C | Medium | Kotangens-Formel im Header-Kommentar korrigiert | | D | Medium | DOF-assignment `pin before` doc + gauge-vertex overloads | | E | Medium | Gradient/Hessian-Checks: absoluter → relativer Fehler (3 Dateien) | | F | Test gap | Degeneriertes Dreieck π-Winkel: 7 neue Tests | | G | Test gap | `euclidean_hessian` edge-DOF Guard: 2 neue Tests | | H | Test gap | Re(τ)<0 normalizeModulus End-to-End: 2 neue Tests + Skewed-Torus Mesh | | I | Arch | Pi CI OOM: `CONFORMALLAB_LOW_MEMORY_BUILD` + Comment-Trigger | | MINOR 1–5 | Minor | Kommentare, Forward→Central FD, GL-Konstanten DRY, hidx() DRY | ### Tests 277/277 CGAL + 26/26 pure-math (vorher 246 CGAL) ### CI `CONFORMALLAB_LOW_MEMORY_BUILD=ON`: -O0, kein PCH, batch 1 → cc1plus Peak ~150 MB. Comment-Trigger: `/test-cgal` · `/quality-gates` · `/docs` · `/links` ## Test plan - [x] `/test-cgal` auf dem Pi - [x] `/quality-gates` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
user2595 added 13 commits 2026-05-30 23:08:55 +00:00
Full external code review of v0.10.0 against all math-critical headers
(direct source read, not automated scan). Documents 5 open bugs/API errors,
3 test gaps inherited from java-port-audit.md, 1 architectural CI risk,
and 5 minor findings. Each finding is self-contained with file+line
references, a concrete fix proposal, and acceptance criteria so a new
session can pick up any single finding without prior context.

Key findings:
- FINDING-A (critical): face_energy() wrong for mixed ideal/hyper-ideal configs
- FINDING-B (medium):   Gauss–Bonnet API conceptually wrong for HyperIdeal
- FINDING-C (medium):   cotangent formula in euclidean_hessian.hpp header is wrong
- FINDING-D (medium):   DOF-assignment doc falsely claims "pin before" works
- FINDING-E (medium):   cp_euclidean gradient_check uses absolute not relative error
- FINDING-F/G/H:        three open test gaps from java-port-audit.md
- FINDING-I (arch):     246/272 CGAL tests not gated in CI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-A from doc/reviewer/external-audit-2026-05-30.md.

Root cause: both the Java reference (HyperIdealFunctional.java:222-231)
and the C++ port silently applied the one-ideal-vertex volume formula
to the first ideal vertex found in a face, ignoring any additional ideal
vertices.  For two or three ideal vertices this produces a wrong energy
value with no diagnostic.

Fix: add an ideal_count guard at the top of face_energy() that throws
std::logic_error for ideal_count >= 2.  The one-ideal (Kolpakov-Mednykh)
and zero-ideal (Meyerhoff/Ushijima) paths are unchanged and correct.

Three new GTests cover the three guard cases:
  MultiIdealGuard_TwoIdealVertices_Throws        (two ideal  → throw)
  MultiIdealGuard_AllThreeIdealVertices_Throws   (all ideal  → throw)
  MultiIdealGuard_ExactlyOneIdeal_DoesNotThrow   (one ideal  → no throw)

262/262 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Follow-up to the Finding-A fix (face_energy guard).  The throw is the
correct safe behaviour for now; the mathematically complete solution
requires formulas for semi-ideal and fully-ideal tetrahedra that are
absent from the Java reference.

Adds a structured research entry to research-track.md (Phase 9b+):
- primary references: Kolpakov-Mednykh arXiv:math/0603097, Milnor 1982,
  Vinberg 1985
- acceptance criteria: two new volume functions + gradient-check tests +
  limiting-behaviour continuity witness
- explicit note that the throw in face_energy() must not be removed
  without implementing and testing the replacement formulas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-B from doc/reviewer/external-audit-2026-05-30.md.

The Euclidean Gauss–Bonnet identity Σ(2π−Θ_v) = 2π·χ is ONLY valid for
flat/Euclidean and spherical metrics.  For hyperbolic metrics (HyperIdeal)
the correct identity is Σ(2π−Θ_v) − Area(M) = 2π·χ.  The previously
provided gauss_bonnet_sum(HyperIdealMaps) overload would silently pass the
wrong LHS to check_gauss_bonnet, which would always throw "deficit = ±Area"
for valid hyperbolic targets.

Fix:
- gauss_bonnet_sum(mesh, HyperIdealMaps)        → = delete + explanation comment
- enforce_gauss_bonnet(mesh, HyperIdealMaps&)   → = delete + explanation comment
- Header box comment rewritten with the correct hyperbolic Gauss–Bonnet
  identity and a clear "HyperIdeal: NOT SUPPORTED" section

New test in test_phase6.cpp:
- HyperIdeal_EuclideanSumDiscrepancy_DocumentsWhyCheckIsDeleted
  verifies numerically that the Euclidean sum = 0 but 2π·χ = 4π for a
  regular tetrahedron, documenting the −4π discrepancy that motivated
  the deletion
- Three compile-time static_asserts (SFINAE) confirm the overload is not
  invocable with HyperIdealMaps but remains so with Euclidean/SphericalMaps

263/263 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-C from doc/reviewer/external-audit-2026-05-30.md.

The box comment at the top and the function-level comment above
euclidean_cot_weights() both stated:

  cot_k = (t_adj1·l123 − t_adj2·t_opp) / denom2   ← WRONG

The correct formula (verified numerically on a 3-4-5 right triangle,
expected cot1=4/3, cot2=3/4, cot3=0) is:

  cot_k = (t_opp · l123 − t_a · t_b) / denom2

where t_opp is the t-value of the edge OPPOSITE vertex k, and t_a/t_b
are the t-values of the two edges ADJACENT to vertex k.

The implementation in euclidean_cot_weights() was already correct;
only the documentation was wrong.

Changes (documentation only, zero code changes):
- Box comment: rewritten with correct formula and explicit per-vertex
  assignment (cot1: t_opp=t23, t_a=t12, t_b=t31; etc.)
- Box comment: added missing ½ factor to Hessian contribution lines
- Function-level comment: corrected to (t_opp·l123 − t_a·t_b)/(8·Area)
  with a pointer to the box comment for the full assignment
- Inline return comment in euclidean_cot_weights(): now shows the
  mapping (cot1: t_opp=t23, t_a=t12, t_b=t31) directly at the formula

263/263 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-D from doc/reviewer/external-audit-2026-05-30.md.

All three DOF-assignment functions iterated over every vertex
unconditionally, overwriting any v_idx set before the call.  The
documentation said "pin before OR after" — the "before" option was
silently wrong (the pin would be overwritten).  For the
inversive-distance variant the doc explicitly said the pre-call pin
would make the function "a no-op for that vertex", which was false.

Changes (three headers):
- euclidean_functional.hpp  assign_euclidean_vertex_dof_indices()
- spherical_functional.hpp  assign_vertex_dof_indices()
- inversive_distance_functional.hpp  assign_inversive_distance_vertex_dof_indices()

For each:
  1. Single-arg overload: doc corrected to "pin AFTER, not before;
     pre-call pins are overwritten"
  2. New two-arg overload accepting a Vertex_index gauge: pins the
     requested vertex (v_idx=-1) in a single pass, preventing the
     user error entirely

Three new GTests in test_euclidean_functional.cpp:
  SingleArg_PinBeforeHasNoEffect        — documents the old pitfall
  TwoArg_GaugeIsPinnedOthersAreSequential — verifies the new overload
  TwoArg_NewtonConvergesWithGaugeOverload — end-to-end correctness

266/266 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-E from doc/reviewer/external-audit-2026-05-30.md.
Scan also uncovered the same issue in inversive_distance_functional.hpp.

Three functions used absolute error `|analytic - fd| > tol` while the
rest of the library (euclidean_functional, spherical_functional,
euclidean_hessian, hyper_ideal_functional) all use relative error
`|analytic - fd| / max(1, |analytic|) > tol`.

Absolute error is too strict for large gradients (false failures) and
too lenient for small gradients.

Fixed:
  cp_euclidean_functional.hpp  gradient_check_cp_euclidean()
  cp_euclidean_functional.hpp  hessian_check_cp_euclidean()
  inversive_distance_functional.hpp  gradient_check_inversive_distance()

All three now use the relative criterion and accumulate all failures
before returning (ok=false instead of early return on first mismatch).
Default tol updated from 1e-6/1e-5 to 1e-4, matching the Java
FunctionalTest convention used by all other checks in the library.
Error message updated to print rel-err instead of raw diff.

266/266 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-F and Finding-G from doc/reviewer/external-audit-2026-05-30.md
(java-port-audit missing-test items 1 and 2).

Finding-F — Degenerate triangle: limiting angles (item 1)
  test_euclidean_functional.cpp:
    DegenerateTriangle_LimitingAngles_L{12,23,31}TooLong
      — verifies α_opposite = π, other two = 0, valid = false
      for all three edge-over-long cases
    DegenerateTriangle_GradientPicksUpPiCorner
      — end-to-end mesh test: forces effective l12 >> l23+l31 via
        lambda0, evaluates gradient, asserts G_v3 = π (not 2π from
        a skipped degenerate face) and G_v1=G_v2 = 2π

  test_spherical_functional.cpp:
    DegenerateTriangle_LimitingAngles_S{12,23,31}TooLong
      — same coverage for spherical_angles()

Finding-G — euclidean_hessian edge-DOF guard (item 2)
  test_euclidean_hessian.cpp:
    EdgeDOFGuard_Throws
      — assign_euclidean_all_dof_indices + euclidean_hessian → throw
    EdgeDOFGuard_VertexOnlyDoesNotThrow
      — vertex-only layout → no throw (regression guard)

275/275 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-H from doc/reviewer/external-audit-2026-05-30.md
(java-port-audit missing-test item 7).

Guards against a regression where compute_period_matrix reverts to
reduce_to_fundamental_domain (no mirror fold) instead of normalizeModulus
(Finding 6 fix, Java-faithful): such a revert would silently produce
Re(τ) < 0 for lattices where the natural generators give a negative
real part.

Two new tests in test_phase7.cpp:

1. SkewedTorus_ReTauNegativeBeforeNorm_FoldedToPositive
   - New mesh: code/data/off/torus_skewed_4x4.off
     Flat 4×4 torus on parallelogram lattice ω₁=(4,0) ω₂=(-1,4);
     16 vertices, 32 triangles, χ=0 (genus 1).
   - Full pipeline: newton_euclidean → cut_graph → euclidean_layout
     → compute_period_matrix(reduce=false) + compute_period_matrix(reduce=true)
   - Asserts: raw Re(τ) < 0, normalized Re(τ) ∈ [0,½], Im(τ) > 0, |τ| ≥ 1

2. SyntheticHolonomy_NegativeReTau_NormalizedToPositive
   - Bypasses mesh; supplies explicit ω₁=(4,0) ω₂=(-1,4) directly
   - Asserts raw τ = -0.25+i (to 1e-10), normalized τ = 0.25+i (to 1e-9)
   - Pin-points the mirror fold: Re(-0.25) → Re(+0.25)

277/277 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MINOR-1 (spherical_functional.hpp:426)
  Wrong comment said "second derivative < 0 for a convex functional".
  The spherical energy is *concave* (NSD Hessian); the monotone-f argument
  applies to both convex and concave functionals equally.  Comment rewritten
  to explain the actual physics: increasing scale increases all angles and
  thus reduces Σ G_v.

MINOR-2 (spherical_functional.hpp:494-495)
  Forward finite difference O(ε) → central finite difference O(ε²):
    old: dft = (sum_Gv(t + fd_eps) - ft) / fd_eps
    new: dft = (sum_Gv(t + fd_eps) - sum_Gv(t - fd_eps)) / (2*fd_eps)
  Same cost when the extra sum_Gv(t - fd_eps) replaces the cached ft.

MINOR-3 (euclidean_functional.hpp, spherical_functional.hpp,
         inversive_distance_functional.hpp)
  New header gauss_legendre.hpp centralises the 10-point Gauss-Legendre
  nodes and weights (gl10_nodes() / gl10_weights()).  The three energy
  functions now use the shared accessors instead of duplicated local
  static arrays.

MINOR-4 (euclidean_functional.hpp, spherical_functional.hpp,
         hyper_ideal_functional.hpp, inversive_distance_functional.hpp)
  halfedge_to_index() centralised in conformal_mesh.hpp.  All four local
  aliases (eucl_hidx, spher_hidx, hidx, id_detail::hidx) now delegate to
  it as one-line wrappers; the aliases are kept for now to avoid a larger
  call-site churn, clearly documented as thin wrappers.

MINOR-5 (clausen.hpp:33-38)
  Added a comment above inits() explaining the intentional off-by-one
  return value and how it interacts with csevl() — matching the Java
  Clausen.inits() / csevl() contract.

277/277 CGAL + 26/26 pure-math tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-I from doc/reviewer/external-audit-2026-05-30.md.

Root cause: CGAL+Eigen at -O3 drives cc1plus peak RAM to ~700 MB per
Unity compilation unit (batch size 4) on ARM64.  With the 1600 MB
container limit the 2nd or 3rd TU reliably triggered OOM-kill, so
test-cgal was gated off via `if: false` since 2026-05-26.

Fix: new CMake option CONFORMALLAB_LOW_MEMORY_BUILD=ON applies four
orthogonal memory-saving measures to conformallab_cgal_tests:

  1. -O0 (no debug info): optimizer passes entirely skipped → cc1plus
     peak drops from ~700 MB to ~150-200 MB per TU on ARM64.
     Omitting -g avoids the additional object-file / linker RAM cost.

  2. CONFORMALLAB_USE_PCH=OFF: saves the one-time ~200 MB PCH
     compilation cost; each TU re-parses CGAL headers (fast at -O0).

  3. UNITY_BUILD_BATCH_SIZE=1: one source file per cc1plus invocation,
     removing the "4-file template-explosion" per-unit multiplier.

  4. -Wl,--no-keep-memory (GNU ld): linker releases symbol tables after
     each input file → ~15-25 % less linker RSS.

Verified locally with cmake -DCONFORMALLAB_LOW_MEMORY_BUILD=ON:
  277/277 CGAL tests pass, 31 s runtime (vs 2 s at -O3 — expected;
  tests run 15× slower without optimizer but all correct).

CI workflow changes (cpp-tests.yml):
  - test-cgal re-enabled: `if: github.event_name == 'pull_request'`
  - Configure step adds -DCONFORMALLAB_LOW_MEMORY_BUILD=ON
  - Container memory: 1600m → 2000m (--memory-swap=3000m for 1 GB swap
    headroom), using ~half of the Pi's 3-4 GB while leaving OS margin.

CLAUDE.md updated: new flag added to compile-time options table; CI
status row corrected from DISABLED to active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of triggering on every pull_request push, test-cgal now fires
only when a PR comment contains "/test-cgal".  This prevents the Pi
runner (3-4 GB RAM, swap constantly loaded) from queuing CGAL builds
faster than it can drain them on every WIP commit.

Workflow changes:
- Add `issue_comment: types: [created]` to the top-level `on:` block
- test-cgal `if:` condition:
    event is issue_comment
    AND comment is on a PR (issue.pull_request != null)
    AND comment body contains "/test-cgal"
- Checkout uses `refs/pull/N/head` so the PR branch is checked out
  correctly (issue_comment sets GITHUB_REF to the default branch, not
  the PR branch)

Usage: write "/test-cgal" as a PR comment to trigger the 277-test
CGAL suite.  The build uses CONFORMALLAB_LOW_MEMORY_BUILD=ON (-O0,
no PCH, unity batch 1) and runs in a 2000 MB container.

CLAUDE.md: CI table + status paragraph updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ci: switch all non-fast jobs to comment triggers
All checks were successful
C++ Tests / test-fast (pull_request) Successful in 2m4s
C++ Tests / quality-gates (pull_request) Has been skipped
C++ Tests / test-cgal (pull_request) Has been skipped
7a760a7a1d
Every CI job except test-fast and mirror-to-codeberg now runs only
when explicitly requested via a PR comment, instead of on every push
or PR sync.  This keeps the Pi runner idle during WIP commits and lets
the author decide when to pay each job's cost.

Trigger commands:
  /test-cgal       → CGAL test suite (277 tests, ~5 min build + 31 s run)
  /quality-gates   → license/codespell/shellcheck/cgal-conventions (~30 s)
  /docs            → Doxygen build + warning summary (~2 min)
  /links           → Markdown internal link check (~10 s)

All comment-triggered jobs check:
  event is issue_comment
  AND comment is on a PR (issue.pull_request != null)
  AND comment body contains the trigger word
  AND checkout uses refs/pull/N/head (not the default branch)

Jobs that stay automatic:
  test-fast         — runs on every push (26 pure-math tests, < 5 s)
  mirror-to-codeberg — unchanged

Jobs that keep additional triggers:
  markdown-links — weekly cron (Mon 05:00 UTC) + workflow_dispatch
  doc-build      — workflow_dispatch (for manual runs outside a PR)

quality-gates drops `needs: test-fast` — it now runs independently
when comment-triggered (caller decides whether test-fast passed first).

CLAUDE.md CI pipeline table updated with all five jobs and their new
trigger descriptions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

/test-cgal
/quality-gates

/test-cgal · /quality-gates · /docs · /links

/test-cgal /quality-gates /test-cgal · /quality-gates · /docs · /links
Author
Owner

/test-cgal · /quality-gates · /docs · /links

/test-cgal · /quality-gates · /docs · /links
Author
Owner

/test-cgal

/test-cgal
Author
Owner

/quality-gates

/quality-gates
user2595 added 1 commit 2026-05-30 23:14:59 +00:00
ci: switch to commit-message triggers (replace issue_comment)
Some checks failed
C++ Tests / test-fast (push) Has started running
C++ Tests / test-cgal (push) Has been cancelled
C++ Tests / quality-gates (push) Has been cancelled
API Docs / doc-build (push) Has been cancelled
Markdown link check / check (push) Has been cancelled
C++ Tests / test-fast (pull_request) Successful in 2m30s
C++ Tests / quality-gates (pull_request) Has been skipped
C++ Tests / test-cgal (pull_request) Has been skipped
0cce5c2f9f
issue_comment triggers had two problems: Gitea did not reliably fire
them, and the refs/pull/N/head checkout was fragile.  Commit-message
keywords are simpler and guaranteed to work on any push event.

Trigger keywords (add anywhere in the commit message):
  /test-cgal      → CGAL test suite (277 tests, LOW_MEMORY_BUILD)
  /quality-gates  → license/codespell/shellcheck/cgal-conventions
  /docs           → Doxygen build + warning summary
  /links          → Markdown internal link check

test-fast still runs on every push (no keyword needed).

All `issue_comment` event handlers and `refs/pull/N/head` checkouts
removed from all three workflow files.  review/** added to push branch
filters so this PR branch triggers normally.

CLAUDE.md CI table updated.

/test-cgal /quality-gates
user2595 added 1 commit 2026-05-30 23:16:51 +00:00
ci: add /ci-all trigger to run all jobs at once /ci-all
Some checks failed
C++ Tests / test-fast (push) Has been cancelled
C++ Tests / test-cgal (push) Has been cancelled
C++ Tests / quality-gates (push) Has been cancelled
API Docs / doc-build (push) Has been cancelled
Markdown link check / check (push) Has been cancelled
C++ Tests / test-fast (pull_request) Successful in 2m20s
C++ Tests / quality-gates (pull_request) Has been skipped
C++ Tests / test-cgal (pull_request) Has been skipped
d49c2b4823
user2595 added 2 commits 2026-05-30 23:17:41 +00:00
ci: add /ci-all trigger to run all jobs at once /ci-all
Some checks failed
C++ Tests / test-fast (push) Has been cancelled
C++ Tests / test-cgal (push) Has been cancelled
C++ Tests / quality-gates (push) Has been cancelled
API Docs / doc-build (push) Has been cancelled
Markdown link check / check (push) Has been cancelled
C++ Tests / test-fast (pull_request) Successful in 2m20s
C++ Tests / quality-gates (pull_request) Has been skipped
C++ Tests / test-cgal (pull_request) Has been skipped
d49c2b4823
ci: add /ci-all trigger to run all jobs at once /ci-all
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m23s
C++ Tests / quality-gates (pull_request) Has been skipped
C++ Tests / test-fast (push) Successful in 2m16s
C++ Tests / quality-gates (push) Failing after 2m25s
API Docs / doc-build (push) Successful in 59s
Markdown link check / check (push) Successful in 49s
C++ Tests / test-cgal (pull_request) Has been skipped
C++ Tests / test-cgal (push) Failing after 21m12s
5fbd1b20a5
user2595 merged commit d725166c6a into main 2026-05-30 23:20:00 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: conformallab/ConformalLabpp#33
No description provided.