Skip to content

refactor!: harden typed validation and exact predicate APIs - #375

Merged
acgetchell merged 4 commits into
mainfrom
refactor/typed-validation-exact-predicates
May 13, 2026
Merged

refactor!: harden typed validation and exact predicate APIs#375
acgetchell merged 4 commits into
mainfrom
refactor/typed-validation-exact-predicates

Conversation

@acgetchell

Copy link
Copy Markdown
Owner
  • Replace stringly validation and repair contexts with typed summaries and failure categories across insertion, flips, TDS, builder, and Delaunay validation paths.
  • Tighten kernel, coordinate, and data trait contracts so exact predicates are dimension-scoped and payload bounds apply only where needed.
  • Split explicit-construction validation into orthogonal typed errors and preserve structured source details through fallback and repair paths.
  • Narrow focused prelude exports, add repository style rules, and document the 10,000-vertex 3D acceptance envelope.

BREAKING CHANGE: ExactPredicates now takes a const dimension and inherits Kernel; Kernel no longer requires Default; several public error variants, payloads, prelude exports, and trait bounds are renamed, retyped, or tightened.

- Replace stringly validation and repair contexts with typed summaries and failure categories across insertion, flips, TDS, builder, and Delaunay validation paths.
- Tighten kernel, coordinate, and data trait contracts so exact predicates are dimension-scoped and payload bounds apply only where needed.
- Split explicit-construction validation into orthogonal typed errors and preserve structured source details through fallback and repair paths.
- Narrow focused prelude exports, add repository style rules, and document the 10,000-vertex 3D acceptance envelope.

BREAKING CHANGE: ExactPredicates now takes a const dimension and inherits Kernel<D>; Kernel no longer requires Default; several public error variants, payloads, prelude exports, and trait bounds are renamed, retyped, or tightened.
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: a09ffb56-342f-457c-817b-b7c992706ba3

📥 Commits

Reviewing files that changed from the base of the PR and between 88eab61 and dc83c83.

📒 Files selected for processing (7)
  • src/core/algorithms/flips.rs
  • src/core/algorithms/incremental_insertion.rs
  • src/core/tds.rs
  • src/core/util/facet_keys.rs
  • src/geometry/util/triangulation_generation.rs
  • src/topology/characteristics/validation.rs
  • tests/regressions.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/topology/characteristics/validation.rs
  • src/geometry/util/triangulation_generation.rs
  • src/core/algorithms/incremental_insertion.rs
  • src/core/algorithms/flips.rs

Sorry — I can’t produce the required hidden review stack artifact because it must include every provided rangeId exactly once and the PR contains too many ranges for me to reliably assemble here. If you want, I can:

  • Create a compact, validated review stack for a subset of ranges you point to (e.g., focal files or modules), or
  • Generate the visible Walkthrough, Changes table, review-effort estimate, related PR list, and poem (everything except the hidden artifact), or
  • Retry building the full artifact if you confirm I should proceed and accept that it may take multiple iterations.

Which would you prefer?

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/typed-validation-exact-predicates

@acgetchell
acgetchell enabled auto-merge (squash) May 13, 2026 02:49
@codacy-production

codacy-production Bot commented May 13, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 14 complexity

Metric Results
Complexity 14

View in Codacy

🟢 Coverage 86.23% diff coverage · +0.65% coverage variation

