serialization.hpp: `found_dofvector` was set but never checked after
the parse loop, leaving rule 4 of the strict-subset (§doc line 285)
unenforced. Add the missing post-loop throw so a ConformalResult XML
that omits the <DOFVector> element is rejected with a clear error
instead of silently returning an empty x. Update the docstring to
remove the misleading "silently returns" note.
finding-orchestration.md: mark H3/H4/H5/V5/V6 ✅ and record the S3
session as complete. Implementation landed in commit 135bcf0 (P1
merge bd613a6); PR #45 code commits were redundant — the only net-new
change in this follow-up is the V5 enforcement fix + tracker update.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add CLAUDE_VERBOSE=1 and outputStyle=Explanatory to project settings
so tool calls and bash outputs are fully visible in the terminal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brings doc/roadmap/ to the same operational level as doc/reviewer/ by adding
the two missing structural files and updating existing docs to close the gap
identified in the system review.
New files:
- doc/roadmap/phase-orchestration.md — master phase table (model assignments,
session IDs, status, review-gate checklist); mirrors finding-orchestration.md
- doc/roadmap/session-prompts.md — copy-paste-ready prompts for P1–P4
(Haiku/Sonnet/Opus) + universal Opus review gate; mirrors reviewer/session-prompts.md
Updated files:
- CLAUDE.md: roadmap section now lists porting-status.md, phase-orchestration.md,
session-prompts.md; reviewer section adds finding-orchestration.md and
session-prompts.md; agentic-workflow section has direct "S3 is next / P1 is next"
entry points so agents don't need to derive the next action from scratch
- doc/roadmap/phases.md: Current-focus table at the top (7 tracks, next session
per track, gating); cross-links to geometry-central-comparison.md,
software-landscape.md, complexity.md added in GC and 9b-analytic sections
- doc/roadmap/porting-status.md: snapshot date updated to 2026-05-31 (post S1+S2);
test count replaced by reference to doc/api/tests.md; §4 gains four new solver
rows (newton_core refactor, NewtonStatus enum, diagnostics, selectable clamp mode);
§7 gains four new research-extension rows from S1/S2
- doc/roadmap/research-track.md: header companion-docs section links to
novelty-statement.md, software-landscape.md, and phase-orchestration.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirror the audit system's structure for the forward pipeline:
- phase-orchestration.md ← finding-orchestration.md: the roadmap DAG as a status
board (phase × type port/research × status × prerequisites × role/model × chain
× effort), the ready-set, the recommended waves (0 quick wins / A Phase 12 /
B genus-g spine), and the per-item gates (spike go/no-go, validation battery,
review).
- phase-prompts.md ← session-prompts.md: ready-to-paste blocks for the ready-set
(9g.1, 9h, 9d.3, Phase 12 two-step, 9b-analytic) plus reusable research-spike
and math-review gate prompts, and the DAG-gated Chain B / G0-blocked notes.
Cross-linked from feature-dev-agentic-system.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Forward counterpart to the audit/remediation system: a DAG-scheduled pipeline
that works the planned phases (phases.md) and research questions
(research-track.md), with a port-vs-research router, a Theorist/Spec-author
(Opus) that derives discrete formulas + designs validation, a research
spike→go/no-go gate, an item-type-specific validation strategy (golden oracle /
invariant / convergence / cross-library), DAG prerequisite gating (chains A/B),
and reuse of the audit system's Integrator/CI/review machinery. The two systems
compose into a loop (build forward, audit back). Reciprocal cross-link added to
agentic-system-design.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns the static plan in finding-orchestration.md into a running multi-agent
pipeline: roles mapped to Claude models (Mechanic=Haiku, Engineer=Sonnet,
Specialist+Reviewer+Auditor=Opus, plus Orchestrator/Integrator/Infra-
Diagnostician and human gates), a blackboard state machine (audit → plan →
(implement → review)* → integrate → merge → re-audit), automated + human gates,
and the run options (manual sessions / Claude Code subagents / Agent SDK).
Grounded in the Session 1+2 trace (incl. the cc1plus-OOM diagnosis and the
#36→#39 rebase).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the test-fast failures (confirmed via a pure-main probe PR that
failed identically): the fast job builds with -j$(nproc) inside an 800m-memory
container, and parallel Eigen+GoogleTest compilation OOM-kills cc1plus
("Killed signal terminated program cc1plus") non-deterministically — unrelated
to branch content (only constants.hpp reaches the fast target's header closure
on this branch, with trivial constexpr additions).
Serialise the fast build (-j1), mirroring the Pi-protection already applied to
the test-cgal job. Fixes test-fast on main and on this PR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Only constants.hpp reaches the fast target's header closure (via
hyper_ideal_utility/geometry); all heavy headers (newton_solver, the CGAL
entry points, the FD Hessians) are NOT compiled by conformallab_tests. The
test-fast failures showed non-deterministic timing (2m21s vs 1m49s) — the
signature of an OOM kill in the 800m-limited Pi container, not a code defect.
Local: 301/301 CGAL + 26/26 fast green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
session-prompts.md: one self-contained, copy-paste prompt per pending session,
each naming the model (Sonnet/Haiku/Opus), the findings + audit doc, the
build/test commands, and the branch/push/PR + review-gate workflow. S6 carries
the G0 precondition. Linked from finding-orchestration.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to S2: surface the new NewtonResult diagnostics through the public
CGAL result types so callers of the high-level API see them too.
- Add `CGAL::Newton_status` (alias of conformallab::NewtonStatus) and three
fields — `status`, `sparse_qr_fallback_used`, `min_ldlt_pivot` — to
Conformal_map_result, Hyper_ideal_map_result and Circle_packing_result.
(sparse_qr_fallback_used already existed on Conformal_map_result but was never
populated; it is now wired through.)
- Map them from NewtonResult at all five entry points (euclidean / spherical /
hyper_ideal / inversive_distance / cp_euclidean).
- Test: SingleTriangleConverges now asserts status == Converged and the
diagnostics propagate.
Additive only — existing `converged`/`iterations`/`gradient_norm` semantics
unchanged. 301/301 CGAL tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update finding-orchestration.md (master table I1/H1/N7 → ✅, S2 session marked
done with the original plan folded into a <details>), README headline cells +
next-batch pointer (now S3), and the test-coverage / numerical-stability audit
banners. S2 was implemented directly by Opus in the warm post-refactor session
(commit 90e966a); 301/301 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the newton_core refactor; all three findings live in exactly that code.
I1 (test-coverage): add NewtonStatus { Converged, MaxIterations,
LinearSolverFailed, LineSearchStalled } and a `status` field to NewtonResult, so
the three non-convergent exits that `converged == false` previously conflated are
now distinguishable. `converged` is kept (== status==Converged) for back-compat;
+ to_string(NewtonStatus) for logs/tests. newton_core sets the status at each
exit point (centralised by the H2 refactor).
H1 (test-coverage): set res.iterations explicitly at the LinearSolverFailed and
LineSearchStalled breaks (= completed steps), instead of relying on the last
successful iteration's stale value.
N7 (numerical-stability): surface linear-algebra conditioning in the result —
`sparse_qr_fallback_used` (any iteration fell back to SparseQR) and
`min_ldlt_pivot` (smallest |Dᵢᵢ| of the last LDLT, a cheap near-singularity
proxy). This catches the silent case the audit flagged: on a gauge-singular
Hessian SimplicialLDLT "succeeds" with a ~0 pivot and no fallback fires — now
min_ldlt_pivot is tiny and observable.
Tests (+3 synthetic newton_core status/diagnostic tests; +1 assertion on the
closed-mesh-no-pin fallback test). All purely additive — no control-flow or
convergence behaviour changes; 301/301 CGAL tests pass incl. Java parity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Session 1 (2026-05-31) resolved 26 findings across Sonnet/Haiku/Opus; 298/298
CGAL tests green. Bring the audit docs in line with reality:
- README.md: G0 banner now records that the original authors were contacted by
email about porting/relicensing rights (awaiting reply); headline-status cells
updated per finding; new "Session 1 — implemented" record; link to the plan.
- Per-audit resolution banners (api-performance, numerical-stability,
input-validation, test-coverage, math-citation, thread-safety) + G0-blocked
banners (cgal-submission, dependency-license).
- NEW finding-orchestration.md: the meta-plan mapping every finding to a session,
an implementing model (Haiku/Sonnet/Opus by the "how much must be understood"
rule), and an Opus review gate after each implementation session. Records S1
(done) and lays out S2–S6 so the next session can be picked up cold.
Docs only — no code change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The five DCE solvers (Euclidean, Spherical, HyperIdeal, CP-Euclidean,
Inversive-Distance) carried near-identical Newton loops differing only in the
gradient function, the Hessian function, and the spherical concave sign
(test-coverage audit H2 — "5 near-identical Newton loops"). Extract one
detail::newton_core<GradFn, HessFn>(x, grad, hess, concave, tol, max_iter);
each public solver is now a thin wrapper passing two lambdas. This lets the
performance fixes land once instead of five times:
- B2 (api-perf): line_search now optionally returns the gradient at the
accepted point; newton_core reuses it as the next iteration's convergence
gradient, removing ~1 redundant full-mesh gradient evaluation per iteration.
- B4 (api-perf): NewtonLinearSolver keeps one persistent SimplicialLDLT and
runs analyzePattern once (symbolic factorization cached across iterations),
re-analyzing only when nnz changes (self-healing for the FD Hessians that
prune near-zero triplets). SparseQR fallback preserved verbatim.
- B3 (api-perf): the Euclidean has_edge_dof scan is hoisted out of the loop
(it is layout-invariant) — now done once before newton_core.
- B5 (api-perf): the dead SimplicialLDLT variable in newton_euclidean is gone.
Behaviour is unchanged: concave spherical still factors −H and solves
(−H)·Δx = G; the merit steepest-descent still uses the true H; HardJava clamp
default preserved. Tests (+2): NewtonCore.ReusesLineSearchGradient_B2_Convex
asserts exactly 2 gradient evals on a unit-Hessian quadratic (vs 3 pre-B2), and
ConcavePathConverges covers the −H branch directly. 298/298 CGAL tests pass,
including all Java golden-vector parity tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hyper-ideal vertex scale b is floored to keep the geometry valid. The
original clamp `b<0 → 0.01` mirrors the Java oracle but is only C⁰ (in fact
value-discontinuous at b=0): a Newton step crossing the feasibility boundary
hits a kink that can stall convergence (numerical-stability audit N3).
Rather than replace the Java-faithful behaviour (which would break the golden
parity tests), make the floor a selectable mode so BOTH the Java standpoint
and the clean mathematics are available:
- HyperIdealScaleClamp::HardJava (DEFAULT) — the original snap, bit-for-bit
faithful to HyperIdealFunctional.java → all parity tests unchanged.
- HyperIdealScaleClamp::SmoothBarrier — C¹ softplus floor
b ↦ floor + softplus_β(b−floor), β = HYPER_IDEAL_SCALE_SHARPNESS (=100);
≈ identity away from the floor, smooth across b=0. Opt-in.
clamp_hyper_ideal_scale centralises the logic (also folds in the N4 nachzügler:
compute_face_angles used a bare 0.01). The mode threads with a defaulted
trailing parameter through compute_face_angles, face_angles_from_local_dofs,
evaluate_hyper_ideal, the four hyper_ideal_hessian* variants and
newton_hyper_ideal — so every existing call site keeps HardJava behaviour.
Tests (+4): clamp-function C¹/floor/identity contract, mode-equivalence away
from the boundary, and end-to-end SmoothBarrier convergence to the same Java
golden vector (LawsonHyperIdeal). 296/296 CGAL tests pass.
Documented in doc/math/geometry-modes.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cotangent weights divided by 2·√(t12·t23·t31·l123). For a needle/cap
(sliver) triangle one of the t-values is the difference of near-equal edge
lengths → catastrophic cancellation, and the area under the sqrt loses
precision, feeding large relative error into every cotangent weight, the
Hessian, and the linear solve (numerical-stability audit N5).
Replace the area computation with Kahan's stable side-length formula
(sort a≥b≥c, evaluate ¼·√[(a+(b+c))(c−(a−b))(c+(a−b))(a+(b−c))]). The
denominator is still exactly 8·Area for well-shaped triangles but accurate
for slivers. The triangle-inequality guard and the cotangent numerators are
unchanged.
Test: CotWeights_SliverMatchesHighPrecisionReference cross-checks a thin
triangle (apex ≈ 0.01 rad) against a long-double law-of-cosines reference.
292/292 CGAL tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Inversive-Distance solver built its Hessian inline by full finite
differences: n perturbations × a full O(F) gradient eval = O(n·F) per Newton
iteration (quadratic in mesh size), with no fast path at all (api-performance
audit B1, second half).
Port the per-face block-FD scheme already used by HyperIdeal (Phase 9b):
the gradient decomposes by face (G_v = Θ_v − Σ_{f∋v} α_v, and each face's
angles depend only on its 3 vertex DOFs), so the Hessian decomposes into
per-face 3×3 blocks. Cost drops to O(F) face evaluations, a ≈ n/6 speed-up.
- inversive_distance_functional.hpp: add the pure 3→3 kernel
inversive_distance_face_grad_contribs (returns the per-face contribution
−α to G; mirrors the gradient's face-skip on ℓ²≤0 exactly).
- inversive_distance_hessian.hpp (new): full-FD baseline + block-FD + sym
variants, mirroring hyper_ideal_hessian.hpp.
- newton_solver.hpp: drop the inline full-FD lambda; call
inversive_distance_hessian_block_fd_sym.
- test: InversiveDistance_BlockFDHessianMatchesFullFD cross-validates the
two Hessians entry-wise on a perturbed (off-equilibrium) config.
291/291 CGAL tests pass; all Inversive-Distance convergence tests unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C2: Fix coverage.sh — remove || true from lcov capture/extract steps so real
lcov failures are visible; empty coverage.info now exits with code 3.
C3: Add coverage gate to quality-gates CI job (SKIP_COVERAGE_GATE=1 ramp-up
mode until I5 is resolved — fast suite covers ~9.6% not 80%). Thresholds:
80% line / 70% branch / 90% function (agreed 2026-05-31).
V1: Wrap JSON parse + field extraction in try/catch — nlohmann parse_error and
type_error now surface as std::runtime_error with the file path.
V2: Wrap stoi/stod in XML Solver parser — missing/non-numeric attributes throw
std::runtime_error instead of leaking std::invalid_argument.
V4: Validate required JSON keys (dof_vector, solver, solver.*) before access —
missing field produces a clear named-field error message.
I2: 6 serialization negative tests (missing file, malformed JSON, missing
dof_vector, missing solver block, missing XML file, non-numeric XML attr).
I3: load_mesh throws on non-triangulated (quad) mesh — covers the
is_triangle_mesh guard that was previously untested.
I4: spherical_hessian throws on edge DOFs — covers the logic_error guard.
290/290 tests pass (+8 new).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
B1 (api-performance): wire hyper_ideal_hessian_block_fd_sym into newton_hyper_ideal
instead of the full-FD path — 33×/1166× faster on cathead/brezel, also fixes the
FD-step vs Newton-tol accuracy floor (N1 cross-fix).
V3 (input-validation): add isfinite check on all vertex coordinates in load_mesh;
NaN/Inf now throws runtime_error at the I/O boundary before poisoning the solver.
C1 (test-coverage): expose the internal ok flag via an optional bool* parameter
on solve_linear_system so double-solver failure is no longer invisible to callers.
+5 new tests (LoadMeshThrowsOnNaN/Inf, OkFlag_True*, OkFlag_NullPointerIsSafe).
282/282 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The dev branch has been removed (trunk-based workflow: push to main, use
short-lived feature branches + PRs for risky/reviewable changes). These three
workflows still listed 'dev' as a push trigger — a dead reference now that the
branch is gone. Removed so the CI config matches reality.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a comprehensive 'External audit documents' section to the reviewer index:
- groups all 12 audits (correctness, API/perf/packaging, robustness)
- surfaces G0 (porting rights) as the cross-cutting release blocker
- records the decisions taken: coverage gate 80/70/90, API naming A1-A5
(A1-A3 implemented in PR #36, A4-A5 deferred pending G0/G1)
- points a new session at the low-risk quick-win findings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Checks code/docs against the published mathematics (vs java-port-audit which
checks code vs Java):
- M1: Kolpakov-Mednykh volume formula used in code but missing from references.md
- M2: BPS circle-packing paper cited as '2010' in code vs '2015' in references.md
- M3: post-2023/preprint citations back unimplemented phases — re-verify + clarify
scope before submission
- M4: no canonical citation-key convention
- M5: large derivations numerically validated but prose unreviewed — expert sign-off
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the dimensions the prior audits did not cover:
- numerical-stability: tolerance hierarchy (FD-step vs Newton tol), discontinuous
clamps, cancellation in cotangent/area, magic constants.
- input-validation: malformed JSON/XML deserialization, NaN/Inf vertex coords on
load, schema-drift handling.
- thread-safety: no mutable static state (good); undocumented same-mesh concurrency
contract; Eigen threading.
- dependency-license: existing THIRD-PARTY-LICENSES matrix is sound but predicated
on the MIT premise that G0 undermines; test meshes have no provenance/license.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every container now has an explicit --memory limit so the OOM killer
has clean boundaries to work with instead of letting any single job
consume all available RAM:
test-fast: 800m (build Eigen+GTest, run 26 tests)
test-cgal: 2000m (LOW_MEMORY_BUILD, was already set)
quality-gates: 600m (apt install + 4 script checks)
doc-build: 800m (Doxygen HTML generation)
markdown-links: 400m (pure Python, very lightweight)
memory-swap = 1.5× memory in each case (150% total) so the OOM
killer fires cleanly without exhausting the SD card.
⚠ This is workflow-side protection only. The runner-side fix
(capacity: 1 in act-runner config) prevents parallel containers
entirely and is the more reliable protection — see CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Pi runner (3-4 GB RAM, swap heavily loaded) OOMs when /ci-all
triggers test-cgal + quality-gates + doc-build + links simultaneously:
four Docker containers start at once after test-fast completes, each
consuming 150-400 MB, exhausting available RAM.
Fix: /ci-all removed from all three workflow files. Each keyword
now triggers exactly one job — no parallel container competition.
Safe workflow: one keyword per commit.
Typical sequence:
git commit -m 'fix: ... /test-cgal' → CGAL tests (~5 min)
git commit -m 'chore: /quality-gates' → style checks (~30 s)
CLAUDE.md: CI table updated with Pi-runner warning note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-U6 from doc/reviewer/usability-audit-2026-05-31.md.
The Finding-D fix (external-audit-2026-05-30) added a clean one-call
gauge-vertex overload:
assign_euclidean_vertex_dof_indices(mesh, maps, gauge_vertex)
But all user-facing code still showed the old verbose manual loop:
auto vit = mesh.vertices().begin();
maps.v_idx[*vit++] = -1;
int idx = 0;
for (; vit != mesh.vertices().end(); ++vit) maps.v_idx[*vit] = idx++;
Replaced in three places:
README.md 'Minimal usage' code block
example_euclidean.cpp Step 3
example_layout.cpp pin_first() helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>