# moonlight-triangulation architecture laws
`moonlight-triangulation.cabal` is the authoritative component, module, and
dependency graph. Duplicating that inventory here only manufactures a stale
second owner. This document records the laws the manifest cannot express.
## Geometry has one owner
The coordinate pages `triPointX` and `triPointY` own vertex geometry. Vertex
payloads are independent annotations: `setVertexData` and `mapVertices` may
change them without moving a vertex. `triPointIndex` is a derived lookup over
the coordinate pages.
Construction may read a position from an incoming payload through
`HasPosition`; no operation may later reconstruct geometry from that payload.
The versioned serialization boundary therefore persists coordinate pages and
payloads independently, then derives only the point index on decode.
## Topology is finite and mutation is sealed
The half-edge DCEL is a structure of arrays. Twin half-edges are arithmetic
complements (`e xor 1`), and every inner face has a three-edge cycle. Persistent
operations thaw into `MutableDcel` in `ST`, perform a bounded local rewrite, and
freeze the result; mutable references never escape.
Validation descends in two stages. Structural cardinality, range, link, face,
vertex, point-index, and Euler obligations must glue first. Geometric
orientation and Delaunay checks run only after that structural section is
valid, because reading geometry through malformed links would turn a reported
obstruction into an indexing crash.
## Constrained regions descend by barrier parity
For a constrained triangulation, a face's region is determined by the minimum
number of constrained edges crossed from the outer face. Even depths are
outside: depth zero is the exterior, depth one the first bounded domain, depth
two a hole, and so on. A dangling constraint can be walked around at depth
zero.
`FloodFillIterator.facesAtEvenBarrierDepth` owns this derived classification.
CDT queries and refinement import it; neither maintains a second definition of
"outside."
## Component cuts justify dependencies
Sublibraries are proof boundaries, not size buckets. In particular,
`serialize` alone owns the `binary` and `bytestring` dependencies. Modules named
`Internal` may be exposed only where Cabal sublibraries must cross them; that
exposure does not make them a supported public API.
## The surface states a theory
`Moonlight.Triangulation` is one export list read as the statement of a theory,
in eight sections: the object, generation, the annotation functor, the
finite-set algebra and its normal form, the constraint layer, refinement,
observations, discharge. A name reaches the surface because a law mentions it.
The headline law is a homomorphism from finite site sets to canonical
observations of triangulations,
publish (A ∪ B) = canonicalize (union (publish A) (publish B))
where `publish` sends a site set through `delaunay` and then through
`canonicalize . mapVertices (const ())`. A raw build or local union may retain
the schedule that produced it, so structural `Eq` compares resident
representations while the finite-set laws compare their canonical,
payload-free images. `canonicalize` is that explicit physical normal form, and
`mapVertices (const ())` is the arrow onto the carrier used by the
geometry-only laws. Annotated `union` and `unions` glue equal-site payloads
through `JoinSemilattice` without allowing payloads to author geometry.
The exact support order is observed by `siteRelation`: equality, either proper
subset direction, disjointness, or a positive partial-overlap count. Restriction
descends through the same private site-set owner. `intersectionWith` combines
the left and right annotations only where both supports contain the coordinate;
`difference` preserves the left annotation; `symmetricDifference` preserves
the annotation of whichever exclusive support contains the coordinate. All
three publish through the same canonical rebuild boundary as the geometry-only
operations.
The generator is named `delaunay` because the equation is false of
triangulations in general and true of Delaunay triangulations. The name has to
make the law true.
The topology arena is finite, so union is not total. `union` and `unions`
therefore return `Either BuildError`; arena exhaustion is a typed obstruction,
not a lawful `Semigroup` instance with a trapdoor beneath it.
The constrained layer is not a total semilattice and does not pretend to be
one. Its
extension is the pair lattice of site sets and segment sets under componentwise
union, on which `constrainedDelaunay` is a partial homomorphism restricted to
the realizable sublattice: partiality kills `Semigroup`, and greedy
totalization kills associativity. `unionConstrained` is therefore surfaced as
an explicit operation returning `ConstrainedUnionError`, never as a typeclass
instance.
## Tiers govern names at their export site
Four tiers, and a name's tier is where it is exported rather than where it is
defined — a surface name may be defined below without lowering its tier.
The facade is the surface, and a subtraction there is a major version. The
sublibrary components are a partition: no manifest outside this package names
one, the in-repo spade referent excepted, since it ships with the package it
measures. Non-`Internal` sublibrary modules are the machine room, semi-stable
and reached by consumers who accept that. `Internal` modules promise nothing.
## Results are values, not histories
`BuildResult` and `RefinementResult` are abstract, reached through named
projections; `buildStats` and the rest of the telemetry stay below the wall.
Neither derives `Eq` or `Show`, because a derived instance observes through a
facade that hides — equality would consult counters the surface does not admit
exist.
## One concept has one owner
`isConstraintEdge` is owned by `Dcel`, polymorphic in the constraint mode;
`Cdt` does not restate it, and no alias survives. `innerFaceVertices` remains a
genuine collision — `Dcel`'s takes raw identifiers and answers `Maybe`, while
`Handles.Dynamic`'s takes and answers handles. Neither is surfaced, so the
collision stays in the machine room and callers name the module they mean.
## Vocabulary precedes representation
`Internal.Types` holds the vocabulary the surface names, none of which mentions
the stored mesh. `Internal.Representation` holds the structure of arrays, the
payload traversals, and the records that carry a built mesh beside its
telemetry. Representation imports vocabulary; the reverse import does not
exist. A type whose definition mentions the `Triangulation` record belongs to
Representation, which is why the result records live there.
## Mechanical acceptance
The native suite checks geometry and topology laws without depending on
`serialize`; the serialization suite checks the binary boundary separately. The
algebra suite is three slices with three dependency sets: fixtures, the laws
stated against the facade alone, and the agreement between the seam schedule
and the reference rebuild, which names an internal schedule and travels with
it. The parallel suite owns tournament behavior. The separate coherence test
imports every slice against the union of their dependencies at `-O0`; it detects
module and instance collisions but does not execute the behavioral suites a
second time.
The package's `warnings-as-errors` flag and `weeder.toml` turn compiler warnings
and unreachable internal declarations into refusals. The dedicated GitHub
workflow selects all library components from Cabal's generated plan rather than
restating build directories by hand.
## Published documentation is assembled, not generated
Cabal writes a Haddock interface for every sublibrary and hands none of them to
the siblings. The facade re-exports names it therefore cannot resolve, and
renders them as text. Cabal also writes one documentation tarball per
component, each under the same name, so the components overwrite one another
and only the last to run survives.
Hackage adds conditions of its own. A module on the package page links to its
documentation only when that page sits at the documentation root, so Cabal's
per-sublibrary subdirectories leave every module listed and unreachable. A
tarball is refused outright for a colon in any filename — which is how Cabal
names the sublibrary Hoogle databases — and for the extended attributes a macOS
`tar` attaches unbidden.
None of this is a defect in the component cuts. The repair changes no module,
no export, and no dependency; it is entirely downstream of the manifest, which
is why the manifest cannot express it. `release/hackage-docs.sh` owns it:
regenerate the facade against every sibling interface, flatten the module pages
to one root, cut a portable tarball. A release that skips the script publishes
a surface of unlinked names.