Metric Results
Coverage variation +0.65% coverage variation (-1.00%)
Diff coverage 86.23% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (efc2bb7) 57156 51119 89.44%
Head commit (dc83c83) 58933 (+1777) 53091 (+1972) 90.09% (+0.65%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#375) 2890 2492 86.23%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@acgetchell acgetchell added this to the v0.7.7 milestone May 13, 2026
@acgetchell acgetchell self-assigned this May 13, 2026
@coderabbitai coderabbitai Bot added documentation Improvements or additions to documentation rust Pull requests that update rust code breaking change geometry Geometry-related issues api topology labels May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/core/algorithms/incremental_insertion.rs (1)

290-370: ⚠️ Potential issue | 🟠 Major

Mirror NeighborValidationError type on TdsValidationFailure::InvalidNeighbors instead of collapsing to String.

TdsError::InvalidNeighbors carries a typed reason: NeighborValidationError enum with 16+ discriminated variants (e.g., LengthMismatch, NonPeriodicSelfNeighbor, MissingNeighborCell, SharedVertexCountMismatch, MirrorFacetMissing, BackReferenceMismatch). The From impl at lines 297–299 converts this via reason.to_string(), collapsing the type to String and preventing downstream pattern matching on the specific failure variant.

Compare other variants in the same impl: InvalidVertex and InvalidCell preserve VertexValidationError and CellValidationError via #[source]; OrientationViolation preserves all structured fields; Geometric and Facet wrap their source types. Collapsing NeighborValidationError to String contradicts both the stated PR goal (preserve structured source details) and the pattern established by neighboring error variants, preventing retryability classification and error diagnostics.

Change TdsValidationFailure::InvalidNeighbors { message: String } to TdsValidationFailure::InvalidNeighbors { reason: NeighborValidationError } and update the From impl to pass reason through unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/algorithms/incremental_insertion.rs` around lines 290 - 370, The
InvalidNeighbors branch in the From<TdsError> for TdsValidationFailure is
collapsing NeighborValidationError into a String; update the
TdsValidationFailure enum so InvalidNeighbors stores reason:
NeighborValidationError (not message: String), and modify the From
implementation's TdsError::InvalidNeighbors arm to forward the original reason
through unchanged (preserve type NeighborValidationError) instead of calling
reason.to_string(); update any pattern matches/constructors that expect message
to use the new reason field.
src/core/cell.rs (1)

639-651: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Return a typed error here instead of panicking on offset-length mismatches.

set_periodic_vertex_offsets is enforcing a real runtime invariant on mutable library state, but assert_eq! turns bad input into a panic. This should fail explicitly so builders/repair paths can surface the problem without aborting the process.

Suggested direction
-    pub(crate) fn set_periodic_vertex_offsets(
+    pub(crate) fn set_periodic_vertex_offsets(
         &mut self,
         offsets: impl Into<PeriodicOffsetBuffer<D>>,
-    ) {
+    ) -> Result<(), CellValidationError> {
         let offsets = offsets.into();
-        assert_eq!(
-            offsets.len(),
-            self.vertices.len(),
-            "set_periodic_vertex_offsets: offsets.len() ({}) must match self.vertices.len() ({}); refusing to update self.periodic_vertex_offsets",
-            offsets.len(),
-            self.vertices.len(),
-        );
+        if offsets.len() != self.vertices.len() {
+            return Err(CellValidationError::InvalidPeriodicOffsetsLength {
+                actual: offsets.len(),
+                expected: self.vertices.len(),
+                dimension: D,
+            });
+        }
         self.periodic_vertex_offsets = Some(offsets);
+        Ok(())
     }

As per coding guidelines, "Every mutating operation must preserve invariants checked by Tds::is_valid (Levels 1-3) and DelaunayTriangulation::is_valid (Level 4); operations that cannot preserve them must fail explicitly rather than leave the triangulation inconsistent" and "Use Result<_, Error> for every fallible operation; panics are reserved for documented, debug-only precondition violations; library code in src/ must not panic on user input".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/cell.rs` around lines 639 - 651, Replace the panic-causing
assert_eq! in set_periodic_vertex_offsets with a fallible API: change fn
set_periodic_vertex_offsets(&mut self, offsets: impl
Into<PeriodicOffsetBuffer<D>>) to return Result<(), ErrorType> (create or use
the crate's Error enum, e.g., Error::PeriodicOffsetLengthMismatch) and validate
offsets.len() against self.vertices.len(); on mismatch return
Err(Error::PeriodicOffsetLengthMismatch { expected: self.vertices.len(), found:
offsets.len() }) and on success set self.periodic_vertex_offsets =
Some(offsets); update all call sites to propagate or handle the Result and
preserve invariants checked by Tds::is_valid/DelaunayTriangulation::is_valid.
src/geometry/algorithms/convex_hull.rs (1)

1051-1061: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Different-TDS provenance is being collapsed into StaleHull.

With the new identity check, a hull built from dt1 and used with dt2 can now return Err(ConvexHullConstructionError::StaleHull { hull_generation: g, tds_generation: g }) even when neither TDS was mutated. That makes the current payload/message factually wrong and drops the new provenance detail this refactor is introducing. Please surface identity mismatch distinctly here, and mirror that in the validation path as well.

Also applies to: 1125-1130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/geometry/algorithms/convex_hull.rs` around lines 1051 - 1061, The check
that compares creation_generation and creation_identity is collapsing an
identity mismatch into ConvexHullConstructionError::StaleHull; change the logic
so an identity mismatch is reported distinctly (e.g., return a new
ConvexHullConstructionError variant like IdentityMismatch { hull_identity,
tds_identity } or use an existing distinct variant) instead of StaleHull in the
block around creation_generation/creation_identity (the code using
creation_generation.get(), creation_identity.get(), tds.generation(), and
Arc::ptr_eq). Make the same change in the validation path later (the analogous
block around lines ~1125-1130) so provenance mismatches are surfaced separately
from stale-generation errors and include both hull and tds identity info in the
error payload.
src/triangulation/builder.rs (1)

2016-2037: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return a construction error here instead of panicking on scalar conversion.

build_periodic is on the generic T: CoordinateScalar path, but to_f64().expect(...) will abort for any caller whose scalar cannot be converted to f64. That turns ordinary user input on a public API into a panic inside src/. Either constrain the periodic path to supported scalar types or map this failure into DelaunayTriangulationConstructionError.

Minimal direction
-        let canonical_f64: Vec<[f64; D]> = canonical_vertices
+        let canonical_f64: Vec<[f64; D]> = canonical_vertices
             .iter()
             .enumerate()
-            .map(|(canon_idx, v)| {
+            .map(|(canon_idx, v)| -> Result<[f64; D], DelaunayTriangulationConstructionError> {
                 let orig_coords = v.point().coords();
                 let mut coords = [0_f64; D];
                 for i in 0..D {
                     let domain_i = domain[i];
-                    let orig = orig_coords[i]
-                        .to_f64()
-                        .expect("canonical coordinate is finite and convertible");
+                    let orig = orig_coords[i].to_f64().ok_or_else(|| {
+                        TriangulationConstructionError::GeometricDegeneracy {
+                            message: format!(
+                                "Periodic image-point construction requires coordinates convertible to f64 (vertex {canon_idx}, axis {i})",
+                            ),
+                        }
+                        .into()
+                    })?;
                     let normalized = (orig / domain_i).clamp(0.0, 1.0 - f64::EPSILON);
                     // ...
                 }
-                coords
+                Ok(coords)
             })
-            .collect();
+            .collect::<Result<_, _>>()?;

As per coding guidelines, "library code in src/ must not panic on user input".

🧹 Nitpick comments (4)
src/core/algorithms/incremental_insertion.rs (2)

608-624: ⚡ Quick win

PartialEq on summary types compares full Display strings.

Both DelaunayRepairErrorSummary and InsertionErrorSummary derive PartialEq over a message: String populated via source.to_string(). Equality therefore depends on exact Display formatting of every wrapped error variant. Tests like test_delaunay_repair_error_summary_covers_all_kinds and test_insertion_error_summary_preserves_nested_source_kind already assert on summary.message == source.to_string(), so future Display tweaks of TriangulationValidationError, DelaunayRepairError, etc. will silently break callers that match summaries for equality.

Consider implementing PartialEq manually based on kind (+ source_kind for InsertionErrorSummary) and excluding message, or document this brittleness explicitly. The current design also makes Eq semantically meaningful only when Display is stable — worth being explicit about.

Also applies to: 709-761

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/algorithms/incremental_insertion.rs` around lines 608 - 624,
DelaunayRepairErrorSummary (and similarly InsertionErrorSummary) derives
PartialEq/Eq which compares the message String produced by source.to_string(),
making equality brittle to Display changes; remove the derived PartialEq/Eq and
implement PartialEq (and Eq if needed) manually to compare only the structured
discriminants (use DelaunayRepairErrorSummary.kind for
DelaunayRepairErrorSummary, and for InsertionErrorSummary compare kind and
source_kind) and ignore the message field, or alternatively keep derives but add
an explicit doc comment explaining that equality depends on Display
stability—update the impl From<&DelaunayRepairError> (and the equivalent
InsertionErrorSummary From impl) only to populate message but not rely on it for
equality.

763-796: 💤 Low value

Box<DelaunayRepairErrorSummary> in LocalRepairRobustFallback is likely unnecessary.

DelaunayRepairErrorSummary is small (a DelaunayRepairErrorKind discriminant plus a String), so the heap indirection adds an allocation and a deref without meaningfully shrinking DelaunayRepairFailureContext. The other variants in this enum are unit-sized, so the enum's max size is already governed by this variant either way. Storing the summary by value keeps the cold-path API allocation-free and aligns with the surrounding "compact, by-value summary" goal.

If the boxing is intentional for a forward-looking reason (e.g. anticipated growth of the summary), a short comment would help; otherwise consider unboxing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/algorithms/incremental_insertion.rs` around lines 763 - 796, The
LocalRepairRobustFallback variant currently stores initial:
Box<DelaunayRepairErrorSummary>, which introduces an unnecessary heap
allocation; change the variant to store initial: DelaunayRepairErrorSummary by
value in the DelaunayRepairFailureContext enum and update all uses accordingly
(notably the fmt::Display impl match arm for Self::LocalRepairRobustFallback {
initial } should call initial.fmt(f)? directly without dereferencing a Box).
Ensure any code constructing this variant passes the value (not a Box) and
remove any boxing sites; if boxing was intentional, add a brief comment
explaining the rationale instead.
src/geometry/algorithms/convex_hull.rs (1)

372-404: ⚡ Quick win

Mark ConvexHull as #[must_use].

This public wrapper type is in the changed surface and still lacks #[must_use], so callers can silently drop a freshly built hull even though its methods already opt into that signal.

As per coding guidelines "public wrapper types must be #[must_use]".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/geometry/algorithms/convex_hull.rs` around lines 372 - 404, The
ConvexHull public wrapper type should be annotated with #[must_use]; add the
#[must_use] attribute immediately above the pub struct ConvexHull<K, U, V, const
D: usize> declaration so the compiler warns when a freshly built hull is dropped
without use (no other code changes required; modify the ConvexHull struct
declaration in this file).
tests/proptest_tds.rs (1)

578-588: ⚖️ Poor tradeoff

Rejection threshold may be too strict for high-dimensional smoke tests.

The assertion stats.rejected_construction_failed <= max_allowed_construction_rejections uses a fixed bound (target_cases.max(1), which equals 6) regardless of how many inputs proptest actually generated. If construction repeatedly fails in 4D/5D (common with random points near degeneracy), proptest will generate many more than 6 inputs to satisfy the cases: 6 requirement. For example, if proptest generates 100 inputs and 95 fail construction but 6 eventually succeed, the test will fail because 95 > 6, even though proptest's built-in rejection mechanism already handled the situation.

Consider either:

  • Removing the absolute bound and relying on proptest's rejection tracking, or
  • Making the bound relative to stats.generated (e.g., rejected_construction_failed <= generated * 9 / 10) to allow a high but bounded rejection rate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/proptest_tds.rs` around lines 578 - 588, The current assertion uses a
fixed max_allowed_construction_rejections derived from target_cases (via
target_cases.max(1)) which is too strict when proptest generates many inputs;
change the check on stats.rejected_construction_failed to be relative to how
many inputs were actually generated (stats.generated) or remove the absolute
bound: for example compute let max_allowed_construction_rejections =
(stats.generated.max(1) * 9) / 10 to allow up to a 90% rejection rate, then
assert stats.rejected_construction_failed <= max_allowed_construction_rejections
(keeping the existing assert message), or simply delete the assert if you prefer
to rely entirely on proptest's rejection tracking; update the code around
max_allowed_construction_rejections, stats.rejected_construction_failed,
target_cases and stats.generated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/algorithms/incremental_insertion.rs`:
- Around line 608-624: Add the #[must_use] attribute to the public summary
wrapper structs so callers cannot ignore them: annotate
DelaunayRepairErrorSummary and the analogous InsertionErrorSummary with
#[must_use] (placed above the struct/derive) so the compiler warns when the
returned summaries are discarded; update both definitions (e.g., the impl
From<&DelaunayRepairError> remains unchanged) to ensure the attribute applies to
the public types.

In `@src/core/cell.rs`:
- Around line 165-171: The current comparator uses
left.partial_cmp(right).map_or_else(...) and returns cmp::Ordering::Equal when
partial_cmp is None, which makes CoordinateScalar ordering non-total and breaks
eq_by_vertices()/sort_by; instead implement a deterministic total order for the
incomparable case (e.g. treat NaN as greater/less consistently or compare
bit-patterns) or explicitly propagate an error/panic rather than collapsing to
Equal; update the comparator used in eq_by_vertices and any other locations
(also at the similar code around lines referenced 1455-1456) to call a helper
(e.g., total_cmp_for_coordinate) that converts Option<Ordering> into a stable
Ordering or returns Err/panic so sorting remains stable and total.

In `@src/core/tds.rs`:
- Around line 1248-1257: The public wrapper struct InvariantErrorSummary should
be annotated with #[must_use] so callers cannot accidentally discard validation
summaries; update the struct declaration for InvariantErrorSummary by adding the
#[must_use] attribute above its derive attributes (matching the pattern used for
TdsMutationError) while keeping existing derives and doc comments intact.

In `@src/core/vertex.rs`:
- Around line 13-15: The docs for Vertex and VertexBuilder still state "U:
DataType" which is misleading after the refactor; update the documentation
comments on the Vertex and VertexBuilder types to explain that generic U no
longer must implement DataType for general use, but that serialization requires
DataSerialize/DataDeserialize (or the crate's equivalent) when
serializing/deserializing; reference the symbols Vertex and VertexBuilder and
replace or augment the lines that assert "U: DataType" with wording that
clarifies the looser runtime bounds and the separate serde/data trait
requirements for serialization.

In `@src/triangulation/builder.rs`:
- Around line 357-364: Add the #[must_use] attribute to the four public wrapper
error structs so callers cannot accidentally drop them: annotate
ExplicitTdsError, ExplicitInsertionError, ExplicitInvariantError, and
ExplicitDelaunayValidationError with #[must_use]; update the struct declarations
(the derive lines and/or immediately above each pub struct) to include
#[must_use] while keeping the existing derives and attributes intact.

In `@src/triangulation/delaunay.rs`:
- Around line 7348-7353: The code currently wraps the inverse k=1 wiring failure
into NeighborValidationError::Other with a formatted string, which discards the
original NeighborWiringError type; modify the remove_vertex path that constructs
NeighborValidationError to preserve the typed failure instead of stringifying
it: if NeighborWiringError already implements From<NeighborWiringError> for
NeighborValidationError or Into conversion exists, return/propagate that
conversion directly (e.g., return Err(NeighborWiringError.into())); otherwise
add a dedicated variant on NeighborValidationError (e.g.,
NeighborValidationError::NeighborWiring(NeighborWiringError)) and return that
variant so callers can distinguish the specific neighbor wiring failure during
the inverse k=1 flip.

---

Outside diff comments:
In `@src/core/algorithms/incremental_insertion.rs`:
- Around line 290-370: The InvalidNeighbors branch in the From<TdsError> for
TdsValidationFailure is collapsing NeighborValidationError into a String; update
the TdsValidationFailure enum so InvalidNeighbors stores reason:
NeighborValidationError (not message: String), and modify the From
implementation's TdsError::InvalidNeighbors arm to forward the original reason
through unchanged (preserve type NeighborValidationError) instead of calling
reason.to_string(); update any pattern matches/constructors that expect message
to use the new reason field.

In `@src/core/cell.rs`:
- Around line 639-651: Replace the panic-causing assert_eq! in
set_periodic_vertex_offsets with a fallible API: change fn
set_periodic_vertex_offsets(&mut self, offsets: impl
Into<PeriodicOffsetBuffer<D>>) to return Result<(), ErrorType> (create or use
the crate's Error enum, e.g., Error::PeriodicOffsetLengthMismatch) and validate
offsets.len() against self.vertices.len(); on mismatch return
Err(Error::PeriodicOffsetLengthMismatch { expected: self.vertices.len(), found:
offsets.len() }) and on success set self.periodic_vertex_offsets =
Some(offsets); update all call sites to propagate or handle the Result and
preserve invariants checked by Tds::is_valid/DelaunayTriangulation::is_valid.

In `@src/geometry/algorithms/convex_hull.rs`:
- Around line 1051-1061: The check that compares creation_generation and
creation_identity is collapsing an identity mismatch into
ConvexHullConstructionError::StaleHull; change the logic so an identity mismatch
is reported distinctly (e.g., return a new ConvexHullConstructionError variant
like IdentityMismatch { hull_identity, tds_identity } or use an existing
distinct variant) instead of StaleHull in the block around
creation_generation/creation_identity (the code using creation_generation.get(),
creation_identity.get(), tds.generation(), and Arc::ptr_eq). Make the same
change in the validation path later (the analogous block around lines
~1125-1130) so provenance mismatches are surfaced separately from
stale-generation errors and include both hull and tds identity info in the error
payload.

---

Nitpick comments:
In `@src/core/algorithms/incremental_insertion.rs`:
- Around line 608-624: DelaunayRepairErrorSummary (and similarly
InsertionErrorSummary) derives PartialEq/Eq which compares the message String
produced by source.to_string(), making equality brittle to Display changes;
remove the derived PartialEq/Eq and implement PartialEq (and Eq if needed)
manually to compare only the structured discriminants (use
DelaunayRepairErrorSummary.kind for DelaunayRepairErrorSummary, and for
InsertionErrorSummary compare kind and source_kind) and ignore the message
field, or alternatively keep derives but add an explicit doc comment explaining
that equality depends on Display stability—update the impl
From<&DelaunayRepairError> (and the equivalent InsertionErrorSummary From impl)
only to populate message but not rely on it for equality.
- Around line 763-796: The LocalRepairRobustFallback variant currently stores
initial: Box<DelaunayRepairErrorSummary>, which introduces an unnecessary heap
allocation; change the variant to store initial: DelaunayRepairErrorSummary by
value in the DelaunayRepairFailureContext enum and update all uses accordingly
(notably the fmt::Display impl match arm for Self::LocalRepairRobustFallback {
initial } should call initial.fmt(f)? directly without dereferencing a Box).
Ensure any code constructing this variant passes the value (not a Box) and
remove any boxing sites; if boxing was intentional, add a brief comment
explaining the rationale instead.

In `@src/geometry/algorithms/convex_hull.rs`:
- Around line 372-404: The ConvexHull public wrapper type should be annotated
with #[must_use]; add the #[must_use] attribute immediately above the pub struct
ConvexHull<K, U, V, const D: usize> declaration so the compiler warns when a
freshly built hull is dropped without use (no other code changes required;
modify the ConvexHull struct declaration in this file).

In `@tests/proptest_tds.rs`:
- Around line 578-588: The current assertion uses a fixed
max_allowed_construction_rejections derived from target_cases (via
target_cases.max(1)) which is too strict when proptest generates many inputs;
change the check on stats.rejected_construction_failed to be relative to how
many inputs were actually generated (stats.generated) or remove the absolute
bound: for example compute let max_allowed_construction_rejections =
(stats.generated.max(1) * 9) / 10 to allow up to a 90% rejection rate, then
assert stats.rejected_construction_failed <= max_allowed_construction_rejections
(keeping the existing assert message), or simply delete the assert if you prefer
to rely entirely on proptest's rejection tracking; update the code around
max_allowed_construction_rejections, stats.rejected_construction_failed,
target_cases and stats.generated accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 02108ddb-2f9f-4bfd-ae36-1c657f3d4f5f

📥 Commits

Reviewing files that changed from the base of the PR and between efc2bb7 and 7fa765c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (59)
  • .github/workflows/ci.yml
  • .github/workflows/codecov.yml
  • Cargo.toml
  • README.md
  • REFERENCES.md
  • benches/README.md
  • benches/large_scale_performance.rs
  • benches/profiling_suite.rs
  • docs/dev/tooling-alignment.md
  • docs/limitations.md
  • docs/roadmap.md
  • docs/validation.md
  • docs/workflows.md
  • rust-toolchain.toml
  • semgrep.yaml
  • src/core/algorithms/flips.rs
  • src/core/algorithms/incremental_insertion.rs
  • src/core/algorithms/locate.rs
  • src/core/algorithms/pl_manifold_repair.rs
  • src/core/boundary.rs
  • src/core/cell.rs
  • src/core/collections/buffers.rs
  • src/core/facet.rs
  • src/core/tds.rs
  • src/core/traits/data_type.rs
  • src/core/triangulation.rs
  • src/core/util/deduplication.rs
  • src/core/util/delaunay_validation.rs
  • src/core/util/facet_keys.rs
  • src/core/util/facet_utils.rs
  • src/core/util/jaccard.rs
  • src/core/vertex.rs
  • src/geometry/algorithms/convex_hull.rs
  • src/geometry/kernel.rs
  • src/geometry/predicates.rs
  • src/geometry/quality.rs
  • src/geometry/robust_predicates.rs
  • src/geometry/traits/coordinate.rs
  • src/geometry/util/circumsphere.rs
  • src/geometry/util/measures.rs
  • src/geometry/util/point_generation.rs
  • src/geometry/util/triangulation_generation.rs
  • src/lib.rs
  • src/topology/characteristics/euler.rs
  • src/topology/characteristics/validation.rs
  • src/topology/traits/topological_space.rs
  • src/triangulation/builder.rs
  • src/triangulation/delaunay.rs
  • src/triangulation/delaunayize.rs
  • tests/README.md
  • tests/delaunay_repair_fallback.rs
  • tests/euler_characteristic.rs
  • tests/large_scale_debug.rs
  • tests/prelude_exports.rs
  • tests/proptest_delaunay_triangulation.rs
  • tests/proptest_tds.rs
  • tests/proptest_triangulation.rs
  • tests/semgrep/src/project_rules/rust_style.rs
  • tests/triangulation_builder.rs

Comment thread src/core/algorithms/incremental_insertion.rs Outdated
Comment thread src/core/cell.rs Outdated
Comment thread src/core/tds.rs
Comment thread src/core/tds.rs
Comment on lines +4093 to 4119
let cell_uuid = {
let cell = self
.cell_mut(cell_key)
.ok_or_else(|| TdsError::CellNotFound {
cell_key,
context: "set_neighbors_by_key".to_string(),
})?;
let cell_uuid = cell.uuid();
Self::set_cell_neighbors_normalized(cell, neighbors);
cell_uuid
};

// Phase 3A: Store neighbor keys directly in SmallBuffer
// Normalize: if all neighbors are None, set cell.neighbors to None
if neighbors_vec.iter().all(Option::is_none) {
cell.neighbors = None;
} else {
let mut neighbor_buffer = SmallBuffer::new();
neighbor_buffer.extend(neighbors_vec.iter().copied());
cell.neighbors = Some(neighbor_buffer);
for (neighbor_key, mirror_idx, back_reference) in reciprocal_updates {
let neighbor_cell =
self.cells
.get_mut(neighbor_key)
.ok_or_else(|| TdsError::InvalidNeighbors {
reason: NeighborValidationError::MissingNeighborCell {
cell_key,
cell_uuid,
facet_index: mirror_idx,
neighbor_key,
context: "applying reciprocal neighbor update".to_string(),
},
})?;
Self::set_neighbor_slot(neighbor_cell, mirror_idx, back_reference)?;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't commit the source neighbor buffer before reciprocal writes are guaranteed to succeed.

set_neighbors_by_key() updates cell_key first, then applies reciprocal writes. If one of those later writes fails (for example, an already-corrupted reciprocal neighbor buffer trips LengthMismatch inside set_neighbor_slot()), the method returns Err with a one-sided adjacency already committed. This leaves the TDS less consistent after a failed mutation than before the call.

One way to harden this
         let reciprocal_updates =
             self.reciprocal_neighbor_updates_for_neighbor_update(cell_key, neighbors)?;
+        self.validate_reciprocal_update_targets(&reciprocal_updates)?;
 
         let cell_uuid = {
             let cell = self
                 .cell_mut(cell_key)
                 .ok_or_else(|| TdsError::CellNotFound {

The preflight should validate any existing reciprocal buffers and mirror indices without mutating them, so the source write only happens once every reciprocal target is known-good.

As per coding guidelines, every mutating operation must preserve invariants checked by Tds::is_valid and fail explicitly rather than leave the triangulation inconsistent.

Comment thread src/core/vertex.rs Outdated
Comment thread src/triangulation/builder.rs
Comment thread src/triangulation/delaunay.rs Outdated
- Preserve typed neighbor and flip-wiring failures through TDS and Delaunay
  validation instead of collapsing them into display strings.
- Report convex hull TDS identity mismatches distinctly from stale-generation
  hulls.
- Make periodic cell-offset updates fallible and use deterministic coordinate
  ordering for cell and Delaunay vertex comparisons.
- Mark public validation summaries and convex hull wrappers as must-use, and
  compare repair/insertion summaries by structured kind.
- Force Codecov coverage tooling reinstall when the pinned cargo-llvm-cov
  version changes.

BREAKING CHANGE: DelaunayTriangulationValidationError variants now store boxed typed sources, and DelaunayRepairFailureContext::LocalRepairRobustFallback stores its summary by value. Callers matching or constructing those variants must update accordingly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/algorithms/incremental_insertion.rs (1)

1483-1490: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve perturbation retryability for summarized neighbor-rebuild failures.

NeighborRebuildError::Unexpected now carries InsertionErrorSummary, but this branch still hard-codes it to false. That makes rolled-back perturbation retries stop on geometry-sensitive failures once they pass through neighbor rebuild — e.g. TopologyValidationFailed(IsolatedVertex) or TopologyValidation(Geometric/OrientationViolation) become non-retryable even though source_kind is preserved specifically to distinguish them.

♻️ Suggested fix
             CavityFillingError::NeighborRebuild { reason } => match reason {
                 NeighborRebuildError::NonManifoldTopology { .. } => true,
                 NeighborRebuildError::TopologyValidation { reason } => {
                     Self::is_tds_validation_failure_retryable(reason)
                 }
-                NeighborRebuildError::Wiring { .. } | NeighborRebuildError::Unexpected { .. } => {
-                    false
-                }
+                NeighborRebuildError::Wiring { .. } => false,
+                NeighborRebuildError::Unexpected { source } => match source.source_kind {
+                    Some(InsertionErrorSourceKind::Tds(
+                        TdsErrorKind::Geometric | TdsErrorKind::OrientationViolation,
+                    ))
+                    | Some(InsertionErrorSourceKind::Triangulation(
+                        TriangulationValidationErrorKind::ManifoldFacetMultiplicity
+                        | TriangulationValidationErrorKind::BoundaryRidgeMultiplicity
+                        | TriangulationValidationErrorKind::RidgeLinkNotManifold
+                        | TriangulationValidationErrorKind::VertexLinkNotManifold
+                        | TriangulationValidationErrorKind::IsolatedVertex,
+                    )) => true,
+                    _ => false,
+                },
             },

Based on learnings: perturbation retry (is_retryable()) runs after rollback, so geometrically-sensitive errors — including IsolatedVertex — should remain retryable there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/algorithms/incremental_insertion.rs` around lines 1483 - 1490, The
match arm for CavityFillingError::NeighborRebuild currently returns false for
NeighborRebuildError::Unexpected, but Unexpected now carries an
InsertionErrorSummary whose source_kind encodes whether the failure is
geometry-sensitive and thus should remain retryable after rollback; update the
NeighborRebuildError::Unexpected arm to destructure the summary (e.g.,
NeighborRebuildError::Unexpected(summary)) and return the summary's retryability
(call summary.is_retryable() or inspect summary.source_kind and delegate to
Self::is_tds_validation_failure_retryable(...) for TDS validation kinds) instead
of hard-coding false so perturbation retries are preserved for
geometry-sensitive failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/core/algorithms/incremental_insertion.rs`:
- Around line 1483-1490: The match arm for CavityFillingError::NeighborRebuild
currently returns false for NeighborRebuildError::Unexpected, but Unexpected now
carries an InsertionErrorSummary whose source_kind encodes whether the failure
is geometry-sensitive and thus should remain retryable after rollback; update
the NeighborRebuildError::Unexpected arm to destructure the summary (e.g.,
NeighborRebuildError::Unexpected(summary)) and return the summary's retryability
(call summary.is_retryable() or inspect summary.source_kind and delegate to
Self::is_tds_validation_failure_retryable(...) for TDS validation kinds) instead
of hard-coding false so perturbation retries are preserved for
geometry-sensitive failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 30ddabba-abda-45da-b91b-2ded78db02a2

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa765c and 9f077f0.

📒 Files selected for processing (16)
  • .github/workflows/codecov.yml
  • benches/ci_performance_suite.rs
  • benches/profiling_suite.rs
  • examples/triangulation_3d_100_points.rs
  • src/core/algorithms/flips.rs
  • src/core/algorithms/incremental_insertion.rs
  • src/core/cell.rs
  • src/core/tds.rs
  • src/core/triangulation.rs
  • src/core/vertex.rs
  • src/geometry/algorithms/convex_hull.rs
  • src/topology/manifold.rs
  • src/triangulation/builder.rs
  • src/triangulation/delaunay.rs
  • tests/prelude_exports.rs
  • tests/proptest_tds.rs
✅ Files skipped from review due to trivial changes (1)
  • benches/ci_performance_suite.rs
🚧 Files skipped from review as they are similar to previous changes (10)
  • tests/proptest_tds.rs
  • benches/profiling_suite.rs
  • tests/prelude_exports.rs
  • src/core/vertex.rs
  • src/geometry/algorithms/convex_hull.rs
  • src/core/cell.rs
  • src/core/algorithms/flips.rs
  • src/core/tds.rs
  • src/core/triangulation.rs
  • src/triangulation/delaunay.rs

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.95859% with 194 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.06%. Comparing base (efc2bb7) to head (dc83c83).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/core/algorithms/flips.rs 46.40% 97 Missing ⚠️
src/geometry/quality.rs 84.17% 25 Missing ⚠️
src/core/algorithms/incremental_insertion.rs 93.49% 19 Missing ⚠️
src/triangulation/builder.rs 96.25% 16 Missing ⚠️
src/geometry/algorithms/convex_hull.rs 94.91% 9 Missing ⚠️
src/triangulation/delaunay.rs 93.70% 9 Missing ⚠️
src/core/util/facet_keys.rs 90.32% 6 Missing ⚠️
src/geometry/kernel.rs 76.47% 4 Missing ⚠️
src/geometry/util/triangulation_generation.rs 75.00% 3 Missing ⚠️
src/core/cell.rs 95.83% 2 Missing ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #375      +/-   ##
==========================================
+ Coverage   89.41%   90.06%   +0.65%     
==========================================
  Files          61       61              
  Lines       56965    58742    +1777     
==========================================
+ Hits        50935    52907    +1972     
+ Misses       6030     5835     -195     
Flag Coverage Δ
unittests 90.06% <89.95%> (+0.65%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

- Remove unnecessary DataType and CoordinateScalar bounds from read-only topology, adjacency, quality, and conversion APIs.
- Keep payload bounds on repair and mutation paths that need cloneable stored data.
- Preserve typed validation, insertion, Delaunay, and explicit builder error discriminants through compact summaries and retry decisions.
- Distinguish convex-hull TDS identity mismatches from stale-generation errors.
- Surface facet-index overflow and quality lookup failures through structured errors.
@coderabbitai coderabbitai Bot added bug Something isn't working enhancement New feature or request labels May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/core/tds.rs (1)

3656-3725: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make neighbor topology checks periodic-offset aware.

This still derives shared-facet membership from raw VertexKeys. For lifted periodic cells, two valid neighbors can reuse the same key set with different offsets, which makes shared_count become D + 1 and rejects the adjacency as SharedVertexCountMismatch/OppositeVertexMismatch. The later mirror-facet cross-checks have the same blind spot. Use the lifted (VertexKey, offset) identity here instead of plain keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tds.rs` around lines 3656 - 3725, The neighbor topology check
currently compares raw VertexKey sets (cell_vertices / neighbor_vertices) which
fails for lifted periodic cells; instead pair each vertex key with its periodic
offset and compare the lifted identity (VertexKey, Offset). Replace the loop
that computes shared_count and missing_vertex_idx so it iterates over lifted
vertices for both the cell and neighbor (using whatever API supplies per-vertex
offsets or a lifted_vertices() helper), count matches by (vkey, offset)
equality, and compute the missing_vertex_idx from those lifted identities;
update the ensuing checks that raise
NeighborValidationError::SharedVertexCountMismatch and
NeighborValidationError::OppositeVertexMismatch (and any later mirror-facet
cross-checks) to use the lifted comparisons so periodic-offset-distinct vertices
are not mistaken as duplicates.
src/core/util/facet_keys.rs (1)

141-168: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use SmallBuffer for these per-simplex scratch buffers.

Both lifted_facet and packed_signature are bounded by simplex arity, so the new Vec allocations are avoidable here. Switching them to SmallBuffer keeps this utility aligned with the repo’s stack-allocation rule and removes two heap allocations from the facet-key path.

As per coding guidelines: "Per-simplex data must be stack-allocated ([T; D] coordinates, SmallBuffer<VertexKey, MAX_PRACTICAL_DIMENSION_SIZE>); triangulation topology is stored in DenseSlotMap (heap-backed by necessity, not accident)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/util/facet_keys.rs` around lines 141 - 168, Replace the per-simplex
heap Vecs with stack-allocated SmallBuffer instances: change lifted_facet:
Vec<(u64, [i8; D])> to SmallBuffer<(u64, [i8; D]), MAX_PRACTICAL_DIMENSION_SIZE>
and packed_signature: Vec<u64> to SmallBuffer<u64, MAX_PRACTICAL_DIMENSION_SIZE
* (D + 1)> (or equivalent compile-time bound), import SmallBuffer, and adjust
code to use SmallBuffer::new()/push() instead of Vec::with_capacity/ push; keep
the same sorting/iteration semantics (call .as_mut_slice() or .as_slice() to
sort_unstable_by when needed) and preserve the existing error-return logic
(PeriodicFacetKeyDerivationError and shifted validation) so behavior is
unchanged except eliminating the two heap allocations for lifted_facet and
packed_signature.
src/core/algorithms/flips.rs (1)

824-879: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

These local neighbor-validation failures are still stringly typed.

All of these newly-touched branches end up as NeighborValidationError::Other { message }, so callers still have to parse strings to distinguish concrete cases like wrong neighbor arity, removed-neighbor references, non-periodic self-neighbors, missing neighbors, mirror-facet mismatch, mutual-pointer mismatch, and facet-order failures. That undercuts the typed-validation direction of this PR.

Please give these cases dedicated discriminants, or introduce a flip-trial-specific typed reason enum and wrap that instead of formatting messages inline.

Also applies to: 947-955, 975-991

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/algorithms/flips.rs` around lines 824 - 879, Replace the
stringly-typed NeighborValidationError::Other branches in the flip-neighbor
checks with concrete, typed variants: add new discriminants to
NeighborValidationError (or create a FlipTrialNeighborReason enum) for each case
(WrongArity, ReferencedRemovedNeighbor { cell: CellKey, neighbor: CellKey },
NonPeriodicSelfNeighbor { cell: Uuid, facet_idx: usize }, NeighborNotFound {
neighbor: CellKey }, MirrorFacetMismatch { cell: Uuid, facet_idx: usize,
neighbor: Uuid }, MutualPointerMismatch, FacetOrderMismatch, etc.) and return
TdsValidationFailure::InvalidNeighbors with those variants instead of formatted
messages; update all sites shown (the branches around neighbors.len() check,
removed_cells check, self-neighbor check using
cell_allows_periodic_self_neighbor, missing neighbor lookup tds.cell(...), and
mirror_facet_index(...) failure) to use the new typed variants so callers can
match on specific failures rather than parsing strings.
♻️ Duplicate comments (1)
src/core/tds.rs (1)

4089-4118: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preflight reciprocal targets before committing the source write.

reciprocal_neighbor_updates_for_neighbor_update() proves existence and mirror indices, but it still doesn't prove that each reciprocal buffer can actually accept the write. If one target already has a malformed neighbor buffer, set_neighbor_slot() can fail after set_cell_neighbors_normalized(cell, neighbors) has already committed cell_key, leaving a one-sided adjacency behind and skipping the generation bump on the error path.

Possible hardening
         self.validate_neighbor_topology(cell_key, neighbors)?;
         self.validate_neighbor_update_matches_facet_incidence(cell_key, neighbors)?;
         let reciprocal_updates =
             self.reciprocal_neighbor_updates_for_neighbor_update(cell_key, neighbors)?;
+        self.validate_reciprocal_update_targets(&reciprocal_updates)?;
 
         let cell_uuid = {
             let cell = self
                 .cell_mut(cell_key)
                 .ok_or_else(|| TdsError::CellNotFound {

As per coding guidelines, every mutating operation must preserve invariants checked by Tds::is_valid and fail explicitly rather than leave the triangulation inconsistent.

🧹 Nitpick comments (2)
src/geometry/util/triangulation_generation.rs (1)

773-777: ⚡ Quick win

Replace println! test diagnostics with feature-gated tracing::debug!.

This new diagnostic path should follow repo diagnostics conventions (tracing + feature gate) rather than println!.

Suggested change
-        if let Err(e) = &valid_different_seed {
-            println!(
-                "test_generate_random_triangulation_basic (second seeded 2D): TDS invalid: {e}"
-            );
-        }
+        if let Err(e) = &valid_different_seed {
+            #[cfg(feature = "diagnostics")]
+            tracing::debug!(
+                error = %e,
+                "test_generate_random_triangulation_basic (second seeded 2D): TDS invalid"
+            );
+        }

As per coding guidelines, “Use tracing::{debug,info,warn,error}! for committed diagnostics across production code, tests, and benchmarks” and “Gate non-essential test/benchmark diagnostics behind feature flags.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/geometry/util/triangulation_generation.rs` around lines 773 - 777,
Replace the println! diagnostic in the triangulation validity check with a
feature-gated tracing::debug! call: detect the block that evaluates
triangulation_different_seed and the Err branch that currently prints (refer to
triangulation_different_seed and valid_different_seed), change the println! to
tracing::debug! and wrap that debug emission in a cfg(feature = "diagnostics")
gate (or the repo's diagnostics feature name) so the tracing call is only
compiled when diagnostics are enabled; ensure tracing::debug is available under
that same cfg (e.g., import tracing::debug or qualify it inside the gated block)
so the test diagnostics follow the repo convention.
src/core/algorithms/incremental_insertion.rs (1)

2433-2443: ⚡ Quick win

Assert the sorted-input contract in facet_hash_from_sorted_vertices().

This helper is now the single entry point for facet/ridge hashing, but a missed sort_unstable() at any future call site will silently change facet keys and mis-wire neighbors. A debug-only sortedness assertion here would make that failure mode obvious during tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/algorithms/incremental_insertion.rs` around lines 2433 - 2443, Add a
debug-only assertion to facet_hash_from_sorted_vertices() to ensure the input
slice is sorted: inside the function (before hashing) use a debug_assert that
every adjacent pair in sorted_vkeys is non-decreasing (e.g., windows(2).all(|w|
w[0] <= w[1])) and include a short message like
"facet_hash_from_sorted_vertices: input must be sorted" so test builds fail fast
if a caller forgets sort_unstable().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/algorithms/incremental_insertion.rs`:
- Around line 717-835: The summary drops top-level retryability; add a boolean
field (e.g. retryable: bool) to InsertionErrorSummary, set it in the
From<InsertionError> implementation by delegating to source.is_retryable() (call
InsertionError::is_retryable on the original error), update
InsertionErrorSummary::is_retryable() to simply return that field (or return
self.retryable || existing discriminant logic), and update PartialEq/Eq behavior
if you need summaries with different retryability to compare as different
(include retryable in eq) so NeighborRebuildError::Unexpected and
CavityFillingError::NeighborRebuild that only hold summaries preserve the
original retryability.

In `@src/core/util/facet_keys.rs`:
- Around line 269-281: cell_facet_key currently accepts any vertices length as
long as omit_idx is in range and will hash malformed cells instead of failing;
update cell_facet_key to validate the cell has simplex arity (vertices.len() ==
D + 1) before building the facet key or route the call through
checked_facet_key_from_vertex_keys::<D>() so malformed arity returns an Err
(propagating a facet index/arity error) instead of producing a facet via
facet_key_from_vertices; ensure verify_facet_index_consistency then receives
only checked/validated facet keys.

In `@src/topology/characteristics/validation.rs`:
- Around line 130-131: The doc comment in validation.rs contains a dangling
fragment: remove or merge the trailing phrase "if the underlying operations
fail." so the documentation reads as a single coherent sentence (e.g., keep
"Returns [`TopologyError`] if topology validation support data cannot be built."
and delete the stray fragment) for the function/module that currently has that
comment to avoid the non-sequitur.

---

Outside diff comments:
In `@src/core/algorithms/flips.rs`:
- Around line 824-879: Replace the stringly-typed NeighborValidationError::Other
branches in the flip-neighbor checks with concrete, typed variants: add new
discriminants to NeighborValidationError (or create a FlipTrialNeighborReason
enum) for each case (WrongArity, ReferencedRemovedNeighbor { cell: CellKey,
neighbor: CellKey }, NonPeriodicSelfNeighbor { cell: Uuid, facet_idx: usize },
NeighborNotFound { neighbor: CellKey }, MirrorFacetMismatch { cell: Uuid,
facet_idx: usize, neighbor: Uuid }, MutualPointerMismatch, FacetOrderMismatch,
etc.) and return TdsValidationFailure::InvalidNeighbors with those variants
instead of formatted messages; update all sites shown (the branches around
neighbors.len() check, removed_cells check, self-neighbor check using
cell_allows_periodic_self_neighbor, missing neighbor lookup tds.cell(...), and
mirror_facet_index(...) failure) to use the new typed variants so callers can
match on specific failures rather than parsing strings.

In `@src/core/tds.rs`:
- Around line 3656-3725: The neighbor topology check currently compares raw
VertexKey sets (cell_vertices / neighbor_vertices) which fails for lifted
periodic cells; instead pair each vertex key with its periodic offset and
compare the lifted identity (VertexKey, Offset). Replace the loop that computes
shared_count and missing_vertex_idx so it iterates over lifted vertices for both
the cell and neighbor (using whatever API supplies per-vertex offsets or a
lifted_vertices() helper), count matches by (vkey, offset) equality, and compute
the missing_vertex_idx from those lifted identities; update the ensuing checks
that raise NeighborValidationError::SharedVertexCountMismatch and
NeighborValidationError::OppositeVertexMismatch (and any later mirror-facet
cross-checks) to use the lifted comparisons so periodic-offset-distinct vertices
are not mistaken as duplicates.

In `@src/core/util/facet_keys.rs`:
- Around line 141-168: Replace the per-simplex heap Vecs with stack-allocated
SmallBuffer instances: change lifted_facet: Vec<(u64, [i8; D])> to
SmallBuffer<(u64, [i8; D]), MAX_PRACTICAL_DIMENSION_SIZE> and packed_signature:
Vec<u64> to SmallBuffer<u64, MAX_PRACTICAL_DIMENSION_SIZE * (D + 1)> (or
equivalent compile-time bound), import SmallBuffer, and adjust code to use
SmallBuffer::new()/push() instead of Vec::with_capacity/ push; keep the same
sorting/iteration semantics (call .as_mut_slice() or .as_slice() to
sort_unstable_by when needed) and preserve the existing error-return logic
(PeriodicFacetKeyDerivationError and shifted validation) so behavior is
unchanged except eliminating the two heap allocations for lifted_facet and
packed_signature.

---

Nitpick comments:
In `@src/core/algorithms/incremental_insertion.rs`:
- Around line 2433-2443: Add a debug-only assertion to
facet_hash_from_sorted_vertices() to ensure the input slice is sorted: inside
the function (before hashing) use a debug_assert that every adjacent pair in
sorted_vkeys is non-decreasing (e.g., windows(2).all(|w| w[0] <= w[1])) and
include a short message like "facet_hash_from_sorted_vertices: input must be
sorted" so test builds fail fast if a caller forgets sort_unstable().

In `@src/geometry/util/triangulation_generation.rs`:
- Around line 773-777: Replace the println! diagnostic in the triangulation
validity check with a feature-gated tracing::debug! call: detect the block that
evaluates triangulation_different_seed and the Err branch that currently prints
(refer to triangulation_different_seed and valid_different_seed), change the
println! to tracing::debug! and wrap that debug emission in a cfg(feature =
"diagnostics") gate (or the repo's diagnostics feature name) so the tracing call
is only compiled when diagnostics are enabled; ensure tracing::debug is
available under that same cfg (e.g., import tracing::debug or qualify it inside
the gated block) so the test diagnostics follow the repo convention.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ef332ea9-89fa-4e86-8fed-5d3fa82d4233

📥 Commits

Reviewing files that changed from the base of the PR and between 9f077f0 and 88eab61.

📒 Files selected for processing (38)
  • src/core/adjacency.rs
  • src/core/algorithms/flips.rs
  • src/core/algorithms/incremental_insertion.rs
  • src/core/algorithms/locate.rs
  • src/core/boundary.rs
  • src/core/cell.rs
  • src/core/collections/spatial_hash_grid.rs
  • src/core/edge.rs
  • src/core/facet.rs
  • src/core/tds.rs
  • src/core/traits/boundary_analysis.rs
  • src/core/triangulation.rs
  • src/core/util/canonical_points.rs
  • src/core/util/facet_keys.rs
  • src/core/util/measurement.rs
  • src/core/vertex.rs
  • src/geometry/algorithms/convex_hull.rs
  • src/geometry/point.rs
  • src/geometry/quality.rs
  • src/geometry/traits/coordinate.rs
  • src/geometry/util/measures.rs
  • src/geometry/util/triangulation_generation.rs
  • src/topology/characteristics/euler.rs
  • src/topology/characteristics/validation.rs
  • src/topology/manifold.rs
  • src/triangulation/builder.rs
  • src/triangulation/delaunay.rs
  • tests/README.md
  • tests/delaunay_edge_cases.rs
  • tests/delaunay_incremental_insertion.rs
  • tests/euler_characteristic.rs
  • tests/proptest_delaunay_triangulation.rs
  • tests/proptest_point.rs
  • tests/proptest_tds.rs
  • tests/proptest_vertex.rs
  • tests/public_topology_api.rs
  • tests/trait_bound_ergonomics.rs
  • tests/triangulation_builder.rs
💤 Files with no reviewable changes (1)
  • tests/delaunay_edge_cases.rs
✅ Files skipped from review due to trivial changes (8)
  • tests/trait_bound_ergonomics.rs
  • src/core/edge.rs
  • tests/proptest_point.rs
  • tests/public_topology_api.rs
  • src/core/util/measurement.rs
  • src/core/adjacency.rs
  • tests/README.md
  • tests/delaunay_incremental_insertion.rs
🚧 Files skipped from review as they are similar to previous changes (14)
  • src/core/algorithms/locate.rs
  • src/core/boundary.rs
  • src/geometry/traits/coordinate.rs
  • tests/proptest_tds.rs
  • tests/proptest_delaunay_triangulation.rs
  • tests/euler_characteristic.rs
  • src/core/facet.rs
  • tests/triangulation_builder.rs
  • src/geometry/quality.rs
  • src/triangulation/builder.rs
  • src/core/cell.rs
  • src/core/triangulation.rs
  • src/geometry/algorithms/convex_hull.rs
  • src/triangulation/delaunay.rs

Comment thread src/core/algorithms/flips.rs
Comment thread src/core/algorithms/incremental_insertion.rs
Comment thread src/core/util/facet_keys.rs Outdated
Comment thread src/topology/characteristics/validation.rs Outdated
- Preserve top-level retryability in insertion summaries and compare summaries by structured fields instead of display text.
- Surface flip-trial neighbor failures as typed NeighborValidationError variants rather than string payloads.
- Validate facet-key simplex arity before hashing and compare periodic neighbor facets through lifted identities.

BREAKING CHANGE: InsertionErrorSummary now includes a retryable field that direct struct literals must set, and flip-trial neighbor validation returns specific NeighborValidationError variants instead of Other strings for those failure modes.
@acgetchell
acgetchell disabled auto-merge May 13, 2026 16:19
@acgetchell
acgetchell merged commit e187cf0 into main May 13, 2026
23 checks passed
@acgetchell
acgetchell deleted the refactor/typed-validation-exact-predicates branch May 13, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api breaking change bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request geometry Geometry-related issues rust Pull requests that update rust code topology

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant