Skip to content

Adopt parse-don't-validate architecture for domain invariants #101

Description

@acgetchell

Current v1.0.0 plan (2026-07-22)

Starts after: #103. Blocks: #102.

For v1.0.0, limit this architecture work to release-critical boundaries where invalid state remains possible: applicable-move preparation/execution, structured move outcomes, runtime configuration, and persistence inputs. Establish the invariant-bearing move boundary that #102 needs for safe CGAL flip integration.

Do not pursue a whole-repository wrapper-type migration, abstraction cleanup, or speculative redesign. Every new type must prevent a concrete invalid state, preserve behavior, and carry deterministic tests and performance evidence proportionate to the affected path.

Native GitHub dependency metadata is the source of truth for ordering.


Summary

Adopt a "parse, don't validate" architecture across CDT++ using C++23 invariant-preserving domain types. Convert weakly typed or untrusted values into representations whose construction establishes the required invariants, then let internal algorithms operate without repeatedly rediscovering those facts.

Use std::expected, std::variant, strong wrapper types, private constructors, and concepts where they materially improve correctness. Preserve zero-overhead behavior, avoid unnecessary allocation, and migrate incrementally through reviewable PRs.

Motivation

The current code frequently carries weak representations—integers, enums, CGAL handles, strings, and mutable manifold state—through multiple layers and validates them again near mutation or output. This makes it difficult to distinguish malformed input, an inapplicable proposal, a valid Metropolis rejection, an execution failure, and an invalid resulting state.

The recent 2-to-3 move failure on macOS is representative: CGAL accepted a topologically legal facet flip that created a spacelike edge, while CDT requires the replacement edge to be timelike. The immediate guard fixes that path, but the stronger design is to construct an applicable 2-to-3 move only after its topological and causal preconditions have been proven.

The desired flow is:

raw input or candidate
  -> parse / prepare
  -> std::expected<ValidatedDomainType, DomainError>
  -> internal operation over validated state

Initial architecture

Introduce progressively stronger representations where useful:

RawMoveRequest
  -> ParsedMoveRequest
  -> ApplicableMove
  -> ExecutedMove / MoveOutcome

For heterogeneous valid states, consider a closed sum type such as:

using ApplicableMove = std::variant<
    ApplicableTwoThreeMove,
    ApplicableThreeTwoMove,
    ApplicableTwoSixMove,
    ApplicableSixTwoMove,
    ApplicableFourFourMove>;

This is a design direction, not a requirement to replace every enum. Retain simpler representations when they already make invalid states impossible and produce clearer code.

Scope

Domain inventory and boundaries

  • Inventory repeated validation and weakly typed domain values in public APIs, CLI/configuration parsing, persistence, topology construction, move preparation, dispatch, geometry updates, and simulation settings.
  • Identify the exact boundary where each invariant should be established and which internal checks become redundant afterward.
  • Define a migration map from raw representations to validated domain types without a flag-day rewrite.

Move preparation and execution

  • Separate move-kind selection, candidate resolution, applicability checking, Metropolis-Hastings evaluation, and atomic execution.
  • Introduce move-specific applicable types whose construction proves topology, causality, simplex classification, adjacency, and local admissibility requirements.
  • Replace stringly move failures with structured error or outcome types that distinguish no candidate, invalid topology, causal invalidity, stale candidate, Metropolis rejection, execution failure, and success where those distinctions are actionable.
  • Let execution consume an applicable move without repeating invariants already guaranteed by that type.
  • Feed typed proposal and outcome states into proposed, accepted, rejected, attempted, succeeded, and failed counters.
  • Preserve failure atomicity and canonical cache/geometry consistency.

Configuration, CLI, and persistence

  • Parse runtime options into validated configuration types with finite physical parameters and positive counts, intervals, radii, and foliation spacing.
  • Introduce strong types for semantically distinct IDs, counts, dimensions, time slices, and physical parameters where interchange is currently possible and harmful.
  • Parse file input into fully validated records before constructing or mutating canonical manifold state.
  • Make unsupported topology, dimensionality, malformed input, narrowing, and serialization failures explicit at the boundary.

Representation and performance

  • Prefer value types with the same representation and calling cost as their underlying scalar where possible.
  • Keep std::expected and std::variant alternatives inline and reasonably sized.
  • Avoid heap allocation, type erasure, or ownership indirection unless measurements justify it.
  • Pass rich validated values by reference or move them rather than copying unnecessarily.
  • Treat CGAL handle lifetime as an explicit invariant: applicable-move objects must not retain handles across mutations that invalidate them.
  • Establish focused benchmarks or repeatable timing baselines before migrating performance-sensitive move paths.

Suggested PR sequence

  1. Document the invariant inventory, target boundaries, error taxonomy, and performance baseline.
  2. Introduce foundational strong scalar/domain types and conversion tests.
  3. Implement applicable types for the 2-to-3 / 3-to-2 move pair.
  4. Extend the proven move design to 2-to-6 / 6-to-2 and 4-to-4.
  5. Integrate typed move states with MoveCommand, Metropolis-Hastings accounting, and atomic mutation.
  6. Parse CLI and simulation settings into validated configuration types.
  7. Parse persistence input into validated records before manifold construction.
  8. Remove duplicated validation made unnecessary by the new boundaries and publish performance results.

The ordering may change after the invariant inventory, but each PR should preserve behavior, keep tests green, and leave a coherent intermediate architecture.

Design constraints

  • Do not introduce wrapper types without a concrete invariant or category-error benefit.
  • Do not use std::variant merely to eliminate an enum and switch.
  • Do not claim exhaustive handling unless all alternatives are intentionally covered and tested.
  • Do not store unstable CGAL handles beyond their documented mutation boundary.
  • Do not weaken debug, sanitizer, topology, causality, or scientific correctness checks while moving validation to boundaries.
  • Do not change scientific behavior except where an existing state or transition violates the documented CDT contract.
  • Prefer typed, actionable errors over free-form strings at programmatic boundaries; format human-readable diagnostics at the presentation boundary.

Acceptance criteria

  • Each migrated boundary returns or constructs a type that encodes its documented invariants.
  • Invalid values and illegal move configurations cannot produce the corresponding validated/applicable type.
  • Internal operations consume validated types and do not repeat the same validation without a documented mutation or trust boundary.
  • Typed move outcomes distinguish inapplicable proposals, Metropolis rejections, execution attempts, successes, and failures.
  • Failed parsing, preparation, or execution leaves topology, geometry, caches, and configuration state unchanged.
  • Prepared-move lifetime and CGAL handle-invalidation rules are explicit and covered by tests.
  • Deterministic tests cover valid and invalid construction, boundary errors, each supported move, and inverse-move behavior where applicable.
  • Sanitizer and supported CI matrices pass throughout the migration.
  • Benchmarks show no meaningful regression in representative initialization, move-selection, move-execution, and simulation workloads.
  • No unnecessary heap allocation is introduced on performance-sensitive validated paths.
  • Public behavior remains compatible except for clearer diagnostics and rejection of states that violate existing documented invariants.
  • The final code documents which invariants are guaranteed by each domain type and where those guarantees are established.

Related work

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions