The "loop over triangles, key edges by canonical-vertex pair,
count uses, classify boundary/manifold/non-manifold" pass was
duplicated across cli_mesh_info, cli_world_info, and the new
cli_audits watertight check. Hoist it into cli_weld as
classifyEdges(indices, canon) returning an EdgeStats struct
with boundary / manifold / nonManifold counters and a
watertight() convenience method.
All three callers verified byte-identical:
• --info-mesh-stats firepit: 180 edges, watertight YES
• --info-wob-stats cube: 18 manifold, watertight YES
• --audit-watertight /tmp/...: 61 meshes, 12 failures, rc=12
About 60 more lines of duplication removed; classifyEdges +
buildWeldMap together form the complete reusable surface for
new weld/topology audit commands.
Three callers were each open-coding the same quantize-and-
bucket pass over vertex positions: --info-mesh-stats,
--info-wob-stats, and --bake-wom-collision. Move the
implementation to cli_weld.{hpp,cpp} as buildWeldMap() and
have each caller pass a flat positions array.
Identical std::map-based exact-equality keying preserved
(unbalanced-hash collisions remain absent). All three call
sites verified to produce byte-identical output:
• info-mesh-stats firepit: 240→80 verts, 0 boundary
• info-wob-stats cube: 8→8 verts, watertight YES
• bake-wom-collision tent: 18→6 verts, 8-tri WOC
About 75 lines of duplication removed; future weld-using
commands now opt in by including one header.